diff --git a/packages/twenty-server/src/engine/api/rest/core/controllers/rest-api-core.controller.ts b/packages/twenty-server/src/engine/api/rest/core/controllers/rest-api-core.controller.ts index d24477bfc..29375a38a 100644 --- a/packages/twenty-server/src/engine/api/rest/core/controllers/rest-api-core.controller.ts +++ b/packages/twenty-server/src/engine/api/rest/core/controllers/rest-api-core.controller.ts @@ -15,10 +15,10 @@ 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/rest-api-core.service'; +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 { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -import { RestApiExceptionFilter } from 'src/engine/api/rest/rest-api-exception.filter'; @Controller('rest/*') @UseGuards(JwtAuthGuard, WorkspaceAuthGuard) @@ -53,10 +53,11 @@ export class RestApiCoreController { } @Post() + @UseFilters(RestApiExceptionFilter) async handleApiPost(@Req() request: Request, @Res() res: Response) { - const result = await this.restApiCoreService.createOne(request); + const result = await this.restApiCoreServiceV2.createOne(request); - res.status(201).send(cleanGraphQLResponse(result.data.data)); + res.status(201).send(result); } @Patch() diff --git a/packages/twenty-server/src/engine/api/rest/core/rest-api-core-v2.service.ts b/packages/twenty-server/src/engine/api/rest/core/rest-api-core-v2.service.ts index 760d36c9b..f5ac3c29b 100644 --- a/packages/twenty-server/src/engine/api/rest/core/rest-api-core-v2.service.ts +++ b/packages/twenty-server/src/engine/api/rest/core/rest-api-core-v2.service.ts @@ -15,39 +15,16 @@ export class RestApiCoreServiceV2 { ) {} async delete(request: Request) { - const { workspace } = request; - const { object: parsedObject, id: recordId } = parseCorePath(request); - - const objectMetadata = await this.coreQueryBuilderFactory.getObjectMetadata( - request, - parsedObject, - ); - - if (!objectMetadata) { - throw new BadRequestException('Object metadata not found'); - } + const { id: recordId } = parseCorePath(request); if (!recordId) { throw new BadRequestException('Record ID not found'); } - const objectMetadataNameSingular = - objectMetadata.objectMetadataItem.nameSingular; - - if (!workspace?.id) { - throw new BadRequestException('Workspace not found'); - } - - const repository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspace.id, - objectMetadataNameSingular, - ); - + const { objectMetadataNameSingular, repository } = + await this.getRepositoryAndMetadataOrFail(request); const recordToDelete = await repository.findOneOrFail({ - where: { - id: recordId, - }, + where: { id: recordId }, }); await repository.delete(recordId); @@ -57,6 +34,20 @@ export class RestApiCoreServiceV2 { }); } + async createOne(request: Request) { + const { body } = request; + + const { objectMetadataNameSingular, repository } = + await this.getRepositoryAndMetadataOrFail(request); + const createdRecord = await repository.save(body); + + return this.formatResult( + 'create', + objectMetadataNameSingular, + createdRecord, + ); + } + private formatResult( operation: 'delete' | 'create' | 'update' | 'find', objectNameSingular: string, @@ -70,4 +61,32 @@ export class RestApiCoreServiceV2 { return result; } + + private async getRepositoryAndMetadataOrFail(request: Request) { + const { workspace } = request; + const { object: parsedObject } = parseCorePath(request); + + const objectMetadata = await this.coreQueryBuilderFactory.getObjectMetadata( + request, + parsedObject, + ); + + if (!objectMetadata) { + throw new BadRequestException('Object metadata not found'); + } + + if (!workspace?.id) { + throw new BadRequestException('Workspace not found'); + } + + const objectMetadataNameSingular = + objectMetadata.objectMetadataItem.nameSingular; + const repository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspace.id, + objectMetadataNameSingular, + ); + + return { objectMetadataNameSingular, repository }; + } } diff --git a/packages/twenty-server/src/engine/core-modules/exception-handler/http-exception-handler.service.ts b/packages/twenty-server/src/engine/core-modules/exception-handler/http-exception-handler.service.ts index 3aeb598a2..e2eb2c947 100644 --- a/packages/twenty-server/src/engine/core-modules/exception-handler/http-exception-handler.service.ts +++ b/packages/twenty-server/src/engine/core-modules/exception-handler/http-exception-handler.service.ts @@ -51,7 +51,7 @@ export class HttpExceptionHandlerService { return response.status(statusCode).send({ statusCode, - error: exception.code || 'Bad Request', + error: exception.name || 'Bad Request', messages: [exception?.message], }); }; diff --git a/packages/twenty-server/test/integration/rest/suites/rest-api-core-create-one.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/rest-api-core-create-one.integration-spec.ts new file mode 100644 index 000000000..fc60cb4f8 --- /dev/null +++ b/packages/twenty-server/test/integration/rest/suites/rest-api-core-create-one.integration-spec.ts @@ -0,0 +1,95 @@ +import { + FAKE_PERSON_ID, + PERSON_2_ID, +} from 'test/integration/constants/mock-person-ids.constants'; +import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util'; +import { generateRecordName } from 'test/integration/utils/generate-record-name'; + +describe('Core REST API Create One endpoint', () => { + afterAll(async () => { + await makeRestAPIRequest({ + method: 'delete', + path: `/people/${PERSON_2_ID}`, + }).expect(200); + }); + + it('2.a. should create a new person', async () => { + const personCity = generateRecordName(PERSON_2_ID); + const requestBody = { + id: PERSON_2_ID, + city: personCity, + }; + + const response = await makeRestAPIRequest({ + method: 'post', + path: `/people`, + body: requestBody, + }); + + const createdPerson = response.body.data.createPerson; + + expect(createdPerson.id).toBe(PERSON_2_ID); + expect(createdPerson.city).toBe(personCity); + }); + + it('2.b. should return a BadRequestException when trying to create a person with an existing ID', async () => { + const personCity = generateRecordName(PERSON_2_ID); + const requestBody = { + id: PERSON_2_ID, + city: personCity, + }; + + await makeRestAPIRequest({ + method: 'post', + path: `/people`, + body: requestBody, + }) + .expect(400) + .expect((res) => { + expect(res.body.messages[0]).toContain( + `duplicate key value violates unique constraint`, + ); + expect(res.body.error).toBe('QueryFailedError'); + }); + }); + + it('2.c. should return an UnauthorizedException when no token is provided', async () => { + await makeRestAPIRequest({ + method: 'post', + path: `/people`, + headers: { authorization: '' }, + body: { id: FAKE_PERSON_ID, city: 'FakeCity' }, + }) + .expect(401) + .expect((res) => { + expect(res.body.error).toBe('UNAUTHENTICATED'); + }); + }); + + it('2.d. should return an UnauthorizedException when an invalid token is provided', async () => { + await makeRestAPIRequest({ + method: 'post', + path: `/people`, + body: { id: FAKE_PERSON_ID, city: 'FakeCity' }, + headers: { authorization: 'Bearer invalid-token' }, + }) + .expect(401) + .expect((res) => { + expect(res.body.error).toBe('UNAUTHENTICATED'); + }); + }); + + it('2.e. should return an UnauthorizedException when an expired token is provided', async () => { + await makeRestAPIRequest({ + method: 'post', + path: `/people`, + body: { id: FAKE_PERSON_ID, city: 'FakeCity' }, + headers: { authorization: `Bearer ${EXPIRED_ACCESS_TOKEN}` }, + }) + .expect(401) + .expect((res) => { + expect(res.body.error).toBe('UNAUTHENTICATED'); + expect(res.body.messages[0]).toBe('Token has expired.'); + }); + }); +}); diff --git a/packages/twenty-server/test/integration/rest/suites/rest-api-core-delete.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/rest-api-core-delete.integration-spec.ts index 54e4add3d..70a27d894 100644 --- a/packages/twenty-server/test/integration/rest/suites/rest-api-core-delete.integration-spec.ts +++ b/packages/twenty-server/test/integration/rest/suites/rest-api-core-delete.integration-spec.ts @@ -3,7 +3,6 @@ import { PERSON_1_ID, } from 'test/integration/constants/mock-person-ids.constants'; import { PERSON_GQL_FIELDS } from 'test/integration/constants/person-gql-fields.constants'; -import { createManyOperationFactory } from 'test/integration/graphql/utils/create-many-operation-factory.util'; import { findOneOperationFactory } from 'test/integration/graphql/utils/find-one-operation-factory.util'; import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util'; @@ -15,21 +14,15 @@ describe('Core REST API Delete One endpoint', () => { beforeAll(async () => { const personCity1 = generateRecordName(PERSON_1_ID); - // TODO: move this creation to REST API when the POST method is migrated - const graphqlOperation = createManyOperationFactory({ - objectMetadataSingularName: 'person', - objectMetadataPluralName: 'people', - gqlFields: PERSON_GQL_FIELDS, - data: [ - { - id: PERSON_1_ID, - city: personCity1, - }, - ], + const response = await makeRestAPIRequest({ + method: 'post', + path: '/people', + body: { + id: PERSON_1_ID, + city: personCity1, + }, }); - const response = await makeGraphqlAPIRequest(graphqlOperation); - people = response.body.data.createPeople; expect(people.length).toBe(1); expect(people[0].id).toBe(PERSON_1_ID); @@ -55,16 +48,19 @@ describe('Core REST API Delete One endpoint', () => { }); it('1a. should delete one person', async () => { - const response = await makeRestAPIRequest( - 'delete', - `/people/${PERSON_1_ID}`, - ); + const response = await makeRestAPIRequest({ + method: 'delete', + path: `/people/${PERSON_1_ID}`, + }); expect(response.body.data.deletePerson.id).toBe(PERSON_1_ID); }); it('1.b. should return a BadRequestException when trying to delete a non-existing person', async () => { - await makeRestAPIRequest('delete', `/people/${FAKE_PERSON_ID}`) + await makeRestAPIRequest({ + method: 'delete', + path: `/people/${FAKE_PERSON_ID}`, + }) .expect(400) .expect((res) => { expect(res.body.messages[0]).toContain( @@ -75,8 +71,12 @@ describe('Core REST API Delete One endpoint', () => { }); it('1.c. should return an UnauthorizedException when no token is provided', async () => { - await makeRestAPIRequest('delete', `/people/${PERSON_1_ID}`, { - authorization: '', + await makeRestAPIRequest({ + method: 'delete', + path: `/people/${PERSON_1_ID}`, + headers: { + authorization: '', + }, }) .expect(401) .expect((res) => { @@ -85,8 +85,12 @@ describe('Core REST API Delete One endpoint', () => { }); it('1.d. should return an UnauthorizedException when an invalid token is provided', async () => { - await makeRestAPIRequest('delete', `/people/${PERSON_1_ID}`, { - authorization: 'Bearer invalid-token', + await makeRestAPIRequest({ + method: 'delete', + path: `/people/${PERSON_1_ID}`, + headers: { + authorization: 'Bearer invalid-token', + }, }) .expect(401) .expect((res) => { @@ -95,8 +99,12 @@ describe('Core REST API Delete One endpoint', () => { }); it('1.e. should return an UnauthorizedException when an expired token is provided', async () => { - await makeRestAPIRequest('delete', `/people/${PERSON_1_ID}`, { - authorization: `Bearer ${EXPIRED_ACCESS_TOKEN}`, + await makeRestAPIRequest({ + method: 'delete', + path: `/people/${PERSON_1_ID}`, + headers: { + authorization: `Bearer ${EXPIRED_ACCESS_TOKEN}`, + }, }) .expect(401) .expect((res) => { diff --git a/packages/twenty-server/test/integration/rest/utils/make-rest-api-request.util.ts b/packages/twenty-server/test/integration/rest/utils/make-rest-api-request.util.ts index 6fd5f5912..90794b21e 100644 --- a/packages/twenty-server/test/integration/rest/utils/make-rest-api-request.util.ts +++ b/packages/twenty-server/test/integration/rest/utils/make-rest-api-request.util.ts @@ -4,15 +4,23 @@ import request from 'supertest'; export type RestAPIRequestMethod = 'get' | 'post' | 'put' | 'patch' | 'delete'; -export const makeRestAPIRequest = ( - method: RestAPIRequestMethod, - path: string, - headers: IncomingHttpHeaders = {}, -) => { +interface RestAPIRequestParams { + method: RestAPIRequestMethod; + path: string; + headers?: IncomingHttpHeaders; + body?: any; +} + +export const makeRestAPIRequest = ({ + method, + path, + headers = {}, + body, +}: RestAPIRequestParams) => { const client = request(`http://localhost:${APP_PORT}`); - return client[method]('/rest' + path) + return client[method](`/rest${path}`) .set('Authorization', `Bearer ${ACCESS_TOKEN}`) - .set({ ...headers }) - .send(); + .set(headers) + .send(body ? JSON.stringify(body) : undefined); };