diff --git a/packages/twenty-server/src/app.module.ts b/packages/twenty-server/src/app.module.ts index 45be22021..2708a1935 100644 --- a/packages/twenty-server/src/app.module.ts +++ b/packages/twenty-server/src/app.module.ts @@ -34,7 +34,12 @@ import { CoreEngineModule } from './engine/core-modules/core-engine.module'; import { I18nModule } from './engine/core-modules/i18n/i18n.module'; // TODO: Remove this middleware when all the rest endpoints are migrated to TwentyORM -const MIGRATED_REST_METHODS = [RequestMethod.DELETE]; +const MIGRATED_REST_METHODS = [ + RequestMethod.DELETE, + RequestMethod.POST, + RequestMethod.PATCH, + RequestMethod.PUT, +]; @Module({ imports: [ 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 29375a38a..dfdd7b8d8 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 @@ -61,19 +61,21 @@ export class RestApiCoreController { } @Patch() + @UseFilters(RestApiExceptionFilter) async handleApiPatch(@Req() request: Request, @Res() res: Response) { - const result = await this.restApiCoreService.update(request); + const result = await this.restApiCoreServiceV2.update(request); - res.status(200).send(cleanGraphQLResponse(result.data.data)); + res.status(200).send(result); } // 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() + @UseFilters(RestApiExceptionFilter) async handleApiPut(@Req() request: Request, @Res() res: Response) { - const result = await this.restApiCoreService.update(request); + const result = await this.restApiCoreServiceV2.update(request); - res.status(200).send(cleanGraphQLResponse(result.data.data)); + res.status(200).send(result); } } 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 f5ac3c29b..58cb5d36b 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 @@ -48,6 +48,32 @@ export class RestApiCoreServiceV2 { ); } + async update(request: Request) { + const { id: recordId } = parseCorePath(request); + + if (!recordId) { + throw new BadRequestException('Record ID not found'); + } + + const { objectMetadataNameSingular, repository } = + await this.getRepositoryAndMetadataOrFail(request); + + const recordToUpdate = await repository.findOneOrFail({ + where: { id: recordId }, + }); + + const updatedRecord = await repository.save({ + ...recordToUpdate, + ...request.body, + }); + + return this.formatResult( + 'update', + objectMetadataNameSingular, + updatedRecord, + ); + } + private formatResult( operation: 'delete' | 'create' | 'update' | 'find', objectNameSingular: string, diff --git a/packages/twenty-server/test/integration/constants/initial-person-data.constants.ts b/packages/twenty-server/test/integration/constants/initial-person-data.constants.ts new file mode 100644 index 000000000..f0c1fa922 --- /dev/null +++ b/packages/twenty-server/test/integration/constants/initial-person-data.constants.ts @@ -0,0 +1,16 @@ +import { PERSON_2_ID } from 'test/integration/constants/mock-person-ids.constants'; + +export const INITIAL_PERSON_DATA = { + id: PERSON_2_ID, + name: { + firstName: 'Testing', + lastName: 'User', + }, + emails: { + primaryEmail: 'test8@user.com', + additionalEmails: ['user8@example.com'], + }, + city: 'New York', + jobTitle: 'Manager', + companyId: '20202020-0713-40a5-8216-82802401d33e', +}; diff --git a/packages/twenty-server/test/integration/rest/suites/rest-api-core-update.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/rest-api-core-update.integration-spec.ts new file mode 100644 index 000000000..ea773f024 --- /dev/null +++ b/packages/twenty-server/test/integration/rest/suites/rest-api-core-update.integration-spec.ts @@ -0,0 +1,90 @@ +import { INITIAL_PERSON_DATA } from 'test/integration/constants/initial-person-data.constants'; +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 Update One endpoint', () => { + let initialPersonData; + + beforeAll(async () => { + initialPersonData = INITIAL_PERSON_DATA; + + await makeRestAPIRequest({ + method: 'post', + path: `/people`, + body: initialPersonData, + }).expect(200); + }); + + afterAll(async () => { + await makeRestAPIRequest({ + method: 'delete', + path: `/people/${PERSON_2_ID}`, + }).expect(200); + }); + + it('3.a. should update an existing person (name, emails, and city)', async () => { + const updatedData = { + name: { + firstName: 'Updated', + lastName: 'Person', + }, + emails: { + primaryEmail: 'updated@example.com', + additionalEmails: ['extra@example.com'], + }, + city: generateRecordName(PERSON_2_ID), + }; + + const response = await makeRestAPIRequest({ + method: 'patch', + path: `/people/${PERSON_2_ID}`, + body: updatedData, + }).expect(200); + + const updatedPerson = response.body.data.updatePerson; + + expect(updatedPerson.id).toBe(PERSON_2_ID); + expect(updatedPerson.name.firstName).toBe(updatedData.name.firstName); + expect(updatedPerson.name.lastName).toBe(updatedData.name.lastName); + expect(updatedPerson.emails.primaryEmail).toBe( + updatedData.emails.primaryEmail, + ); + expect(updatedPerson.emails.additionalEmails).toEqual( + updatedData.emails.additionalEmails, + ); + expect(updatedPerson.city).toBe(updatedData.city); + + expect(updatedPerson.jobTitle).toBe(initialPersonData.jobTitle); + expect(updatedPerson.companyId).toBe(initialPersonData.companyId); + }); + + it('3.b. should return a BadRequestException when trying to update a non-existing person', async () => { + await makeRestAPIRequest({ + method: 'patch', + path: `/people/${FAKE_PERSON_ID}`, + body: { city: 'NonExistingCity' }, + }) + .expect(400) + .expect((res) => { + expect(res.body.error).toBe('BadRequestException'); + expect(res.body.messages[0]).toContain('Record ID not found'); + }); + }); + + it('3.c. should return an UnauthorizedException when an invalid token is provided', async () => { + await makeRestAPIRequest({ + method: 'patch', + path: `/people/${PERSON_2_ID}`, + headers: { authorization: 'Bearer invalid-token' }, + body: { city: 'InvalidTokenCity' }, + }) + .expect(401) + .expect((res) => { + expect(res.body.error).toBe('UNAUTHENTICATED'); + }); + }); +});