Share RICH_TEXT_V2 field value override between REST and GraphQL APIs (#10912)

Fixes issue #10606.

This PR makes `RICH_TEXT_V2` field behavior in REST API matche the
current behavior in GraphQL API:

Currently both `markdown` and `blocknote` fields must be included in the
request, one of them can be `null`. The field with a `null` value will
be filled by the converted value of the other field.

In other words, this works:
```
curl http://localhost:3000/rest/notes \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzQxODA1MzQyLCJleHAiOjQ4OTU0MDUzNDEsImp0aSI6ImZlMzU0NTBkLTlhMDMtNGE2ZS04ODVjLTBlNTU3M2Y3YTE0NiJ9.6_g8cwoSE7ZCX1Zzsw44gZIyBdLKNsnDqMOmm1bKik0' \
  --data '{
  "position": 1,
  "title": "a",
  "bodyV2": {    
    "markdown": "test4\n\ntest3\n\n# test1\n",
    "blocknote": null
  },
  "createdBy": {
    "source": "EMAIL"
  }
}'
```

And this does not work:
```
curl http://localhost:3000/rest/notes \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzQxODA1MzQyLCJleHAiOjQ4OTU0MDUzNDEsImp0aSI6ImZlMzU0NTBkLTlhMDMtNGE2ZS04ODVjLTBlNTU3M2Y3YTE0NiJ9.6_g8cwoSE7ZCX1Zzsw44gZIyBdLKNsnDqMOmm1bKik0' \
  --data '{
  "position": 1,
  "title": "",
  "body": "",
  "bodyV2": {    
    "markdown": "test4\n\ntest3\n\n# test1\n"
  },
  "createdBy": {
    "source": "EMAIL"
  }
}'
```

---

It would be nice not to require the null value, maybe let's make that a
separate PR?
This commit is contained in:
eliasylonen
2025-04-08 00:30:53 +03:00
committed by GitHub
parent 6bc18960c9
commit 95eba07f6e
7 changed files with 295 additions and 110 deletions

View File

@ -1,13 +1,14 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { Request } from 'express';
import { capitalize } from 'twenty-shared/utils';
import { Request } from 'express';
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { CoreQueryBuilderFactory } from 'src/engine/api/rest/core/query-builder/core-query-builder.factory';
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
@ -16,7 +17,7 @@ export class RestApiCoreServiceV2 {
constructor(
private readonly coreQueryBuilderFactory: CoreQueryBuilderFactory,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly recordInputTransformerService: RecordInputTransformerService,
protected readonly apiEventEmitterService: ApiEventEmitterService,
) {}
@ -47,11 +48,15 @@ export class RestApiCoreServiceV2 {
}
async createOne(request: Request) {
const { body } = request;
const { objectMetadataNameSingular, objectMetadata, repository } =
await this.getRepositoryAndMetadataOrFail(request);
const createdRecord = await repository.save(body);
const overriddenBody = await this.recordInputTransformerService.process({
recordInput: request.body,
objectMetadataMapItem: objectMetadata.objectMetadataMapItem,
});
const createdRecord = await repository.save(overriddenBody);
this.apiEventEmitterService.emitCreateEvents(
[createdRecord],
@ -80,9 +85,14 @@ export class RestApiCoreServiceV2 {
where: { id: recordId },
});
const overriddenBody = await this.recordInputTransformerService.process({
recordInput: request.body,
objectMetadataMapItem: objectMetadata.objectMetadataMapItem,
});
const updatedRecord = await repository.save({
...recordToUpdate,
...request.body,
...overriddenBody,
});
this.apiEventEmitterService.emitUpdateEvents(
@ -139,7 +149,11 @@ export class RestApiCoreServiceV2 {
objectMetadataNameSingular,
);
return { objectMetadataNameSingular, objectMetadata, repository };
return {
objectMetadataNameSingular,
objectMetadata,
repository,
};
}
private getAuthContextFromRequest(request: Request): AuthContext {

View File

@ -14,6 +14,7 @@ import { RestApiMetadataController } from 'src/engine/api/rest/metadata/rest-api
import { RestApiMetadataService } from 'src/engine/api/rest/metadata/rest-api-metadata.service';
import { RestApiService } from 'src/engine/api/rest/rest-api.service';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module';
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service';
@ -26,6 +27,7 @@ import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-run
AuthModule,
HttpModule,
TwentyORMModule,
RecordTransformerModule,
],
controllers: [
RestApiMetadataController,