Common - Update one/many, restore one/many, findDuplicates & mergeMany (#15279)

closes https://github.com/twentyhq/core-team-issues/issues/1739
This commit is contained in:
Etienne
2025-10-24 14:41:17 +02:00
committed by GitHub
parent 8f60f7e277
commit 10f4f1592f
60 changed files with 2346 additions and 74 deletions
@@ -106,6 +106,32 @@ export class RestApiCoreController {
res.status(200).send(result);
}
@Patch('restore/*')
async handleApiRestore(
@Req() request: AuthenticatedRequest,
@Res() res: Response,
) {
this.logger.log(
`[REST API] Processing RESTORE request to ${request.path} on workspace ${request.workspaceId}`,
);
const result = await this.restApiCoreService.restore(request);
res.status(200).send(result);
}
@Patch('*/merge')
async handleApiMerge(
@Req() request: AuthenticatedRequest,
@Res() res: Response,
) {
this.logger.log(
`[REST API] Processing MERGE request to ${request.path} on workspace ${request.workspaceId}`,
);
const result = await this.restApiCoreService.mergeMany(request);
res.status(200).send(result);
}
@Patch('*')
async handleApiPatch(
@Req() request: AuthenticatedRequest,
@@ -11,7 +11,6 @@ import { capitalize, isDefined } from 'twenty-shared/utils';
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
import { CommonCreateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service';
import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type';
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
import { parseUpsertRestRequest } from 'src/engine/api/rest/input-request-parsers/upsert-parser-utils/parse-upsert-rest-request.util';
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
@@ -49,7 +48,6 @@ export class RestApiCreateManyHandler extends RestApiBaseHandler {
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
},
CommonQueryNames.CREATE_MANY,
);
return this.formatRestResponse(
@@ -11,7 +11,6 @@ import { capitalize, isDefined } from 'twenty-shared/utils';
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
import { CommonCreateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-one-query-runner.service';
import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type';
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
import { parseUpsertRestRequest } from 'src/engine/api/rest/input-request-parsers/upsert-parser-utils/parse-upsert-rest-request.util';
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
@@ -50,7 +49,6 @@ export class RestApiCreateOneHandler extends RestApiBaseHandler {
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
},
CommonQueryNames.CREATE_ONE,
);
return this.formatRestResponse(
@@ -6,7 +6,6 @@ import { capitalize } from 'twenty-shared/utils';
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
import { CommonDeleteManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-delete-many-query-runner.service';
import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type';
import { parseFilterRestRequest } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util';
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util';
@@ -36,7 +35,6 @@ export class RestApiDeleteManyHandler extends RestApiBaseHandler {
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
},
CommonQueryNames.DELETE_MANY,
);
return this.formatRestResponse(
@@ -6,9 +6,7 @@ import { capitalize, isDefined } from 'twenty-shared/utils';
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
import { CommonDeleteOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-delete-one-query-runner.service';
import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type';
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util';
@@ -37,7 +35,6 @@ export class RestApiDeleteOneHandler extends RestApiBaseHandler {
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
},
CommonQueryNames.DELETE_ONE,
);
return this.formatRestResponse(
@@ -64,7 +61,6 @@ export class RestApiDeleteOneHandler extends RestApiBaseHandler {
return {
id,
depth: parseDepthRestRequest(request),
};
}
}
@@ -6,7 +6,6 @@ import { capitalize } from 'twenty-shared/utils';
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
import { CommonDestroyManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service';
import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type';
import { parseFilterRestRequest } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util';
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util';
@@ -36,7 +35,6 @@ export class RestApiDestroyManyHandler extends RestApiBaseHandler {
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
},
CommonQueryNames.DESTROY_MANY,
);
return this.formatRestResponse(
@@ -6,7 +6,6 @@ import { capitalize, isDefined } from 'twenty-shared/utils';
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
import { CommonDestroyOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-destroy-one-query-runner.service';
import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type';
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util';
@@ -37,7 +36,6 @@ export class RestApiDestroyOneHandler extends RestApiBaseHandler {
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
},
CommonQueryNames.DESTROY_ONE,
);
return this.formatRestResponse(
@@ -10,11 +10,93 @@ import {
RestApiBaseHandler,
} from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
import { CommonFindDuplicatesQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service';
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util';
import { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils';
@Injectable()
export class RestApiFindDuplicatesHandler extends RestApiBaseHandler {
constructor(
private readonly commonFindDuplicatesQueryRunnerService: CommonFindDuplicatesQueryRunnerService,
) {
super();
}
async commonHandle(request: AuthenticatedRequest) {
try {
this.validate(request);
const { data, ids, depth } = this.parseRequestArgs(request);
const {
authContext,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
} = await this.buildCommonOptions(request);
const selectedFields = await this.computeSelectedFields({
depth,
objectMetadataMapItem: objectMetadataItemWithFieldMaps,
objectMetadataMaps,
authContext,
});
const duplicateConnections =
await this.commonFindDuplicatesQueryRunnerService.execute(
{ data, ids, selectedFields },
{
authContext,
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
},
);
return this.formatRestResponse(
duplicateConnections,
objectMetadataItemWithFieldMaps.nameSingular,
);
} catch (error) {
workspaceQueryRunnerRestApiExceptionHandler(error);
}
}
private formatRestResponse(
duplicateConnections: Array<{
records: ObjectRecord[];
totalCount: number;
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
endCursor: string | null;
}>,
objectNameSingular: string,
) {
return {
data: duplicateConnections.map((connection) => ({
data: {
[`${objectNameSingular}Duplicates`]: connection.records,
},
totalCount: connection.totalCount,
pageInfo: {
hasNextPage: connection.hasNextPage,
hasPreviousPage: connection.hasPreviousPage,
startCursor: connection.startCursor,
endCursor: connection.endCursor,
},
})),
};
}
private parseRequestArgs(request: AuthenticatedRequest) {
return {
data: request.body.data,
ids: request.body.ids,
depth: parseDepthRestRequest(request),
};
}
async handle(request: AuthenticatedRequest) {
this.validate(request);
@@ -8,7 +8,6 @@ import {
} from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
import { CommonFindManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-many-query-runner.service';
import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type';
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
import { parseEndingBeforeRestRequest } from 'src/engine/api/rest/input-request-parsers/ending-before-parser-utils/parse-ending-before-rest-request.util';
import { parseFilterRestRequest } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util';
@@ -118,7 +117,6 @@ export class RestApiFindManyHandler extends RestApiBaseHandler {
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
},
CommonQueryNames.FIND_MANY,
);
return this.formatRestResponse(
@@ -6,7 +6,6 @@ import { isDefined } from 'twenty-shared/utils';
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
import { CommonFindOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-one-query-runner.service';
import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type';
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
@@ -81,7 +80,6 @@ export class RestApiFindOneHandler extends RestApiBaseHandler {
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
},
CommonQueryNames.FIND_ONE,
);
return this.formatRestResponse(
@@ -3,7 +3,6 @@ import { Injectable } from '@nestjs/common';
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
import { CommonGroupByQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-group-by-query-runner.service';
import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type';
import { parseAggregateFieldsRestRequest } from 'src/engine/api/rest/input-request-parsers/aggregate-fields-parser-utils/parse-aggregate-fields-rest-request.util';
import { parseFilterRestRequest } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util';
import { parseGroupByRestRequest } from 'src/engine/api/rest/input-request-parsers/group-by-parser-utils/parse-group-by-rest-request.util';
@@ -44,10 +43,9 @@ export class RestApiGroupByHandler extends RestApiBaseHandler {
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
},
CommonQueryNames.GROUP_BY,
);
} catch (error) {
throw workspaceQueryRunnerRestApiExceptionHandler(error);
return workspaceQueryRunnerRestApiExceptionHandler(error);
}
}
@@ -0,0 +1,69 @@
import { Injectable } from '@nestjs/common';
import { ObjectRecord } from 'twenty-shared/types';
import { capitalize } from 'twenty-shared/utils';
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
import { CommonMergeManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-merge-many-query-runner.service';
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util';
@Injectable()
export class RestApiMergeManyHandler extends RestApiBaseHandler {
constructor(
private readonly commonMergeManyQueryRunnerService: CommonMergeManyQueryRunnerService,
) {
super();
}
async handle(request: AuthenticatedRequest) {
try {
const { depth, ...restArgs } = await this.parseRequestArgs(request);
const {
authContext,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
} = await this.buildCommonOptions(request);
const selectedFields = await this.computeSelectedFields({
depth,
objectMetadataMapItem: objectMetadataItemWithFieldMaps,
objectMetadataMaps,
authContext,
});
const record = await this.commonMergeManyQueryRunnerService.execute(
{ ...restArgs, selectedFields },
{
authContext,
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
},
);
return this.formatRestResponse(
record,
objectMetadataItemWithFieldMaps.nameSingular,
);
} catch (error) {
return workspaceQueryRunnerRestApiExceptionHandler(error);
}
}
private formatRestResponse(record: ObjectRecord, objectNamePlural: string) {
return { data: { [`merge${capitalize(objectNamePlural)}`]: record } };
}
private async parseRequestArgs(request: AuthenticatedRequest) {
const depth = parseDepthRestRequest(request);
return {
conflictPriorityIndex: request.body.conflictPriorityIndex,
dryRun: request.body.dryRun,
ids: request.body.ids,
depth,
};
}
}
@@ -0,0 +1,80 @@
import { Injectable } from '@nestjs/common';
import { ObjectRecord } from 'twenty-shared/types';
import { capitalize } from 'twenty-shared/utils';
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
import { CommonRestoreManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-restore-many-query-runner.service';
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
import { parseFilterRestRequest } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util';
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util';
@Injectable()
export class RestApiRestoreManyHandler extends RestApiBaseHandler {
constructor(
private readonly commonRestoreManyQueryRunnerService: CommonRestoreManyQueryRunnerService,
) {
super();
}
async handle(request: AuthenticatedRequest): Promise<{
data: {
[x: string]: ObjectRecord[];
};
}> {
try {
const { filter, depth } = this.parseRequestArgs(request);
const {
authContext,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
} = await this.buildCommonOptions(request);
const selectedFields = await this.computeSelectedFields({
depth,
objectMetadataMapItem: objectMetadataItemWithFieldMaps,
objectMetadataMaps,
authContext,
});
const records = await this.commonRestoreManyQueryRunnerService.execute(
{ filter, selectedFields },
{
authContext,
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
},
);
return this.formatRestResponse(
records,
objectMetadataItemWithFieldMaps.namePlural,
);
} catch (error) {
return workspaceQueryRunnerRestApiExceptionHandler(error);
}
}
private formatRestResponse(
records: ObjectRecord[],
objectNamePlural: string,
) {
return {
data: {
[`restore${capitalize(objectNamePlural)}`]: records,
},
};
}
private parseRequestArgs(request: AuthenticatedRequest) {
const filter = parseFilterRestRequest(request);
return {
filter,
depth: parseDepthRestRequest(request),
};
}
}
@@ -0,0 +1,75 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { ObjectRecord } from 'twenty-shared/types';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
import { CommonRestoreOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-restore-one-query-runner.service';
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util';
@Injectable()
export class RestApiRestoreOneHandler extends RestApiBaseHandler {
constructor(
private readonly commonRestoreOneQueryRunnerService: CommonRestoreOneQueryRunnerService,
) {
super();
}
async handle(request: AuthenticatedRequest) {
try {
const { id, depth } = this.parseRequestArgs(request);
const {
authContext,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
} = await this.buildCommonOptions(request);
const selectedFields = await this.computeSelectedFields({
depth,
objectMetadataMapItem: objectMetadataItemWithFieldMaps,
objectMetadataMaps,
authContext,
});
const record = await this.commonRestoreOneQueryRunnerService.execute(
{ id, selectedFields },
{
authContext,
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
},
);
return this.formatRestResponse(
record,
objectMetadataItemWithFieldMaps.nameSingular,
);
} catch (error) {
return workspaceQueryRunnerRestApiExceptionHandler(error);
}
}
private formatRestResponse(record: ObjectRecord, objectNameSingular: string) {
return {
data: { [`restore${capitalize(objectNameSingular)}`]: record },
};
}
private parseRequestArgs(request: AuthenticatedRequest) {
const { id } = parseCorePath(request);
if (!isDefined(id)) {
throw new BadRequestException('Record ID not found');
}
return {
id,
depth: parseDepthRestRequest(request),
};
}
}
@@ -0,0 +1,75 @@
import { Injectable } from '@nestjs/common';
import { ObjectRecord } from 'twenty-shared/types';
import { capitalize } from 'twenty-shared/utils';
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
import { CommonUpdateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-update-many-query-runner.service';
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
import { parseFilterRestRequest } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util';
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util';
@Injectable()
export class RestApiUpdateManyHandler extends RestApiBaseHandler {
constructor(
private readonly commonUpdateManyQueryRunnerService: CommonUpdateManyQueryRunnerService,
) {
super();
}
async handle(request: AuthenticatedRequest) {
try {
const { data, depth, filter } = this.parseRequestArgs(request);
const {
authContext,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
} = await this.buildCommonOptions(request);
const selectedFields = await this.computeSelectedFields({
depth,
objectMetadataMapItem: objectMetadataItemWithFieldMaps,
objectMetadataMaps,
authContext,
});
const records = await this.commonUpdateManyQueryRunnerService.execute(
{ data, filter, selectedFields },
{
authContext,
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
},
);
return this.formatRestResponse(
records,
objectMetadataItemWithFieldMaps.namePlural,
);
} catch (error) {
return workspaceQueryRunnerRestApiExceptionHandler(error);
}
}
private formatRestResponse(
records: ObjectRecord[],
objectNamePlural: string,
) {
return {
data: {
[`update${capitalize(objectNamePlural)}`]: records,
},
};
}
private parseRequestArgs(request: AuthenticatedRequest) {
return {
data: request.body,
depth: parseDepthRestRequest(request),
filter: parseFilterRestRequest(request),
};
}
}
@@ -5,17 +5,79 @@ import {
} from '@nestjs/common';
import isEmpty from 'lodash.isempty';
import { isDefined } from 'twenty-shared/utils';
import { ObjectRecord } from 'twenty-shared/types';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
import { CommonUpdateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-update-one-query-runner.service';
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util';
import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils';
@Injectable()
export class RestApiUpdateOneHandler extends RestApiBaseHandler {
constructor(
private readonly commonUpdateOneQueryRunnerService: CommonUpdateOneQueryRunnerService,
) {
super();
}
async commonHandle(request: AuthenticatedRequest) {
try {
const { id, data, depth } = this.parseRequestArgs(request);
const {
authContext,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
} = await this.buildCommonOptions(request);
const selectedFields = await this.computeSelectedFields({
depth,
objectMetadataMapItem: objectMetadataItemWithFieldMaps,
objectMetadataMaps,
authContext,
});
const record = await this.commonUpdateOneQueryRunnerService.execute(
{ id, data, selectedFields },
{
authContext,
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
},
);
return this.formatRestResponse(
record,
objectMetadataItemWithFieldMaps.nameSingular,
);
} catch (error) {
workspaceQueryRunnerRestApiExceptionHandler(error);
}
}
private formatRestResponse(record: ObjectRecord, objectNameSingular: string) {
return { data: { [`update${capitalize(objectNameSingular)}`]: record } };
}
private parseRequestArgs(request: AuthenticatedRequest) {
const { id } = parseCorePath(request);
if (!id) {
throw new BadRequestException('Record ID not found');
}
return {
id,
data: request.body,
depth: parseDepthRestRequest(request),
};
}
async handle(request: AuthenticatedRequest) {
const { id: recordId } = parseCorePath(request);
@@ -1,7 +1,7 @@
import { BadRequestException } from '@nestjs/common';
import { type Request } from 'express';
import { isValidUuid } from 'twenty-shared/utils';
import { isDefined, isValidUuid } from 'twenty-shared/utils';
export const parseCorePath = (
request: Request,
@@ -12,7 +12,10 @@ export const parseCorePath = (
.split('/')
.filter(Boolean);
if (queryAction.length > 2) {
if (
queryAction.length > 2 ||
(queryAction.length > 3 && queryAction[0] === 'restore')
) {
throw new BadRequestException(
`Query path '${request.path}' invalid. Valid examples: /rest/companies/id or /rest/companies or /rest/batch/companies`,
);
@@ -32,10 +35,27 @@ export const parseCorePath = (
return { object: queryAction[1] };
}
if (queryAction[1] === 'duplicates' || queryAction[1] === 'group') {
if (
queryAction[1] === 'duplicates' ||
queryAction[1] === 'group' ||
queryAction[1] === 'merge'
) {
return { object: queryAction[0] };
}
if (queryAction[0] === 'restore') {
const recordId = queryAction[2];
if (isDefined(recordId) && !isValidUuid(recordId)) {
throw new BadRequestException(`'${recordId}' is not a valid UUID`);
}
return {
object: queryAction[1],
id: recordId,
};
}
const recordId = queryAction[1];
if (!isValidUuid(recordId)) {
@@ -13,6 +13,10 @@ import { RestApiFindDuplicatesHandler } from 'src/engine/api/rest/core/handlers/
import { RestApiFindManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-many.handler';
import { RestApiFindOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-one.handler';
import { RestApiGroupByHandler } from 'src/engine/api/rest/core/handlers/rest-api-group-by.handler';
import { RestApiMergeManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-merge-many.handler';
import { RestApiRestoreManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-restore-many.handler';
import { RestApiRestoreOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-restore-one.handler';
import { RestApiUpdateManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-many.handler';
import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler';
import { CoreQueryBuilderModule } from 'src/engine/api/rest/core/query-builder/core-query-builder.module';
import { coreQueryBuilderFactories } from 'src/engine/api/rest/core/query-builder/factories/factories';
@@ -33,6 +37,8 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
const restApiCoreResolvers = [
RestApiCreateOneHandler,
RestApiCreateManyHandler,
RestApiUpdateOneHandler,
RestApiUpdateManyHandler,
RestApiFindOneHandler,
RestApiFindManyHandler,
RestApiFindDuplicatesHandler,
@@ -42,6 +48,9 @@ const restApiCoreResolvers = [
RestApiDestroyManyHandler,
RestApiDeleteOneHandler,
RestApiDeleteManyHandler,
RestApiRestoreOneHandler,
RestApiRestoreManyHandler,
RestApiMergeManyHandler,
];
@Module({
@@ -12,6 +12,10 @@ import { RestApiFindDuplicatesHandler } from 'src/engine/api/rest/core/handlers/
import { RestApiFindManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-many.handler';
import { RestApiFindOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-one.handler';
import { RestApiGroupByHandler } from 'src/engine/api/rest/core/handlers/rest-api-group-by.handler';
import { RestApiMergeManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-merge-many.handler';
import { RestApiRestoreManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-restore-many.handler';
import { RestApiRestoreOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-restore-one.handler';
import { RestApiUpdateManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-many.handler';
import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler';
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
import { parseSoftDeleteRestRequest } from 'src/engine/api/rest/input-request-parsers/soft-delete-parser-utils/parse-soft-delete-rest-request.util';
@@ -24,15 +28,19 @@ export class RestApiCoreService {
constructor(
private readonly restApiCreateOneHandler: RestApiCreateOneHandler,
private readonly restApiCreateManyHandler: RestApiCreateManyHandler,
private readonly restApiUpdateOneHandler: RestApiUpdateOneHandler,
private readonly restApiUpdateManyHandler: RestApiUpdateManyHandler,
private readonly restApiFindOneHandler: RestApiFindOneHandler,
private readonly restApiFindManyHandler: RestApiFindManyHandler,
private readonly restApiFindDuplicatesHandler: RestApiFindDuplicatesHandler,
private readonly restApiGroupByHandler: RestApiGroupByHandler,
private readonly restApiUpdateOneHandler: RestApiUpdateOneHandler,
private readonly restApiDestroyOneHandler: RestApiDestroyOneHandler,
private readonly restApiDestroyManyHandler: RestApiDestroyManyHandler,
private readonly restApiDeleteOneHandler: RestApiDeleteOneHandler,
private readonly restApiDeleteManyHandler: RestApiDeleteManyHandler,
private readonly restApiRestoreOneHandler: RestApiRestoreOneHandler,
private readonly restApiRestoreManyHandler: RestApiRestoreManyHandler,
private readonly restApiMergeManyHandler: RestApiMergeManyHandler,
private readonly featureFlagService: FeatureFlagService,
) {}
@@ -63,6 +71,37 @@ export class RestApiCoreService {
}
}
async findDuplicates(request: AuthenticatedRequest) {
const isCommonApiEnabled = await this.isCommonApiEnabled(request);
if (isCommonApiEnabled) {
return await this.restApiFindDuplicatesHandler.commonHandle(request);
} else {
return await this.restApiFindDuplicatesHandler.handle(request);
}
}
async update(request: AuthenticatedRequest) {
const { id: recordId } = parseCorePath(request);
const isCommonApiEnabled = await this.isCommonApiEnabled(request);
if (isCommonApiEnabled) {
if (isDefined(recordId)) {
return await this.restApiUpdateOneHandler.commonHandle(request);
} else {
return await this.restApiUpdateManyHandler.handle(request);
}
} else {
if (isDefined(recordId)) {
return await this.restApiUpdateOneHandler.handle(request);
} else {
throw new BadRequestException(
'Activate feature flag to use UpdateMany in the REST API',
);
}
}
}
async get(request: AuthenticatedRequest) {
const { id: recordId } = parseCorePath(request);
const isCommonApiEnabled = await this.isCommonApiEnabled(request);
@@ -94,14 +133,6 @@ export class RestApiCoreService {
}
}
async findDuplicates(request: AuthenticatedRequest) {
return await this.restApiFindDuplicatesHandler.handle(request);
}
async update(request: AuthenticatedRequest) {
return await this.restApiUpdateOneHandler.handle(request);
}
async delete(request: AuthenticatedRequest) {
const { id: recordId } = parseCorePath(request);
@@ -125,4 +156,34 @@ export class RestApiCoreService {
'Activate feature flag IS_COMMON_API_ENABLED to use Delete in the REST API',
);
}
async restore(request: AuthenticatedRequest) {
const { id: recordId } = parseCorePath(request);
const isCommonApiEnabled = await this.isCommonApiEnabled(request);
if (isCommonApiEnabled) {
if (isDefined(recordId)) {
return await this.restApiRestoreOneHandler.handle(request);
} else {
return await this.restApiRestoreManyHandler.handle(request);
}
} else {
throw new BadRequestException(
'Activate feature flag to use Restore in the REST API',
);
}
}
async mergeMany(request: AuthenticatedRequest) {
const isCommonApiEnabled = await this.isCommonApiEnabled(request);
if (isCommonApiEnabled) {
return await this.restApiMergeManyHandler.handle(request);
} else {
throw new BadRequestException(
'Activate feature flag to use Merge in the REST API',
);
}
}
}