[Rest Api] Fix find duplicates endpoint (#12044)
- fix endpoint - migrate to new rest api v2 service - add integration test
This commit is contained in:
@ -1,129 +0,0 @@
|
|||||||
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 { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.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,
|
|
||||||
UserRoleService,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
.overrideProvider(WorkspaceQueryHookService)
|
|
||||||
.useValue({})
|
|
||||||
.overrideProvider(QueryRunnerArgsFactory)
|
|
||||||
.useValue({})
|
|
||||||
.overrideProvider(QueryResultGettersFactory)
|
|
||||||
.useValue({})
|
|
||||||
.overrideProvider(ApiEventEmitterService)
|
|
||||||
.useValue({})
|
|
||||||
.overrideProvider(TwentyORMGlobalManager)
|
|
||||||
.useValue({})
|
|
||||||
.overrideProvider(ProcessNestedRelationsHelper)
|
|
||||||
.useValue({})
|
|
||||||
.overrideProvider(FeatureFlagService)
|
|
||||||
.useValue({})
|
|
||||||
.overrideProvider(PermissionsService)
|
|
||||||
.useValue({})
|
|
||||||
.overrideProvider(UserRoleService)
|
|
||||||
.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',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -9,7 +9,6 @@ import {
|
|||||||
} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service';
|
} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service';
|
||||||
import {
|
import {
|
||||||
ObjectRecord,
|
ObjectRecord,
|
||||||
ObjectRecordFilter,
|
|
||||||
OrderByDirection,
|
OrderByDirection,
|
||||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||||
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
|
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
|
||||||
@ -22,14 +21,10 @@ import {
|
|||||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||||
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 { 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 {
|
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||||
formatResult,
|
import { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils';
|
||||||
getCompositeFieldMetadataMap,
|
|
||||||
} from 'src/engine/twenty-orm/utils/format-result.util';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseResolverService<
|
export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseResolverService<
|
||||||
@ -90,7 +85,7 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR
|
|||||||
|
|
||||||
const duplicateConnections: IConnection<ObjectRecord>[] = await Promise.all(
|
const duplicateConnections: IConnection<ObjectRecord>[] = await Promise.all(
|
||||||
objectRecords.map(async (record) => {
|
objectRecords.map(async (record) => {
|
||||||
const duplicateConditions = this.buildDuplicateConditions(
|
const duplicateConditions = buildDuplicateConditions(
|
||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
[record],
|
[record],
|
||||||
record.id,
|
record.id,
|
||||||
@ -143,72 +138,6 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR
|
|||||||
return duplicateConnections;
|
return duplicateConnections;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildDuplicateConditions(
|
|
||||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
|
|
||||||
records?: Partial<ObjectRecord>[] | undefined,
|
|
||||||
filteringByExistingRecordId?: string,
|
|
||||||
): Partial<ObjectRecordFilter> {
|
|
||||||
if (!records || records.length === 0) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const criteriaCollection =
|
|
||||||
objectMetadataItemWithFieldMaps.duplicateCriteria || [];
|
|
||||||
|
|
||||||
const formattedRecords = formatData(
|
|
||||||
records,
|
|
||||||
objectMetadataItemWithFieldMaps,
|
|
||||||
);
|
|
||||||
|
|
||||||
const compositeFieldMetadataMap = getCompositeFieldMetadataMap(
|
|
||||||
objectMetadataItemWithFieldMaps,
|
|
||||||
);
|
|
||||||
|
|
||||||
const conditions = formattedRecords.flatMap((record) => {
|
|
||||||
const criteriaWithMatchingArgs = criteriaCollection.filter((criteria) =>
|
|
||||||
criteria.every((columnName) => {
|
|
||||||
const value = record[columnName] as string | undefined;
|
|
||||||
|
|
||||||
return (
|
|
||||||
value && value.length >= settings.minLengthOfStringForDuplicateCheck
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return criteriaWithMatchingArgs.map((criteria) => {
|
|
||||||
const condition = {};
|
|
||||||
|
|
||||||
criteria.forEach((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;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const filter: Partial<ObjectRecordFilter> = {};
|
|
||||||
|
|
||||||
if (conditions && !isEmpty(conditions)) {
|
|
||||||
filter.or = conditions;
|
|
||||||
|
|
||||||
if (filteringByExistingRecordId) {
|
|
||||||
filter.id = { neq: filteringByExistingRecordId };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
async validate(
|
async validate(
|
||||||
args: FindDuplicatesResolverArgs,
|
args: FindDuplicatesResolverArgs,
|
||||||
_options: WorkspaceQueryRunnerOptions,
|
_options: WorkspaceQueryRunnerOptions,
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import {
|
|||||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||||
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 { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
|
import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
|
||||||
import { computeCursorArgFilter } from 'src/engine/api/graphql/graphql-query-runner/utils/compute-cursor-arg-filter';
|
import { computeCursorArgFilter } from 'src/engine/api/utils/compute-cursor-arg-filter.utils';
|
||||||
import {
|
import {
|
||||||
getCursor,
|
getCursor,
|
||||||
getPaginationInfo,
|
getPaginationInfo,
|
||||||
|
|||||||
@ -13,10 +13,8 @@ import {
|
|||||||
|
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
import { RestApiCoreServiceV2 } from 'src/engine/api/rest/core/rest-api-core-v2.service';
|
import { RestApiCoreService } from 'src/engine/api/rest/core/services/rest-api-core.service';
|
||||||
import { RestApiCoreService } from 'src/engine/api/rest/core/rest-api-core.service';
|
|
||||||
import { RestApiExceptionFilter } from 'src/engine/api/rest/rest-api-exception.filter';
|
import { RestApiExceptionFilter } from 'src/engine/api/rest/rest-api-exception.filter';
|
||||||
import { cleanGraphQLResponse } from 'src/engine/api/rest/utils/clean-graphql-response.utils';
|
|
||||||
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
|
||||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||||
|
|
||||||
@ -24,49 +22,46 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
|||||||
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
|
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
|
||||||
@UseFilters(RestApiExceptionFilter)
|
@UseFilters(RestApiExceptionFilter)
|
||||||
export class RestApiCoreController {
|
export class RestApiCoreController {
|
||||||
constructor(
|
constructor(private readonly restApiCoreService: RestApiCoreService) {}
|
||||||
private readonly restApiCoreService: RestApiCoreService,
|
|
||||||
private readonly restApiCoreServiceV2: RestApiCoreServiceV2,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Post('batch/*')
|
@Post('batch/*')
|
||||||
async handleApiPostBatch(@Req() request: Request, @Res() res: Response) {
|
async handleApiPostBatch(@Req() request: Request, @Res() res: Response) {
|
||||||
const result = await this.restApiCoreServiceV2.createMany(request);
|
const result = await this.restApiCoreService.createMany(request);
|
||||||
|
|
||||||
res.status(201).send(result);
|
res.status(201).send(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('duplicates')
|
@Post('*/duplicates')
|
||||||
async handleApiFindDuplicates(@Req() request: Request, @Res() res: Response) {
|
async handleApiFindDuplicates(@Req() request: Request, @Res() res: Response) {
|
||||||
const result = await this.restApiCoreService.findDuplicates(request);
|
const result = await this.restApiCoreService.findDuplicates(request);
|
||||||
|
|
||||||
res.status(200).send(cleanGraphQLResponse(result.data.data));
|
res.status(200).send(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('*')
|
@Post('*')
|
||||||
async handleApiPost(@Req() request: Request, @Res() res: Response) {
|
async handleApiPost(@Req() request: Request, @Res() res: Response) {
|
||||||
const result = await this.restApiCoreServiceV2.createOne(request);
|
const result = await this.restApiCoreService.createOne(request);
|
||||||
|
|
||||||
res.status(201).send(result);
|
res.status(201).send(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('*')
|
@Get('*')
|
||||||
async handleApiGet(@Req() request: Request, @Res() res: Response) {
|
async handleApiGet(@Req() request: Request, @Res() res: Response) {
|
||||||
const result = await this.restApiCoreServiceV2.get(request);
|
const result = await this.restApiCoreService.get(request);
|
||||||
|
|
||||||
res.status(200).send(result);
|
res.status(200).send(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete('*')
|
@Delete('*')
|
||||||
async handleApiDelete(@Req() request: Request, @Res() res: Response) {
|
async handleApiDelete(@Req() request: Request, @Res() res: Response) {
|
||||||
const result = await this.restApiCoreServiceV2.delete(request);
|
const result = await this.restApiCoreService.delete(request);
|
||||||
|
|
||||||
res.status(200).send(result);
|
res.status(200).send(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch('*')
|
@Patch('*')
|
||||||
async handleApiPatch(@Req() request: Request, @Res() res: Response) {
|
async handleApiPatch(@Req() request: Request, @Res() res: Response) {
|
||||||
const result = await this.restApiCoreServiceV2.update(request);
|
const result = await this.restApiCoreService.update(request);
|
||||||
|
|
||||||
res.status(200).send(result);
|
res.status(200).send(result);
|
||||||
}
|
}
|
||||||
@ -76,7 +71,7 @@ export class RestApiCoreController {
|
|||||||
// of PATCH, and because the PUT verb is often used as a PATCH.
|
// of PATCH, and because the PUT verb is often used as a PATCH.
|
||||||
@Put('*')
|
@Put('*')
|
||||||
async handleApiPut(@Req() request: Request, @Res() res: Response) {
|
async handleApiPut(@Req() request: Request, @Res() res: Response) {
|
||||||
const result = await this.restApiCoreServiceV2.update(request);
|
const result = await this.restApiCoreService.update(request);
|
||||||
|
|
||||||
res.status(200).send(result);
|
res.status(200).send(result);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,147 @@
|
|||||||
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { Request } from 'express';
|
||||||
|
import isEmpty from 'lodash.isempty';
|
||||||
|
import { In } from 'typeorm';
|
||||||
|
|
||||||
|
import {
|
||||||
|
FormatResult,
|
||||||
|
RestApiBaseHandler,
|
||||||
|
} from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||||
|
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||||
|
|
||||||
|
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||||
|
import { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RestApiFindDuplicatesHandler extends RestApiBaseHandler {
|
||||||
|
async handle(request: Request) {
|
||||||
|
this.validate(request);
|
||||||
|
|
||||||
|
const {
|
||||||
|
objectMetadataNameSingular,
|
||||||
|
repository,
|
||||||
|
objectMetadata,
|
||||||
|
objectMetadataItemWithFieldsMaps,
|
||||||
|
} = await this.getRepositoryAndMetadataOrFail(request);
|
||||||
|
|
||||||
|
const existingRecordsQueryBuilder = repository.createQueryBuilder(
|
||||||
|
objectMetadataItemWithFieldsMaps.nameSingular,
|
||||||
|
);
|
||||||
|
|
||||||
|
let objectRecords: Partial<ObjectRecord>[] = [];
|
||||||
|
|
||||||
|
if (request.body.ids) {
|
||||||
|
const nonFormattedObjectRecords = (await existingRecordsQueryBuilder
|
||||||
|
.where({ id: In(request.body.ids) })
|
||||||
|
.getMany()) as ObjectRecord[];
|
||||||
|
|
||||||
|
objectRecords = formatResult(
|
||||||
|
nonFormattedObjectRecords,
|
||||||
|
objectMetadataItemWithFieldsMaps,
|
||||||
|
objectMetadata.objectMetadataMaps,
|
||||||
|
);
|
||||||
|
} else if (request.body.data && !isEmpty(request.body.data)) {
|
||||||
|
objectRecords = request.body.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const duplicateConditions = objectRecords.map((record) =>
|
||||||
|
buildDuplicateConditions(
|
||||||
|
objectMetadataItemWithFieldsMaps,
|
||||||
|
[record],
|
||||||
|
record.id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result: { data: FormatResult[] } = {
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const duplicateCondition of duplicateConditions) {
|
||||||
|
const {
|
||||||
|
records,
|
||||||
|
isForwardPagination,
|
||||||
|
hasMoreRecords,
|
||||||
|
totalCount,
|
||||||
|
startCursor,
|
||||||
|
endCursor,
|
||||||
|
} = await this.findRecords({
|
||||||
|
request,
|
||||||
|
repository,
|
||||||
|
objectMetadata,
|
||||||
|
objectMetadataNameSingular,
|
||||||
|
objectMetadataItemWithFieldsMaps,
|
||||||
|
extraFilters: duplicateCondition,
|
||||||
|
});
|
||||||
|
|
||||||
|
const paginatedResult = this.formatPaginatedDuplicatesResult({
|
||||||
|
finalRecords: records,
|
||||||
|
objectMetadataNameSingular,
|
||||||
|
isForwardPagination,
|
||||||
|
hasMoreRecords,
|
||||||
|
totalCount,
|
||||||
|
startCursor,
|
||||||
|
endCursor,
|
||||||
|
});
|
||||||
|
|
||||||
|
result.data.push(paginatedResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private validate(request: Request) {
|
||||||
|
const { data, ids } = request.body;
|
||||||
|
|
||||||
|
if (!data && !ids) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'You have to provide either "data" or "ids" argument',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data && ids) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'You cannot provide both "data" and "ids" arguments',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ids && isEmpty(data)) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'The "data" condition can not be empty when "ids" input not provided',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatPaginatedDuplicatesResult({
|
||||||
|
finalRecords,
|
||||||
|
objectMetadataNameSingular,
|
||||||
|
isForwardPagination,
|
||||||
|
hasMoreRecords,
|
||||||
|
totalCount,
|
||||||
|
startCursor,
|
||||||
|
endCursor,
|
||||||
|
}: {
|
||||||
|
finalRecords: any[];
|
||||||
|
objectMetadataNameSingular: string;
|
||||||
|
isForwardPagination: boolean;
|
||||||
|
hasMoreRecords: boolean;
|
||||||
|
totalCount: number;
|
||||||
|
startCursor: string | null;
|
||||||
|
endCursor: string | null;
|
||||||
|
}) {
|
||||||
|
const hasPreviousPage = !isForwardPagination && hasMoreRecords;
|
||||||
|
|
||||||
|
return this.formatResult({
|
||||||
|
operation: 'findDuplicates',
|
||||||
|
objectNameSingular: objectMetadataNameSingular,
|
||||||
|
data: isForwardPagination ? finalRecords : finalRecords.reverse(),
|
||||||
|
pageInfo: {
|
||||||
|
hasNextPage: isForwardPagination && hasMoreRecords,
|
||||||
|
...(hasPreviousPage ? { hasPreviousPage } : {}),
|
||||||
|
startCursor,
|
||||||
|
endCursor,
|
||||||
|
},
|
||||||
|
totalCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,7 +5,7 @@ import { Request } from 'express';
|
|||||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RestApiGetManyHandler extends RestApiBaseHandler {
|
export class RestApiFindManyHandler extends RestApiBaseHandler {
|
||||||
async handle(request: Request) {
|
async handle(request: Request) {
|
||||||
const {
|
const {
|
||||||
objectMetadataNameSingular,
|
objectMetadataNameSingular,
|
||||||
@ -40,4 +40,37 @@ export class RestApiGetManyHandler extends RestApiBaseHandler {
|
|||||||
endCursor,
|
endCursor,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
formatPaginatedResult({
|
||||||
|
finalRecords,
|
||||||
|
objectMetadataNamePlural,
|
||||||
|
isForwardPagination,
|
||||||
|
hasMoreRecords,
|
||||||
|
totalCount,
|
||||||
|
startCursor,
|
||||||
|
endCursor,
|
||||||
|
}: {
|
||||||
|
finalRecords: any[];
|
||||||
|
objectMetadataNamePlural: string;
|
||||||
|
isForwardPagination: boolean;
|
||||||
|
hasMoreRecords: boolean;
|
||||||
|
totalCount: number;
|
||||||
|
startCursor: string | null;
|
||||||
|
endCursor: string | null;
|
||||||
|
}) {
|
||||||
|
const hasPreviousPage = !isForwardPagination && hasMoreRecords;
|
||||||
|
|
||||||
|
return this.formatResult({
|
||||||
|
operation: 'findMany',
|
||||||
|
objectNamePlural: objectMetadataNamePlural,
|
||||||
|
data: isForwardPagination ? finalRecords : finalRecords.reverse(),
|
||||||
|
pageInfo: {
|
||||||
|
hasNextPage: isForwardPagination && hasMoreRecords,
|
||||||
|
...(hasPreviousPage ? { hasPreviousPage } : {}),
|
||||||
|
startCursor,
|
||||||
|
endCursor,
|
||||||
|
},
|
||||||
|
totalCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -8,7 +8,7 @@ import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api
|
|||||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RestApiGetOneHandler extends RestApiBaseHandler {
|
export class RestApiFindOneHandler extends RestApiBaseHandler {
|
||||||
async handle(request: Request) {
|
async handle(request: Request) {
|
||||||
const { id: recordId } = parseCorePath(request);
|
const { id: recordId } = parseCorePath(request);
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.
|
|||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
import { formatResult as formatGetManyData } from 'src/engine/twenty-orm/utils/format-result.util';
|
import { formatResult as formatGetManyData } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||||
import { encodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
|
import { encodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
|
||||||
import { computeCursorArgFilter } from 'src/engine/api/graphql/graphql-query-runner/utils/compute-cursor-arg-filter';
|
import { computeCursorArgFilter } from 'src/engine/api/utils/compute-cursor-arg-filter.utils';
|
||||||
|
|
||||||
export interface PageInfo {
|
export interface PageInfo {
|
||||||
hasNextPage?: boolean;
|
hasNextPage?: boolean;
|
||||||
@ -41,7 +41,13 @@ export interface PageInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface FormatResultParams<T> {
|
interface FormatResultParams<T> {
|
||||||
operation: 'delete' | 'create' | 'update' | 'findOne' | 'findMany';
|
operation:
|
||||||
|
| 'delete'
|
||||||
|
| 'create'
|
||||||
|
| 'update'
|
||||||
|
| 'findOne'
|
||||||
|
| 'findMany'
|
||||||
|
| 'findDuplicates';
|
||||||
objectNameSingular?: string;
|
objectNameSingular?: string;
|
||||||
objectNamePlural?: string;
|
objectNamePlural?: string;
|
||||||
data: T;
|
data: T;
|
||||||
@ -50,7 +56,7 @@ interface FormatResultParams<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface FormatResult {
|
export interface FormatResult {
|
||||||
data: {
|
data?: {
|
||||||
[operation: string]: object;
|
[operation: string]: object;
|
||||||
};
|
};
|
||||||
pageInfo?: PageInfo;
|
pageInfo?: PageInfo;
|
||||||
@ -73,7 +79,9 @@ export abstract class RestApiBaseHandler {
|
|||||||
@Inject()
|
@Inject()
|
||||||
protected readonly apiEventEmitterService: ApiEventEmitterService;
|
protected readonly apiEventEmitterService: ApiEventEmitterService;
|
||||||
|
|
||||||
protected abstract handle(request: Request): Promise<FormatResult>;
|
protected abstract handle(
|
||||||
|
request: Request,
|
||||||
|
): Promise<FormatResult | { data: FormatResult[] }>;
|
||||||
|
|
||||||
public async getRepositoryAndMetadataOrFail(request: Request) {
|
public async getRepositoryAndMetadataOrFail(request: Request) {
|
||||||
const { workspace, apiKey, userWorkspaceId } = request;
|
const { workspace, apiKey, userWorkspaceId } = request;
|
||||||
@ -107,6 +115,12 @@ export abstract class RestApiBaseHandler {
|
|||||||
objectMetadataNameSingular,
|
objectMetadataNameSingular,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!isDefined(objectMetadataItemWithFieldsMaps)) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Object metadata item with name singular ${objectMetadataNameSingular} not found`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const shouldBypassPermissionChecks = !!apiKey;
|
const shouldBypassPermissionChecks = !!apiKey;
|
||||||
|
|
||||||
const roleId =
|
const roleId =
|
||||||
@ -238,53 +252,28 @@ export abstract class RestApiBaseHandler {
|
|||||||
prefix = objectNameSingular || '';
|
prefix = objectNameSingular || '';
|
||||||
} else if (operation === 'findMany') {
|
} else if (operation === 'findMany') {
|
||||||
prefix = objectNamePlural || '';
|
prefix = objectNamePlural || '';
|
||||||
|
} else if (operation === 'findDuplicates') {
|
||||||
|
prefix = `${objectNameSingular}Duplicates`;
|
||||||
} else {
|
} else {
|
||||||
prefix =
|
prefix =
|
||||||
operation + capitalize(objectNameSingular || objectNamePlural || '');
|
operation + capitalize(objectNameSingular || objectNamePlural || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: {
|
...(operation === 'findDuplicates'
|
||||||
[prefix]: data,
|
? {
|
||||||
},
|
[prefix]: data,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
data: {
|
||||||
|
[prefix]: data,
|
||||||
|
},
|
||||||
|
}),
|
||||||
...(isDefined(pageInfo) ? { pageInfo } : {}),
|
...(isDefined(pageInfo) ? { pageInfo } : {}),
|
||||||
...(isDefined(totalCount) ? { totalCount } : {}),
|
...(isDefined(totalCount) ? { totalCount } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
formatPaginatedResult({
|
|
||||||
finalRecords,
|
|
||||||
objectMetadataNamePlural,
|
|
||||||
isForwardPagination,
|
|
||||||
hasMoreRecords,
|
|
||||||
totalCount,
|
|
||||||
startCursor,
|
|
||||||
endCursor,
|
|
||||||
}: {
|
|
||||||
finalRecords: any[];
|
|
||||||
objectMetadataNamePlural: string;
|
|
||||||
isForwardPagination: boolean;
|
|
||||||
hasMoreRecords: boolean;
|
|
||||||
totalCount: number;
|
|
||||||
startCursor: string | null;
|
|
||||||
endCursor: string | null;
|
|
||||||
}) {
|
|
||||||
const hasPreviousPage = !isForwardPagination && hasMoreRecords;
|
|
||||||
|
|
||||||
return this.formatResult({
|
|
||||||
operation: 'findMany',
|
|
||||||
objectNamePlural: objectMetadataNamePlural,
|
|
||||||
data: isForwardPagination ? finalRecords : finalRecords.reverse(),
|
|
||||||
pageInfo: {
|
|
||||||
hasNextPage: isForwardPagination && hasMoreRecords,
|
|
||||||
...(hasPreviousPage ? { hasPreviousPage } : {}),
|
|
||||||
startCursor,
|
|
||||||
endCursor,
|
|
||||||
},
|
|
||||||
totalCount,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async findRecords({
|
async findRecords({
|
||||||
request,
|
request,
|
||||||
recordId,
|
recordId,
|
||||||
@ -292,6 +281,7 @@ export abstract class RestApiBaseHandler {
|
|||||||
objectMetadata,
|
objectMetadata,
|
||||||
objectMetadataNameSingular,
|
objectMetadataNameSingular,
|
||||||
objectMetadataItemWithFieldsMaps,
|
objectMetadataItemWithFieldsMaps,
|
||||||
|
extraFilters,
|
||||||
}: {
|
}: {
|
||||||
request: Request;
|
request: Request;
|
||||||
recordId?: string;
|
recordId?: string;
|
||||||
@ -304,6 +294,7 @@ export abstract class RestApiBaseHandler {
|
|||||||
objectMetadataItemWithFieldsMaps:
|
objectMetadataItemWithFieldsMaps:
|
||||||
| ObjectMetadataItemWithFieldMaps
|
| ObjectMetadataItemWithFieldMaps
|
||||||
| undefined;
|
| undefined;
|
||||||
|
extraFilters?: Partial<ObjectRecordFilter>;
|
||||||
}) {
|
}) {
|
||||||
const qb = repository.createQueryBuilder(objectMetadataNameSingular);
|
const qb = repository.createQueryBuilder(objectMetadataNameSingular);
|
||||||
|
|
||||||
@ -331,6 +322,7 @@ export abstract class RestApiBaseHandler {
|
|||||||
inputs,
|
inputs,
|
||||||
objectMetadata,
|
objectMetadata,
|
||||||
isForwardPagination,
|
isForwardPagination,
|
||||||
|
extraFilters,
|
||||||
});
|
});
|
||||||
|
|
||||||
let selectQueryBuilder = isDefined(filters)
|
let selectQueryBuilder = isDefined(filters)
|
||||||
@ -409,6 +401,7 @@ export abstract class RestApiBaseHandler {
|
|||||||
inputs,
|
inputs,
|
||||||
objectMetadata,
|
objectMetadata,
|
||||||
isForwardPagination,
|
isForwardPagination,
|
||||||
|
extraFilters,
|
||||||
}: {
|
}: {
|
||||||
inputs: QueryVariables;
|
inputs: QueryVariables;
|
||||||
objectMetadata: {
|
objectMetadata: {
|
||||||
@ -416,9 +409,16 @@ export abstract class RestApiBaseHandler {
|
|||||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||||
};
|
};
|
||||||
isForwardPagination: boolean;
|
isForwardPagination: boolean;
|
||||||
|
extraFilters?: Partial<ObjectRecordFilter>;
|
||||||
}) {
|
}) {
|
||||||
let appliedFilters = inputs.filter;
|
let appliedFilters = inputs.filter;
|
||||||
|
|
||||||
|
if (extraFilters) {
|
||||||
|
appliedFilters = (appliedFilters
|
||||||
|
? { and: [appliedFilters, extraFilters] }
|
||||||
|
: extraFilters) as unknown as ObjectRecordFilter;
|
||||||
|
}
|
||||||
|
|
||||||
const cursor = inputs.startingAfter || inputs.endingBefore;
|
const cursor = inputs.startingAfter || inputs.endingBefore;
|
||||||
|
|
||||||
if (cursor) {
|
if (cursor) {
|
||||||
@ -429,9 +429,9 @@ export abstract class RestApiBaseHandler {
|
|||||||
isForwardPagination,
|
isForwardPagination,
|
||||||
);
|
);
|
||||||
|
|
||||||
appliedFilters = (inputs.filter
|
appliedFilters = (appliedFilters
|
||||||
? {
|
? {
|
||||||
and: [inputs.filter, { or: cursorArgFilter }],
|
and: [appliedFilters, { or: cursorArgFilter }],
|
||||||
}
|
}
|
||||||
: { or: cursorArgFilter }) as unknown as ObjectRecordFilter;
|
: { or: cursorArgFilter }) as unknown as ObjectRecordFilter;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,6 +27,14 @@ describe('parseCorePath', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw for wrong request', () => {
|
||||||
|
const request: any = { path: '/rest' };
|
||||||
|
|
||||||
|
expect(() => parseCorePath(request)).toThrow(
|
||||||
|
"Query path '/rest' invalid. Valid examples: /rest/companies/id or /rest/companies or /rest/batch/companies",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should parse object from batch request', () => {
|
it('should parse object from batch request', () => {
|
||||||
const request: any = { path: '/rest/batch/companies' };
|
const request: any = { path: '/rest/batch/companies' };
|
||||||
|
|
||||||
@ -43,4 +51,13 @@ describe('parseCorePath', () => {
|
|||||||
"Query path '/rest/batch/companies/uuid' invalid. Valid examples: /rest/companies/id or /rest/companies or /rest/batch/companies",
|
"Query path '/rest/batch/companies/uuid' invalid. Valid examples: /rest/companies/id or /rest/companies or /rest/batch/companies",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should parse object from duplicates request', () => {
|
||||||
|
const request: any = { path: '/rest/companies/duplicates' };
|
||||||
|
|
||||||
|
expect(parseCorePath(request)).toEqual({
|
||||||
|
object: 'companies',
|
||||||
|
id: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,7 +5,11 @@ import { Request } from 'express';
|
|||||||
export const parseCorePath = (
|
export const parseCorePath = (
|
||||||
request: Request,
|
request: Request,
|
||||||
): { object: string; id?: string } => {
|
): { object: string; id?: string } => {
|
||||||
const queryAction = request.path.replace('/rest/', '').split('/');
|
const queryAction = request.path
|
||||||
|
.replace('/rest/', '')
|
||||||
|
.replace('/rest', '')
|
||||||
|
.split('/')
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
if (queryAction.length > 2) {
|
if (queryAction.length > 2) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
@ -13,13 +17,23 @@ export const parseCorePath = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queryAction.length === 2 && queryAction[0] === 'batch') {
|
if (queryAction.length === 0) {
|
||||||
return { object: queryAction[1] };
|
throw new BadRequestException(
|
||||||
|
`Query path '${request.path}' invalid. Valid examples: /rest/companies/id or /rest/companies or /rest/batch/companies`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queryAction.length === 1) {
|
if (queryAction.length === 1) {
|
||||||
return { object: queryAction[0] };
|
return { object: queryAction[0] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (queryAction[0] === 'batch') {
|
||||||
|
return { object: queryAction[1] };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryAction[1] === 'duplicates') {
|
||||||
|
return { object: queryAction[0] };
|
||||||
|
}
|
||||||
|
|
||||||
return { object: queryAction[0], id: queryAction[1] };
|
return { object: queryAction[0], id: queryAction[1] };
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,8 +4,8 @@ import { HttpModule } from '@nestjs/axios';
|
|||||||
import { RestApiDeleteOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-delete-one.handler';
|
import { RestApiDeleteOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-delete-one.handler';
|
||||||
import { RestApiCreateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-one.handler';
|
import { RestApiCreateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-one.handler';
|
||||||
import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler';
|
import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler';
|
||||||
import { RestApiGetOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-get-one.handler';
|
import { RestApiFindOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-one.handler';
|
||||||
import { RestApiGetManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-get-many.handler';
|
import { RestApiFindManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-many.handler';
|
||||||
import { CoreQueryBuilderModule } from 'src/engine/api/rest/core/query-builder/core-query-builder.module';
|
import { CoreQueryBuilderModule } from 'src/engine/api/rest/core/query-builder/core-query-builder.module';
|
||||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||||
@ -13,20 +13,21 @@ import { RecordTransformerModule } from 'src/engine/core-modules/record-transfor
|
|||||||
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
||||||
import { RestApiCoreController } from 'src/engine/api/rest/core/controllers/rest-api-core.controller';
|
import { RestApiCoreController } from 'src/engine/api/rest/core/controllers/rest-api-core.controller';
|
||||||
import { coreQueryBuilderFactories } from 'src/engine/api/rest/core/query-builder/factories/factories';
|
import { coreQueryBuilderFactories } from 'src/engine/api/rest/core/query-builder/factories/factories';
|
||||||
import { RestApiCoreServiceV2 } from 'src/engine/api/rest/core/rest-api-core-v2.service';
|
import { RestApiCoreService } from 'src/engine/api/rest/core/services/rest-api-core.service';
|
||||||
import { RestApiCoreService } from 'src/engine/api/rest/core/rest-api-core.service';
|
|
||||||
import { RestApiService } from 'src/engine/api/rest/rest-api.service';
|
import { RestApiService } from 'src/engine/api/rest/rest-api.service';
|
||||||
import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service';
|
import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service';
|
||||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||||
import { RestApiCreateManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-many.handler';
|
import { RestApiCreateManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-many.handler';
|
||||||
|
import { RestApiFindDuplicatesHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler';
|
||||||
|
|
||||||
const restApiCoreResolvers = [
|
const restApiCoreResolvers = [
|
||||||
RestApiDeleteOneHandler,
|
RestApiDeleteOneHandler,
|
||||||
RestApiCreateOneHandler,
|
RestApiCreateOneHandler,
|
||||||
RestApiCreateManyHandler,
|
RestApiCreateManyHandler,
|
||||||
RestApiUpdateOneHandler,
|
RestApiUpdateOneHandler,
|
||||||
RestApiGetOneHandler,
|
RestApiFindOneHandler,
|
||||||
RestApiGetManyHandler,
|
RestApiFindManyHandler,
|
||||||
|
RestApiFindDuplicatesHandler,
|
||||||
];
|
];
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@ -43,7 +44,6 @@ const restApiCoreResolvers = [
|
|||||||
providers: [
|
providers: [
|
||||||
RestApiService,
|
RestApiService,
|
||||||
RestApiCoreService,
|
RestApiCoreService,
|
||||||
RestApiCoreServiceV2,
|
|
||||||
ApiEventEmitterService,
|
ApiEventEmitterService,
|
||||||
...coreQueryBuilderFactories,
|
...coreQueryBuilderFactories,
|
||||||
...restApiCoreResolvers,
|
...restApiCoreResolvers,
|
||||||
|
|||||||
@ -1,29 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { Request } from 'express';
|
|
||||||
|
|
||||||
import { CoreQueryBuilderFactory } from 'src/engine/api/rest/core/query-builder/core-query-builder.factory';
|
|
||||||
import {
|
|
||||||
GraphqlApiType,
|
|
||||||
RestApiService,
|
|
||||||
} from 'src/engine/api/rest/rest-api.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class RestApiCoreService {
|
|
||||||
constructor(
|
|
||||||
private readonly coreQueryBuilderFactory: CoreQueryBuilderFactory,
|
|
||||||
private readonly restApiService: RestApiService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async createMany(request: Request) {
|
|
||||||
const data = await this.coreQueryBuilderFactory.createMany(request);
|
|
||||||
|
|
||||||
return await this.restApiService.call(GraphqlApiType.CORE, request, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
async findDuplicates(request: Request) {
|
|
||||||
const data = await this.coreQueryBuilderFactory.findDuplicates(request);
|
|
||||||
|
|
||||||
return await this.restApiService.call(GraphqlApiType.CORE, request, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -7,19 +7,21 @@ import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path
|
|||||||
import { RestApiDeleteOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-delete-one.handler';
|
import { RestApiDeleteOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-delete-one.handler';
|
||||||
import { RestApiCreateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-one.handler';
|
import { RestApiCreateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-one.handler';
|
||||||
import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler';
|
import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler';
|
||||||
import { RestApiGetOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-get-one.handler';
|
import { RestApiFindOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-one.handler';
|
||||||
import { RestApiGetManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-get-many.handler';
|
import { RestApiFindManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-many.handler';
|
||||||
import { RestApiCreateManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-many.handler';
|
import { RestApiCreateManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-many.handler';
|
||||||
|
import { RestApiFindDuplicatesHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RestApiCoreServiceV2 {
|
export class RestApiCoreService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly restApiDeleteOneHandler: RestApiDeleteOneHandler,
|
private readonly restApiDeleteOneHandler: RestApiDeleteOneHandler,
|
||||||
private readonly restApiCreateOneHandler: RestApiCreateOneHandler,
|
private readonly restApiCreateOneHandler: RestApiCreateOneHandler,
|
||||||
private readonly restApiCreateManyHandler: RestApiCreateManyHandler,
|
private readonly restApiCreateManyHandler: RestApiCreateManyHandler,
|
||||||
private readonly restApiUpdateOneHandler: RestApiUpdateOneHandler,
|
private readonly restApiUpdateOneHandler: RestApiUpdateOneHandler,
|
||||||
private readonly restApiGetOneHandler: RestApiGetOneHandler,
|
private readonly restApiFindOneHandler: RestApiFindOneHandler,
|
||||||
private readonly restApiGetManyHandler: RestApiGetManyHandler,
|
private readonly restApiFindManyHandler: RestApiFindManyHandler,
|
||||||
|
private readonly restApiFindDuplicatesHandler: RestApiFindDuplicatesHandler,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async delete(request: Request) {
|
async delete(request: Request) {
|
||||||
@ -34,6 +36,10 @@ export class RestApiCoreServiceV2 {
|
|||||||
return await this.restApiCreateManyHandler.handle(request);
|
return await this.restApiCreateManyHandler.handle(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findDuplicates(request: Request) {
|
||||||
|
return await this.restApiFindDuplicatesHandler.handle(request);
|
||||||
|
}
|
||||||
|
|
||||||
async update(request: Request) {
|
async update(request: Request) {
|
||||||
return await this.restApiUpdateOneHandler.handle(request);
|
return await this.restApiUpdateOneHandler.handle(request);
|
||||||
}
|
}
|
||||||
@ -42,9 +48,9 @@ export class RestApiCoreServiceV2 {
|
|||||||
const { id: recordId } = parseCorePath(request);
|
const { id: recordId } = parseCorePath(request);
|
||||||
|
|
||||||
if (isDefined(recordId)) {
|
if (isDefined(recordId)) {
|
||||||
return await this.restApiGetOneHandler.handle(request);
|
return await this.restApiFindOneHandler.handle(request);
|
||||||
} else {
|
} else {
|
||||||
return await this.restApiGetManyHandler.handle(request);
|
return await this.restApiFindManyHandler.handle(request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
import { mockPersonObjectMetadata } from 'src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata';
|
||||||
|
import { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils';
|
||||||
|
import { mockPersonRecords } from 'src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonRecords';
|
||||||
|
|
||||||
|
describe('buildDuplicateConditions', () => {
|
||||||
|
it('should build conditions based on duplicate criteria from composite field', () => {
|
||||||
|
const duplicateConditons = 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 = 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 = buildDuplicateConditions(
|
||||||
|
mockPersonObjectMetadata([['linkedinLinkPrimaryLinkUrl']]),
|
||||||
|
mockPersonRecords,
|
||||||
|
'recordId',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(duplicateConditons).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should build conditions based on duplicate criteria and without recordId filter', () => {
|
||||||
|
const duplicateConditons = buildDuplicateConditions(
|
||||||
|
mockPersonObjectMetadata([['jobTitle']]),
|
||||||
|
mockPersonRecords,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(duplicateConditons).toEqual({
|
||||||
|
or: [
|
||||||
|
{
|
||||||
|
jobTitle: {
|
||||||
|
eq: 'Test job',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -3,7 +3,7 @@ import { FieldMetadataType } from 'twenty-shared/types';
|
|||||||
import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||||
|
|
||||||
import { GraphqlQueryRunnerException } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
import { GraphqlQueryRunnerException } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||||
import { computeCursorArgFilter } from 'src/engine/api/graphql/graphql-query-runner/utils/compute-cursor-arg-filter';
|
import { computeCursorArgFilter } from 'src/engine/api/utils/compute-cursor-arg-filter.utils';
|
||||||
|
|
||||||
describe('computeCursorArgFilter', () => {
|
describe('computeCursorArgFilter', () => {
|
||||||
const mockFieldMetadataMap = {
|
const mockFieldMetadataMap = {
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
import isEmpty from 'lodash.isempty';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ObjectRecord,
|
||||||
|
ObjectRecordFilter,
|
||||||
|
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||||
|
|
||||||
|
import { settings } from 'src/engine/constants/settings';
|
||||||
|
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||||
|
import { getCompositeFieldMetadataMap } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||||
|
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
|
||||||
|
|
||||||
|
export const buildDuplicateConditions = (
|
||||||
|
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
|
||||||
|
records?: Partial<ObjectRecord>[] | undefined,
|
||||||
|
filteringByExistingRecordId?: string,
|
||||||
|
): Partial<ObjectRecordFilter> => {
|
||||||
|
if (!records || records.length === 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const criteriaCollection =
|
||||||
|
objectMetadataItemWithFieldMaps.duplicateCriteria || [];
|
||||||
|
|
||||||
|
const formattedRecords = formatData(records, objectMetadataItemWithFieldMaps);
|
||||||
|
|
||||||
|
const compositeFieldMetadataMap = getCompositeFieldMetadataMap(
|
||||||
|
objectMetadataItemWithFieldMaps,
|
||||||
|
);
|
||||||
|
|
||||||
|
const conditions = formattedRecords.flatMap((record) => {
|
||||||
|
const criteriaWithMatchingArgs = criteriaCollection.filter((criteria) =>
|
||||||
|
criteria.every((columnName) => {
|
||||||
|
const value = record[columnName] as string | undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
value && value.length >= settings.minLengthOfStringForDuplicateCheck
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return criteriaWithMatchingArgs.map((criteria) => {
|
||||||
|
const condition = {};
|
||||||
|
|
||||||
|
criteria.forEach((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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const filter: Partial<ObjectRecordFilter> = {};
|
||||||
|
|
||||||
|
if (conditions && !isEmpty(conditions)) {
|
||||||
|
filter.or = conditions;
|
||||||
|
|
||||||
|
if (filteringByExistingRecordId) {
|
||||||
|
filter.id = { neq: filteringByExistingRecordId };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
};
|
||||||
@ -302,7 +302,7 @@ export const getFindDuplicatesResponse200 = (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
companyDuplicates: {
|
[`${item.nameSingular}Duplicates`]: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
items: {
|
items: {
|
||||||
$ref: schemaRef,
|
$ref: schemaRef,
|
||||||
|
|||||||
@ -0,0 +1,280 @@
|
|||||||
|
import {
|
||||||
|
TEST_PERSON_1_ID,
|
||||||
|
TEST_PERSON_2_ID,
|
||||||
|
TEST_PERSON_3_ID,
|
||||||
|
} from 'test/integration/constants/test-person-ids.constants';
|
||||||
|
import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util';
|
||||||
|
import { deleteAllRecords } from 'test/integration/utils/delete-all-records';
|
||||||
|
import { TEST_PRIMARY_LINK_URL } from 'test/integration/constants/test-primary-link-url.constant';
|
||||||
|
import { TEST_COMPANY_1_ID } from 'test/integration/constants/test-company-ids.constants';
|
||||||
|
|
||||||
|
describe('Core REST API Find Duplicates endpoint', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await deleteAllRecords('person');
|
||||||
|
await deleteAllRecords('company');
|
||||||
|
|
||||||
|
await makeRestAPIRequest({
|
||||||
|
method: 'post',
|
||||||
|
path: '/companies',
|
||||||
|
body: {
|
||||||
|
id: TEST_COMPANY_1_ID,
|
||||||
|
domainName: {
|
||||||
|
primaryLinkUrl: TEST_PRIMARY_LINK_URL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).expect(201);
|
||||||
|
|
||||||
|
await makeRestAPIRequest({
|
||||||
|
method: 'post',
|
||||||
|
path: '/batch/people',
|
||||||
|
body: [
|
||||||
|
{
|
||||||
|
id: TEST_PERSON_1_ID,
|
||||||
|
companyId: TEST_COMPANY_1_ID,
|
||||||
|
name: {
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: TEST_PERSON_2_ID,
|
||||||
|
companyId: TEST_COMPANY_1_ID,
|
||||||
|
name: {
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: TEST_PERSON_3_ID,
|
||||||
|
companyId: TEST_COMPANY_1_ID,
|
||||||
|
name: {
|
||||||
|
firstName: 'Phil',
|
||||||
|
lastName: 'Collins',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).expect(201);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retrieve duplicates by object data', async () => {
|
||||||
|
const response = await makeRestAPIRequest({
|
||||||
|
method: 'post',
|
||||||
|
path: `/people/duplicates`,
|
||||||
|
body: {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}).expect(200);
|
||||||
|
|
||||||
|
const data = response.body.data;
|
||||||
|
|
||||||
|
expect(data.length).toBe(1);
|
||||||
|
const duplicatesInfo = data[0];
|
||||||
|
|
||||||
|
expect(duplicatesInfo.totalCount).toBe(2);
|
||||||
|
expect(duplicatesInfo.personDuplicates.length).toBe(2);
|
||||||
|
|
||||||
|
const [personDuplicated1, personDuplicated2] =
|
||||||
|
duplicatesInfo.personDuplicates;
|
||||||
|
|
||||||
|
expect(personDuplicated1.id).toBe(TEST_PERSON_1_ID);
|
||||||
|
expect(personDuplicated2.id).toBe(TEST_PERSON_2_ID);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retrieve duplicates by ids', async () => {
|
||||||
|
const response = await makeRestAPIRequest({
|
||||||
|
method: 'post',
|
||||||
|
path: `/people/duplicates`,
|
||||||
|
body: {
|
||||||
|
ids: [TEST_PERSON_1_ID],
|
||||||
|
},
|
||||||
|
}).expect(200);
|
||||||
|
|
||||||
|
const data = response.body.data;
|
||||||
|
|
||||||
|
expect(data.length).toBe(1);
|
||||||
|
const duplicatesInfo = data[0];
|
||||||
|
|
||||||
|
expect(duplicatesInfo.totalCount).toBe(1);
|
||||||
|
expect(duplicatesInfo.personDuplicates.length).toBe(1);
|
||||||
|
|
||||||
|
const [personDuplicated] = duplicatesInfo.personDuplicates;
|
||||||
|
|
||||||
|
expect(personDuplicated.id).toBe(TEST_PERSON_2_ID);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not provide wrong duplicates', async () => {
|
||||||
|
const response = await makeRestAPIRequest({
|
||||||
|
method: 'post',
|
||||||
|
path: `/people/duplicates`,
|
||||||
|
body: {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
firstName: 'Not',
|
||||||
|
lastName: 'Existing',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}).expect(200);
|
||||||
|
|
||||||
|
const data = response.body.data;
|
||||||
|
|
||||||
|
expect(data.length).toBe(1);
|
||||||
|
const duplicatesInfo = data[0];
|
||||||
|
|
||||||
|
expect(duplicatesInfo.totalCount).toBe(0);
|
||||||
|
expect(duplicatesInfo.personDuplicates.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 400 error when empty object data provided', async () => {
|
||||||
|
const response = await makeRestAPIRequest({
|
||||||
|
method: 'post',
|
||||||
|
path: `/people/duplicates`,
|
||||||
|
body: {
|
||||||
|
data: [],
|
||||||
|
},
|
||||||
|
}).expect(400);
|
||||||
|
|
||||||
|
expect(response.body.messages[0]).toContain(
|
||||||
|
'The "data" condition can not be empty when "ids" input not provided',
|
||||||
|
);
|
||||||
|
expect(response.body.error).toBe('BadRequestException');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty result when empty ids provided', async () => {
|
||||||
|
const response = await makeRestAPIRequest({
|
||||||
|
method: 'post',
|
||||||
|
path: `/people/duplicates`,
|
||||||
|
body: {
|
||||||
|
ids: [],
|
||||||
|
},
|
||||||
|
}).expect(200);
|
||||||
|
|
||||||
|
expect(response.body.data.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 400 error when ids and data are provided', async () => {
|
||||||
|
const response = await makeRestAPIRequest({
|
||||||
|
method: 'post',
|
||||||
|
path: `/people/duplicates`,
|
||||||
|
body: {
|
||||||
|
data: [],
|
||||||
|
ids: [],
|
||||||
|
},
|
||||||
|
}).expect(400);
|
||||||
|
|
||||||
|
expect(response.body.messages[0]).toContain(
|
||||||
|
'You cannot provide both "data" and "ids" arguments',
|
||||||
|
);
|
||||||
|
expect(response.body.error).toBe('BadRequestException');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support depth 0 parameter', async () => {
|
||||||
|
const response = await makeRestAPIRequest({
|
||||||
|
method: 'post',
|
||||||
|
path: `/people/duplicates?depth=0`,
|
||||||
|
body: {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}).expect(200);
|
||||||
|
|
||||||
|
const data = response.body.data;
|
||||||
|
|
||||||
|
expect(data.length).toBe(1);
|
||||||
|
const duplicatesInfo = data[0];
|
||||||
|
|
||||||
|
const [personDuplicated1, personDuplicated2] =
|
||||||
|
duplicatesInfo.personDuplicates;
|
||||||
|
|
||||||
|
expect(personDuplicated1.companyId).toBe(TEST_COMPANY_1_ID);
|
||||||
|
expect(personDuplicated1.company).not.toBeDefined();
|
||||||
|
expect(personDuplicated2.companyId).toBe(TEST_COMPANY_1_ID);
|
||||||
|
expect(personDuplicated2.company).not.toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support depth 1 parameter', async () => {
|
||||||
|
const response = await makeRestAPIRequest({
|
||||||
|
method: 'post',
|
||||||
|
path: `/people/duplicates?depth=1`,
|
||||||
|
body: {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}).expect(200);
|
||||||
|
|
||||||
|
const data = response.body.data;
|
||||||
|
|
||||||
|
expect(data.length).toBe(1);
|
||||||
|
const duplicatesInfo = data[0];
|
||||||
|
|
||||||
|
const [personDuplicated1, personDuplicated2] =
|
||||||
|
duplicatesInfo.personDuplicates;
|
||||||
|
|
||||||
|
expect(personDuplicated1.company).toBeDefined();
|
||||||
|
expect(personDuplicated1.company.id).toBe(TEST_COMPANY_1_ID);
|
||||||
|
expect(personDuplicated1.company.people).not.toBeDefined();
|
||||||
|
|
||||||
|
expect(personDuplicated2.company).toBeDefined();
|
||||||
|
expect(personDuplicated2.company.id).toBe(TEST_COMPANY_1_ID);
|
||||||
|
expect(personDuplicated2.company.people).not.toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support depth 2 parameter', async () => {
|
||||||
|
const response = await makeRestAPIRequest({
|
||||||
|
method: 'post',
|
||||||
|
path: `/people/duplicates?depth=2`,
|
||||||
|
body: {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}).expect(200);
|
||||||
|
|
||||||
|
const data = response.body.data;
|
||||||
|
|
||||||
|
expect(data.length).toBe(1);
|
||||||
|
const duplicatesInfo = data[0];
|
||||||
|
|
||||||
|
const [personDuplicated1, personDuplicated2] =
|
||||||
|
duplicatesInfo.personDuplicates;
|
||||||
|
|
||||||
|
expect(personDuplicated1.company.people).toBeDefined();
|
||||||
|
expect(personDuplicated2.company.people).toBeDefined();
|
||||||
|
|
||||||
|
const depth2Person1 = personDuplicated1.company.people.find(
|
||||||
|
(p) => p.id === personDuplicated1.id,
|
||||||
|
);
|
||||||
|
const depth2Person2 = personDuplicated2.company.people.find(
|
||||||
|
(p) => p.id === personDuplicated2.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(depth2Person1).toBeDefined();
|
||||||
|
expect(depth2Person2).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user