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

@ -10,6 +10,7 @@ import {
RecordPositionService, RecordPositionService,
RecordPositionServiceCreateArgs, RecordPositionServiceCreateArgs,
} from 'src/engine/core-modules/record-position/services/record-position.service'; } from 'src/engine/core-modules/record-position/services/record-position.service';
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
describe('QueryRunnerArgsFactory', () => { describe('QueryRunnerArgsFactory', () => {
@ -65,6 +66,7 @@ describe('QueryRunnerArgsFactory', () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [ providers: [
QueryRunnerArgsFactory, QueryRunnerArgsFactory,
RecordInputTransformerService,
{ {
provide: RecordPositionService, provide: RecordPositionService,
useValue: recordPositionService, useValue: recordPositionService,

View File

@ -1,6 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ServerBlockNoteEditor } from '@blocknote/server-util';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { import {
@ -21,13 +20,9 @@ import {
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { lowercaseDomain } from 'src/engine/api/graphql/workspace-query-runner/utils/query-runner-links.util';
import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service'; import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
import {
RichTextV2Metadata,
richTextV2ValueSchema,
} from 'src/engine/metadata-modules/field-metadata/composite-types/rich-text-v2.composite-type';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
type ArgPositionBackfillInput = { type ArgPositionBackfillInput = {
argIndex?: number; argIndex?: number;
@ -36,7 +31,10 @@ type ArgPositionBackfillInput = {
@Injectable() @Injectable()
export class QueryRunnerArgsFactory { export class QueryRunnerArgsFactory {
constructor(private readonly recordPositionService: RecordPositionService) {} constructor(
private readonly recordPositionService: RecordPositionService,
private readonly recordInputTransformerService: RecordInputTransformerService,
) {}
async create( async create(
args: ResolverArgs, args: ResolverArgs,
@ -101,7 +99,7 @@ export class QueryRunnerArgsFactory {
case ResolverArgsType.UpdateMany: case ResolverArgsType.UpdateMany:
return { return {
...args, ...args,
filter: await this.overrideFilterByFieldMetadata( filter: this.overrideFilterByFieldMetadata(
(args as UpdateManyResolverArgs).filter, (args as UpdateManyResolverArgs).filter,
fieldMetadataMapByNameByName, fieldMetadataMapByNameByName,
), ),
@ -118,7 +116,7 @@ export class QueryRunnerArgsFactory {
case ResolverArgsType.FindOne: case ResolverArgsType.FindOne:
return { return {
...args, ...args,
filter: await this.overrideFilterByFieldMetadata( filter: this.overrideFilterByFieldMetadata(
(args as FindOneResolverArgs).filter, (args as FindOneResolverArgs).filter,
fieldMetadataMapByNameByName, fieldMetadataMapByNameByName,
), ),
@ -126,7 +124,7 @@ export class QueryRunnerArgsFactory {
case ResolverArgsType.FindMany: case ResolverArgsType.FindMany:
return { return {
...args, ...args,
filter: await this.overrideFilterByFieldMetadata( filter: this.overrideFilterByFieldMetadata(
(args as FindManyResolverArgs).filter, (args as FindManyResolverArgs).filter,
fieldMetadataMapByNameByName, fieldMetadataMapByNameByName,
), ),
@ -205,93 +203,17 @@ export class QueryRunnerArgsFactory {
return [key, newValue]; return [key, newValue];
} }
case FieldMetadataType.NUMBER: case FieldMetadataType.NUMBER:
return [key, value === null ? null : Number(value)];
case FieldMetadataType.RICH_TEXT: case FieldMetadataType.RICH_TEXT:
throw new Error( case FieldMetadataType.RICH_TEXT_V2:
'Rich text is not supported, please use RICH_TEXT_V2 instead', case FieldMetadataType.LINKS:
);
case FieldMetadataType.RICH_TEXT_V2: {
const richTextV2Value = richTextV2ValueSchema.parse(value);
const serverBlockNoteEditor = ServerBlockNoteEditor.create();
const convertedMarkdown = richTextV2Value.blocknote
? await serverBlockNoteEditor.blocksToMarkdownLossy(
JSON.parse(richTextV2Value.blocknote),
)
: null;
const convertedBlocknote = richTextV2Value.markdown
? JSON.stringify(
await serverBlockNoteEditor.tryParseMarkdownToBlocks(
richTextV2Value.markdown,
),
)
: null;
const valueInBothFormats: RichTextV2Metadata = {
markdown: richTextV2Value.markdown || convertedMarkdown,
blocknote: richTextV2Value.blocknote || convertedBlocknote,
};
return [key, valueInBothFormats];
}
case FieldMetadataType.LINKS: {
const newPrimaryLinkUrl = lowercaseDomain(value?.primaryLinkUrl);
let secondaryLinks = value?.secondaryLinks;
if (secondaryLinks) {
try {
const secondaryLinksArray = JSON.parse(secondaryLinks);
secondaryLinks = JSON.stringify(
secondaryLinksArray.map((link) => {
return {
...link,
url: lowercaseDomain(link.url),
};
}),
);
} catch {
/* empty */
}
}
return [
key,
{
...value,
primaryLinkUrl: newPrimaryLinkUrl,
secondaryLinks,
},
];
}
case FieldMetadataType.EMAILS: { case FieldMetadataType.EMAILS: {
let additionalEmails = value?.additionalEmails; const transformedValue =
const primaryEmail = value?.primaryEmail await this.recordInputTransformerService.transformFieldValue(
? value.primaryEmail.toLowerCase() fieldMetadata.type,
: ''; value,
);
if (additionalEmails) { return [key, transformedValue];
try {
const emailArray = JSON.parse(additionalEmails) as string[];
additionalEmails = JSON.stringify(
emailArray.map((email) => email.toLowerCase()),
);
} catch {
/* empty */
}
}
return [
key,
{
primaryEmail,
additionalEmails,
},
];
} }
default: default:
return [key, value]; return [key, value];
@ -342,7 +264,7 @@ export class QueryRunnerArgsFactory {
} else if (key === 'not') { } else if (key === 'not') {
acc[key] = overrideFilter(value); acc[key] = overrideFilter(value);
} else { } else {
acc[key] = this.transformValueByType( acc[key] = this.transformFilterValueByType(
key, key,
value, value,
fieldMetadataMapByName, fieldMetadataMapByName,
@ -356,7 +278,7 @@ export class QueryRunnerArgsFactory {
return overrideFilter(filter); return overrideFilter(filter);
} }
private transformValueByType( private transformFilterValueByType(
key: string, key: string,
value: any, value: any,
fieldMetadataMapByName: FieldMetadataMap, fieldMetadataMapByName: FieldMetadataMap,
@ -367,8 +289,9 @@ export class QueryRunnerArgsFactory {
return value; return value;
} }
// Special handling for filter values, which have a specific structure
switch (fieldMetadata.type) { switch (fieldMetadata.type) {
case 'NUMBER': { case FieldMetadataType.NUMBER: {
if (value?.is === 'NULL') { if (value?.is === 'NULL') {
return value; return value;
} else { } else {
@ -396,11 +319,9 @@ export class QueryRunnerArgsFactory {
return value; return value;
} }
switch (fieldMetadata.type) { return this.recordInputTransformerService.transformFieldValue(
case FieldMetadataType.NUMBER: fieldMetadata.type,
return Number(value); value,
default: );
return value;
}
} }
} }

View File

@ -13,6 +13,7 @@ import { FileModule } from 'src/engine/core-modules/file/file.module';
import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module'; import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module';
import { TelemetryModule } from 'src/engine/core-modules/telemetry/telemetry.module'; import { TelemetryModule } from 'src/engine/core-modules/telemetry/telemetry.module';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@ -30,6 +31,7 @@ import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listen
TelemetryModule, TelemetryModule,
FileModule, FileModule,
FeatureFlagModule, FeatureFlagModule,
RecordTransformerModule,
RecordPositionModule, RecordPositionModule,
], ],
providers: [ providers: [

View File

@ -1,13 +1,14 @@
import { BadRequestException, Injectable } from '@nestjs/common'; import { BadRequestException, Injectable } from '@nestjs/common';
import { Request } from 'express';
import { capitalize } from 'twenty-shared/utils'; 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 { 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 { 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 { 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 { 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 { 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'; import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
@ -16,7 +17,7 @@ export class RestApiCoreServiceV2 {
constructor( constructor(
private readonly coreQueryBuilderFactory: CoreQueryBuilderFactory, private readonly coreQueryBuilderFactory: CoreQueryBuilderFactory,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly recordInputTransformerService: RecordInputTransformerService,
protected readonly apiEventEmitterService: ApiEventEmitterService, protected readonly apiEventEmitterService: ApiEventEmitterService,
) {} ) {}
@ -47,11 +48,15 @@ export class RestApiCoreServiceV2 {
} }
async createOne(request: Request) { async createOne(request: Request) {
const { body } = request;
const { objectMetadataNameSingular, objectMetadata, repository } = const { objectMetadataNameSingular, objectMetadata, repository } =
await this.getRepositoryAndMetadataOrFail(request); 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( this.apiEventEmitterService.emitCreateEvents(
[createdRecord], [createdRecord],
@ -80,9 +85,14 @@ export class RestApiCoreServiceV2 {
where: { id: recordId }, where: { id: recordId },
}); });
const overriddenBody = await this.recordInputTransformerService.process({
recordInput: request.body,
objectMetadataMapItem: objectMetadata.objectMetadataMapItem,
});
const updatedRecord = await repository.save({ const updatedRecord = await repository.save({
...recordToUpdate, ...recordToUpdate,
...request.body, ...overriddenBody,
}); });
this.apiEventEmitterService.emitUpdateEvents( this.apiEventEmitterService.emitUpdateEvents(
@ -139,7 +149,11 @@ export class RestApiCoreServiceV2 {
objectMetadataNameSingular, objectMetadataNameSingular,
); );
return { objectMetadataNameSingular, objectMetadata, repository }; return {
objectMetadataNameSingular,
objectMetadata,
repository,
};
} }
private getAuthContextFromRequest(request: Request): AuthContext { 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 { RestApiMetadataService } from 'src/engine/api/rest/metadata/rest-api-metadata.service';
import { RestApiService } from 'src/engine/api/rest/rest-api.service'; import { RestApiService } from 'src/engine/api/rest/rest-api.service';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; 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 { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.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'; 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, AuthModule,
HttpModule, HttpModule,
TwentyORMModule, TwentyORMModule,
RecordTransformerModule,
], ],
controllers: [ controllers: [
RestApiMetadataController, RestApiMetadataController,

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { RecordInputTransformerService } from './services/record-input-transformer.service';
@Module({
providers: [RecordInputTransformerService],
exports: [RecordInputTransformerService],
})
export class RecordTransformerModule {}

View File

@ -0,0 +1,235 @@
import { Injectable } from '@nestjs/common';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { ServerBlockNoteEditor } from '@blocknote/server-util';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import {
RichTextV2Metadata,
richTextV2ValueSchema,
} from 'src/engine/metadata-modules/field-metadata/composite-types/rich-text-v2.composite-type';
import { lowercaseDomain } from 'src/engine/api/graphql/workspace-query-runner/utils/query-runner-links.util';
import { LinkMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
@Injectable()
export class RecordInputTransformerService {
async process({
recordInput,
objectMetadataMapItem,
}: {
recordInput: Record<string, any>;
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
}): Promise<Record<string, any>> {
if (!recordInput) {
return recordInput;
}
const fieldMetadataByFieldName = objectMetadataMapItem.fields.reduce(
(acc, field) => {
acc[field.name] = field;
return acc;
},
{} as Record<string, FieldMetadataInterface>,
);
let transformedEntries = {};
for (const [key, value] of Object.entries(recordInput)) {
const fieldMetadata = fieldMetadataByFieldName[key];
if (!fieldMetadata) {
transformedEntries = { ...transformedEntries, [key]: value };
continue;
}
const transformedValue = this.parseSubFields(
fieldMetadata.type,
await this.transformFieldValue(
fieldMetadata.type,
this.stringifySubFields(fieldMetadata.type, value),
),
);
transformedEntries = { ...transformedEntries, [key]: transformedValue };
}
return transformedEntries;
}
async transformFieldValue(
fieldType: FieldMetadataType,
value: any,
): Promise<any> {
if (!isDefined(value)) {
return value;
}
switch (fieldType) {
case FieldMetadataType.UUID:
return value || null;
case FieldMetadataType.NUMBER:
return value === null ? null : Number(value);
case FieldMetadataType.RICH_TEXT:
throw new Error(
'Rich text is not supported, please use RICH_TEXT_V2 instead',
);
case FieldMetadataType.RICH_TEXT_V2:
return this.transformRichTextV2Value(value);
case FieldMetadataType.LINKS:
return this.transformLinksValue(value);
case FieldMetadataType.EMAILS:
return this.transformEmailsValue(value);
default:
return value;
}
}
private async transformRichTextV2Value(
richTextValue: any,
): Promise<RichTextV2Metadata> {
const parsedValue = richTextV2ValueSchema.parse(richTextValue);
const serverBlockNoteEditor = ServerBlockNoteEditor.create();
const convertedMarkdown = parsedValue.blocknote
? await serverBlockNoteEditor.blocksToMarkdownLossy(
JSON.parse(parsedValue.blocknote),
)
: null;
const convertedBlocknote = parsedValue.markdown
? JSON.stringify(
await serverBlockNoteEditor.tryParseMarkdownToBlocks(
parsedValue.markdown,
),
)
: null;
return {
markdown: parsedValue.markdown || convertedMarkdown,
blocknote: parsedValue.blocknote || convertedBlocknote,
};
}
private transformLinksValue(value: any): any {
if (!value) {
return value;
}
const newPrimaryLinkUrl = lowercaseDomain(value?.primaryLinkUrl);
let secondaryLinks = value?.secondaryLinks;
if (secondaryLinks) {
try {
const secondaryLinksArray = JSON.parse(secondaryLinks);
secondaryLinks = JSON.stringify(
secondaryLinksArray.map((link: LinkMetadata) => {
return {
...link,
url: lowercaseDomain(link.url),
};
}),
);
} catch {
/* empty */
}
}
return {
...value,
primaryLinkUrl: newPrimaryLinkUrl,
secondaryLinks,
};
}
private transformEmailsValue(value: any): any {
if (!value) {
return value;
}
let additionalEmails = value?.additionalEmails;
const primaryEmail = value?.primaryEmail
? value.primaryEmail.toLowerCase()
: '';
if (additionalEmails) {
try {
const emailArray = JSON.parse(additionalEmails) as string[];
additionalEmails = JSON.stringify(
emailArray.map((email) => email.toLowerCase()),
);
} catch {
/* empty */
}
}
return {
primaryEmail,
additionalEmails,
};
}
private stringifySubFields(fieldMetadataType: FieldMetadataType, value: any) {
const compositeType = compositeTypeDefinitions.get(fieldMetadataType);
if (!compositeType) {
return value;
}
return Object.entries(value).reduce(
(acc, [subFieldName, subFieldValue]) => {
const subFieldType = compositeType.properties.find(
(property) => property.name === subFieldName,
)?.type;
if (subFieldType === FieldMetadataType.RAW_JSON) {
return {
...acc,
[subFieldName]: subFieldValue
? JSON.stringify(subFieldValue)
: subFieldValue,
};
}
return { ...acc, [subFieldName]: subFieldValue };
},
{},
);
}
private parseSubFields(fieldMetadataType: FieldMetadataType, value: any) {
const compositeType = compositeTypeDefinitions.get(fieldMetadataType);
if (!compositeType) {
return value;
}
return Object.entries(value).reduce(
(acc, [subFieldName, subFieldValue]: [string, any]) => {
const subFieldType = compositeType.properties.find(
(property) => property.name === subFieldName,
)?.type;
if (subFieldType === FieldMetadataType.RAW_JSON) {
return {
...acc,
[subFieldName]: subFieldValue
? JSON.parse(subFieldValue)
: subFieldValue,
};
}
return { ...acc, [subFieldName]: subFieldValue };
},
{},
);
}
}