[3/n]: Migrate the PUT rest/* and PATCH rest/* to use TwentyORM (#10002)

# This PR

- Is addressing #3644 
- Migrates the PUT and PATCH rest/* endpoints to use twentyORM directly
- Adds integration tests
This commit is contained in:
P A C · 先生
2025-02-04 18:25:02 +02:00
committed by GitHub
parent 7dfb9dd77f
commit 5be22413c9
5 changed files with 144 additions and 5 deletions

View File

@ -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: [

View File

@ -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);
}
}

View File

@ -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<T>(
operation: 'delete' | 'create' | 'update' | 'find',
objectNameSingular: string,

View File

@ -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',
};

View File

@ -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');
});
});
});