960 api rest batch create not working (#12028)
- fix batch endpoint - migrate batch endpoint to the rest api v2 - add new integration test for batch endpoints
This commit is contained in:
@ -1,21 +0,0 @@
|
||||
import { Controller, Post, Req, Res, UseGuards } from '@nestjs/common';
|
||||
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
import { RestApiCoreService } from 'src/engine/api/rest/core/rest-api-core.service';
|
||||
import { cleanGraphQLResponse } from 'src/engine/api/rest/utils/clean-graphql-response.utils';
|
||||
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
|
||||
@Controller('rest/batch/*')
|
||||
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
|
||||
export class RestApiCoreBatchController {
|
||||
constructor(private readonly restApiCoreService: RestApiCoreService) {}
|
||||
|
||||
@Post()
|
||||
async handleApiPost(@Req() request: Request, @Res() res: Response) {
|
||||
const result = await this.restApiCoreService.createMany(request);
|
||||
|
||||
res.status(201).send(cleanGraphQLResponse(result.data));
|
||||
}
|
||||
}
|
||||
@ -20,7 +20,7 @@ import { cleanGraphQLResponse } from 'src/engine/api/rest/utils/clean-graphql-re
|
||||
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
|
||||
@Controller('rest/*')
|
||||
@Controller('rest')
|
||||
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
|
||||
@UseFilters(RestApiExceptionFilter)
|
||||
export class RestApiCoreController {
|
||||
@ -29,37 +29,42 @@ export class RestApiCoreController {
|
||||
private readonly restApiCoreServiceV2: RestApiCoreServiceV2,
|
||||
) {}
|
||||
|
||||
@Post('batch/*')
|
||||
async handleApiPostBatch(@Req() request: Request, @Res() res: Response) {
|
||||
const result = await this.restApiCoreServiceV2.createMany(request);
|
||||
|
||||
res.status(201).send(result);
|
||||
}
|
||||
|
||||
@Post('duplicates')
|
||||
@UseFilters(RestApiExceptionFilter)
|
||||
async handleApiFindDuplicates(@Req() request: Request, @Res() res: Response) {
|
||||
const result = await this.restApiCoreService.findDuplicates(request);
|
||||
|
||||
res.status(200).send(cleanGraphQLResponse(result.data.data));
|
||||
}
|
||||
|
||||
@Get()
|
||||
async handleApiGet(@Req() request: Request, @Res() res: Response) {
|
||||
const result = await this.restApiCoreServiceV2.get(request);
|
||||
|
||||
res.status(200).send(result);
|
||||
}
|
||||
|
||||
@Delete()
|
||||
async handleApiDelete(@Req() request: Request, @Res() res: Response) {
|
||||
const result = await this.restApiCoreServiceV2.delete(request);
|
||||
|
||||
res.status(200).send(result);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Post('*')
|
||||
async handleApiPost(@Req() request: Request, @Res() res: Response) {
|
||||
const result = await this.restApiCoreServiceV2.createOne(request);
|
||||
|
||||
res.status(201).send(result);
|
||||
}
|
||||
|
||||
@Patch()
|
||||
@UseFilters(RestApiExceptionFilter)
|
||||
@Get('*')
|
||||
async handleApiGet(@Req() request: Request, @Res() res: Response) {
|
||||
const result = await this.restApiCoreServiceV2.get(request);
|
||||
|
||||
res.status(200).send(result);
|
||||
}
|
||||
|
||||
@Delete('*')
|
||||
async handleApiDelete(@Req() request: Request, @Res() res: Response) {
|
||||
const result = await this.restApiCoreServiceV2.delete(request);
|
||||
|
||||
res.status(200).send(result);
|
||||
}
|
||||
|
||||
@Patch('*')
|
||||
async handleApiPatch(@Req() request: Request, @Res() res: Response) {
|
||||
const result = await this.restApiCoreServiceV2.update(request);
|
||||
|
||||
@ -67,9 +72,9 @@ export class RestApiCoreController {
|
||||
}
|
||||
|
||||
// This endpoint is not documented in the OpenAPI schema.
|
||||
// We keep it to avoid a breaking change since it initially used PUT instead of PATCH,
|
||||
// and because the PUT verb is often used as a PATCH.
|
||||
@Put()
|
||||
// We keep it to avoid a breaking change since it initially used PUT instead
|
||||
// of PATCH, and because the PUT verb is often used as a PATCH.
|
||||
@Put('*')
|
||||
async handleApiPut(@Req() request: Request, @Res() res: Response) {
|
||||
const result = await this.restApiCoreServiceV2.update(request);
|
||||
|
||||
|
||||
@ -0,0 +1,74 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
|
||||
import { Request } from 'express';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiCreateManyHandler extends RestApiBaseHandler {
|
||||
async handle(request: Request) {
|
||||
const { objectMetadataNamePlural, objectMetadata, repository } =
|
||||
await this.getRepositoryAndMetadataOrFail(request);
|
||||
|
||||
const body = request.body;
|
||||
|
||||
if (!Array.isArray(body)) {
|
||||
throw new BadRequestException('Body must be an array');
|
||||
}
|
||||
|
||||
if (body.length === 0) {
|
||||
throw new BadRequestException('Input must not be empty');
|
||||
}
|
||||
|
||||
const overriddenRecordsToCreate: Record<string, any>[] = [];
|
||||
|
||||
for (const recordToCreate of body) {
|
||||
const overriddenBody = await this.recordInputTransformerService.process({
|
||||
recordInput: recordToCreate,
|
||||
objectMetadataMapItem: objectMetadata.objectMetadataMapItem,
|
||||
});
|
||||
|
||||
const recordExists =
|
||||
isDefined(overriddenBody.id) &&
|
||||
(await repository.exists({
|
||||
where: {
|
||||
id: overriddenBody.id,
|
||||
},
|
||||
}));
|
||||
|
||||
if (recordExists) {
|
||||
throw new BadRequestException('Record already exists');
|
||||
}
|
||||
|
||||
overriddenRecordsToCreate.push(overriddenBody);
|
||||
}
|
||||
|
||||
const createdRecords = await repository.save(overriddenRecordsToCreate);
|
||||
|
||||
this.apiEventEmitterService.emitCreateEvents(
|
||||
createdRecords,
|
||||
this.getAuthContextFromRequest(request),
|
||||
objectMetadata.objectMetadataMapItem,
|
||||
);
|
||||
|
||||
const records = await this.getRecord({
|
||||
recordIds: createdRecords.map((record) => record.id),
|
||||
repository,
|
||||
objectMetadata,
|
||||
depth: this.depthInputFactory.create(request),
|
||||
});
|
||||
|
||||
if (records.length !== body.length) {
|
||||
throw new Error(
|
||||
`Error when creating records. ${body.length - records.length} records are missing after creation.`,
|
||||
);
|
||||
}
|
||||
|
||||
return this.formatResult({
|
||||
operation: 'create',
|
||||
objectNamePlural: objectMetadataNamePlural,
|
||||
data: records,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -228,12 +228,19 @@ export abstract class RestApiBaseHandler {
|
||||
}: FormatResultParams<T>) {
|
||||
let prefix: string;
|
||||
|
||||
if (isDefined(objectNameSingular) && isDefined(objectNamePlural)) {
|
||||
throw new Error(
|
||||
'Cannot define both objectNameSingular and objectNamePlural',
|
||||
);
|
||||
}
|
||||
|
||||
if (operation === 'findOne') {
|
||||
prefix = objectNameSingular || '';
|
||||
} else if (operation === 'findMany') {
|
||||
prefix = objectNamePlural || '';
|
||||
} else {
|
||||
prefix = operation + capitalize(objectNameSingular || '');
|
||||
prefix =
|
||||
operation + capitalize(objectNameSingular || objectNamePlural || '');
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@ -23,7 +23,24 @@ describe('parseCorePath', () => {
|
||||
const request: any = { path: '/rest/companies/uuid/toto' };
|
||||
|
||||
expect(() => parseCorePath(request)).toThrow(
|
||||
"Query path '/rest/companies/uuid/toto' invalid. Valid examples: /rest/companies/id or /rest/companies",
|
||||
"Query path '/rest/companies/uuid/toto' invalid. Valid examples: /rest/companies/id or /rest/companies or /rest/batch/companies",
|
||||
);
|
||||
});
|
||||
|
||||
it('should parse object from batch request', () => {
|
||||
const request: any = { path: '/rest/batch/companies' };
|
||||
|
||||
expect(parseCorePath(request)).toEqual({
|
||||
object: 'companies',
|
||||
id: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw for wrong batch request', () => {
|
||||
const request: any = { path: '/rest/batch/companies/uuid' };
|
||||
|
||||
expect(() => parseCorePath(request)).toThrow(
|
||||
"Query path '/rest/batch/companies/uuid' invalid. Valid examples: /rest/companies/id or /rest/companies or /rest/batch/companies",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -9,10 +9,14 @@ export const parseCorePath = (
|
||||
|
||||
if (queryAction.length > 2) {
|
||||
throw new BadRequestException(
|
||||
`Query path '${request.path}' invalid. Valid examples: /rest/companies/id or /rest/companies`,
|
||||
`Query path '${request.path}' invalid. Valid examples: /rest/companies/id or /rest/companies or /rest/batch/companies`,
|
||||
);
|
||||
}
|
||||
|
||||
if (queryAction.length === 2 && queryAction[0] === 'batch') {
|
||||
return { object: queryAction[1] };
|
||||
}
|
||||
|
||||
if (queryAction.length === 1) {
|
||||
return { object: queryAction[0] };
|
||||
}
|
||||
|
||||
@ -9,12 +9,14 @@ import { RestApiCreateOneHandler } from 'src/engine/api/rest/core/handlers/rest-
|
||||
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 { RestApiGetManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-get-many.handler';
|
||||
import { RestApiCreateManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-many.handler';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiCoreServiceV2 {
|
||||
constructor(
|
||||
private readonly restApiDeleteOneHandler: RestApiDeleteOneHandler,
|
||||
private readonly restApiCreateOneHandler: RestApiCreateOneHandler,
|
||||
private readonly restApiCreateManyHandler: RestApiCreateManyHandler,
|
||||
private readonly restApiUpdateOneHandler: RestApiUpdateOneHandler,
|
||||
private readonly restApiGetOneHandler: RestApiGetOneHandler,
|
||||
private readonly restApiGetManyHandler: RestApiGetManyHandler,
|
||||
@ -28,6 +30,10 @@ export class RestApiCoreServiceV2 {
|
||||
return await this.restApiCreateOneHandler.handle(request);
|
||||
}
|
||||
|
||||
async createMany(request: Request) {
|
||||
return await this.restApiCreateManyHandler.handle(request);
|
||||
}
|
||||
|
||||
async update(request: Request) {
|
||||
return await this.restApiUpdateOneHandler.handle(request);
|
||||
}
|
||||
|
||||
@ -13,16 +13,17 @@ import { RecordTransformerModule } from 'src/engine/core-modules/record-transfor
|
||||
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 { coreQueryBuilderFactories } from 'src/engine/api/rest/core/query-builder/factories/factories';
|
||||
import { RestApiCoreBatchController } from 'src/engine/api/rest/core/controllers/rest-api-core-batch.controller';
|
||||
import { RestApiCoreServiceV2 } from 'src/engine/api/rest/core/rest-api-core-v2.service';
|
||||
import { RestApiCoreService } from 'src/engine/api/rest/core/rest-api-core.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 { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { RestApiCreateManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-many.handler';
|
||||
|
||||
const restApiCoreResolvers = [
|
||||
RestApiDeleteOneHandler,
|
||||
RestApiCreateOneHandler,
|
||||
RestApiCreateManyHandler,
|
||||
RestApiUpdateOneHandler,
|
||||
RestApiGetOneHandler,
|
||||
RestApiGetManyHandler,
|
||||
@ -38,7 +39,7 @@ const restApiCoreResolvers = [
|
||||
RecordTransformerModule,
|
||||
WorkspacePermissionsCacheModule,
|
||||
],
|
||||
controllers: [RestApiCoreController, RestApiCoreBatchController],
|
||||
controllers: [RestApiCoreController],
|
||||
providers: [
|
||||
RestApiService,
|
||||
RestApiCoreService,
|
||||
|
||||
@ -0,0 +1,169 @@
|
||||
import {
|
||||
TEST_PERSON_1_ID,
|
||||
TEST_PERSON_2_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_COMPANY_1_ID } from 'test/integration/constants/test-company-ids.constants';
|
||||
import { TEST_PRIMARY_LINK_URL } from 'test/integration/constants/test-primary-link-url.constant';
|
||||
|
||||
describe('Core REST API Create Many endpoint', () => {
|
||||
beforeEach(async () => {
|
||||
await deleteAllRecords('person');
|
||||
await makeRestAPIRequest({
|
||||
method: 'post',
|
||||
path: '/companies',
|
||||
body: {
|
||||
id: TEST_COMPANY_1_ID,
|
||||
domainName: {
|
||||
primaryLinkUrl: TEST_PRIMARY_LINK_URL,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should create many person', async () => {
|
||||
const requestBody = [
|
||||
{
|
||||
id: TEST_PERSON_1_ID,
|
||||
},
|
||||
{
|
||||
id: TEST_PERSON_2_ID,
|
||||
},
|
||||
];
|
||||
|
||||
await makeRestAPIRequest({
|
||||
method: 'post',
|
||||
path: `/batch/people`,
|
||||
body: requestBody,
|
||||
})
|
||||
.expect(201)
|
||||
.expect((res) => {
|
||||
const createdPeople = res.body.data.createPeople;
|
||||
|
||||
expect(createdPeople.length).toBe(2);
|
||||
expect(createdPeople[0].id).toBe(TEST_PERSON_1_ID);
|
||||
expect(createdPeople[1].id).toBe(TEST_PERSON_2_ID);
|
||||
});
|
||||
});
|
||||
|
||||
it('should support depth 0 parameter', async () => {
|
||||
const requestBody = [
|
||||
{
|
||||
id: TEST_PERSON_1_ID,
|
||||
companyId: TEST_COMPANY_1_ID,
|
||||
},
|
||||
{
|
||||
id: TEST_PERSON_2_ID,
|
||||
companyId: TEST_COMPANY_1_ID,
|
||||
},
|
||||
];
|
||||
|
||||
await makeRestAPIRequest({
|
||||
method: 'post',
|
||||
path: `/batch/people?depth=0`,
|
||||
body: requestBody,
|
||||
})
|
||||
.expect(201)
|
||||
.expect((res) => {
|
||||
const [createdPerson1, createdPerson2] = res.body.data.createPeople;
|
||||
|
||||
expect(createdPerson1.companyId).toBeDefined();
|
||||
expect(createdPerson1.company).not.toBeDefined();
|
||||
expect(createdPerson2.companyId).toBeDefined();
|
||||
expect(createdPerson2.company).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support depth 1 parameter', async () => {
|
||||
const requestBody = [
|
||||
{
|
||||
id: TEST_PERSON_1_ID,
|
||||
companyId: TEST_COMPANY_1_ID,
|
||||
},
|
||||
{
|
||||
id: TEST_PERSON_2_ID,
|
||||
companyId: TEST_COMPANY_1_ID,
|
||||
},
|
||||
];
|
||||
|
||||
await makeRestAPIRequest({
|
||||
method: 'post',
|
||||
path: `/batch/people?depth=1`,
|
||||
body: requestBody,
|
||||
})
|
||||
.expect(201)
|
||||
.expect((res) => {
|
||||
const [createdPerson1, createdPerson2] = res.body.data.createPeople;
|
||||
|
||||
expect(createdPerson1.company).toBeDefined();
|
||||
expect(createdPerson1.company.people).not.toBeDefined();
|
||||
expect(createdPerson2.company).toBeDefined();
|
||||
expect(createdPerson2.company.people).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support depth 2 parameter', async () => {
|
||||
const requestBody = [
|
||||
{
|
||||
id: TEST_PERSON_1_ID,
|
||||
companyId: TEST_COMPANY_1_ID,
|
||||
},
|
||||
{
|
||||
id: TEST_PERSON_2_ID,
|
||||
companyId: TEST_COMPANY_1_ID,
|
||||
},
|
||||
];
|
||||
|
||||
await makeRestAPIRequest({
|
||||
method: 'post',
|
||||
path: `/batch/people?depth=2`,
|
||||
body: requestBody,
|
||||
})
|
||||
.expect(201)
|
||||
.expect((res) => {
|
||||
const [createdPerson1, createdPerson2] = res.body.data.createPeople;
|
||||
|
||||
expect(createdPerson1.company.people).toBeDefined();
|
||||
expect(createdPerson2.company.people).toBeDefined();
|
||||
|
||||
const depth2Person1 = createdPerson1.company.people.find(
|
||||
(p) => p.id === createdPerson1.id,
|
||||
);
|
||||
const depth2Person2 = createdPerson2.company.people.find(
|
||||
(p) => p.id === createdPerson2.id,
|
||||
);
|
||||
|
||||
expect(depth2Person1).toBeDefined();
|
||||
expect(depth2Person2).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a BadRequestException when trying to create a person with an existing ID', async () => {
|
||||
const requestBody = [
|
||||
{
|
||||
id: TEST_PERSON_1_ID,
|
||||
},
|
||||
{
|
||||
id: TEST_PERSON_2_ID,
|
||||
},
|
||||
];
|
||||
|
||||
await makeRestAPIRequest({
|
||||
method: 'post',
|
||||
path: `/batch/people`,
|
||||
body: requestBody,
|
||||
});
|
||||
|
||||
await makeRestAPIRequest({
|
||||
method: 'post',
|
||||
path: `/batch/people`,
|
||||
body: requestBody,
|
||||
})
|
||||
.expect(400)
|
||||
.expect((res) => {
|
||||
expect(res.body.messages[0]).toContain(`Record already exists`);
|
||||
expect(res.body.error).toBe('BadRequestException');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user