From 7b6fb52df779c792bd0f2d0f41b05eda5b58f4bc Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Tue, 24 Mar 2026 14:53:15 +0100 Subject: [PATCH] fix: validate blocknote JSON in rich text fields (#18902) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - **Backend**: Add JSON validation for the `blocknote` subfield in rich text API inputs — rejects values that aren't valid JSON or aren't arrays (BlockNote content is always `PartialBlock[]`). This prevents corrupted data from being persisted to the database. - **Frontend**: Replace all 5 unprotected `JSON.parse` calls on blocknote content with the safe `parseJson` utility from `twenty-shared`. Invalid content now degrades gracefully (empty block / empty string / unchanged passthrough) instead of crashing the app. - **Tests**: Added integration tests for invalid blocknote JSON (both GraphQL and REST), unit tests for the new validation, and updated existing test constants to use valid BlockNote JSON. ## Context A user reported a `SyntaxError: Expected ',' or ']' after array element` crash caused by malformed blocknote JSON stored in the database. The data had `"children":[]` nested inside the `content` array instead of as a sibling property. The API accepted this invalid JSON because it only validated that `blocknote` was a string, not that it contained valid JSON. On the frontend, 5 call sites used bare `JSON.parse` with no error handling, causing a white-screen crash. ## Test plan - [x] Unit tests pass: `validate-rich-text-field-or-throw.util.spec.ts` (10/10) - [x] Integration tests pass: `rich-text-field-create-input-validation` (8/8) - [ ] Verify creating a note with valid rich text still works end-to-end - [ ] Verify API returns clear error when blocknote contains invalid JSON - [ ] Verify frontend renders empty block instead of crashing when encountering corrupted data 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .../__tests__/getActivitySummary.test.ts | 2 +- .../getActivityAttachmentPathsAndName.ts | 20 ++-- .../activities/utils/getActivityPreview.ts | 76 ++++++--------- .../activities/utils/getActivitySummary.ts | 28 +----- .../hooks/useReplaceBlockEditorContent.ts | 15 +-- .../utils/prepareBodyWithSignedUrls.ts | 6 +- .../components/RichTextFieldDisplay.tsx | 9 +- .../data-arg-processor.service.spec.ts.snap | 6 +- ...-inputs-by-field-metadata-type.constant.ts | 14 ++- ...date-rich-text-field-or-throw.util.spec.ts | 45 ++++++++- .../validate-rich-text-field-or-throw.util.ts | 96 ++++++++++++------- ...-input-validation.integration-spec.ts.snap | 10 +- ...e-input-by-field-metadata-type.constant.ts | 15 +++ ...e-input-by-field-metadata-type.constant.ts | 6 +- 14 files changed, 206 insertions(+), 142 deletions(-) diff --git a/packages/twenty-front/src/modules/activities/utils/__tests__/getActivitySummary.test.ts b/packages/twenty-front/src/modules/activities/utils/__tests__/getActivitySummary.test.ts index 763b877fa84..a975557c340 100644 --- a/packages/twenty-front/src/modules/activities/utils/__tests__/getActivitySummary.test.ts +++ b/packages/twenty-front/src/modules/activities/utils/__tests__/getActivitySummary.test.ts @@ -107,7 +107,7 @@ describe('getActivitySummary', () => { const res = getActivitySummary(JSON.stringify(activityBody)); - expect(res).toEqual(''); + expect(res).toEqual('TEST'); }); it('should work for table as first block', () => { diff --git a/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentPathsAndName.ts b/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentPathsAndName.ts index 6919b7ff789..07ac380fbdb 100644 --- a/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentPathsAndName.ts +++ b/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentPathsAndName.ts @@ -1,22 +1,30 @@ import { isNonEmptyString } from '@sniptt/guards'; +import { parseInitialBlocknote } from '@/blocknote-editor/utils/parseInitialBlocknote'; + export type AttachmentInfo = { path: string; name: string; }; + +const ATTACHMENT_BLOCK_TYPES = ['image', 'file', 'video', 'audio']; + export const getActivityAttachmentPathsAndName = ( stringifiedActivityBlocknote: string, ): AttachmentInfo[] => { - const activityBlocknote = JSON.parse(stringifiedActivityBlocknote ?? '{}'); + const blocks = parseInitialBlocknote(stringifiedActivityBlocknote) ?? []; + + return blocks.reduce((acc: AttachmentInfo[], block) => { + const props = block.props as { url?: string; name?: string } | undefined; - return activityBlocknote.reduce((acc: AttachmentInfo[], block: any) => { if ( - ['image', 'file', 'video', 'audio'].includes(block.type) && - isNonEmptyString(block.props.url) + block.type !== undefined && + ATTACHMENT_BLOCK_TYPES.includes(block.type) && + isNonEmptyString(props?.url) ) { acc.push({ - path: block.props.url, - name: block.props.name, + path: props.url, + name: props?.name ?? '', }); } return acc; diff --git a/packages/twenty-front/src/modules/activities/utils/getActivityPreview.ts b/packages/twenty-front/src/modules/activities/utils/getActivityPreview.ts index 91426553cc2..6258276d96b 100644 --- a/packages/twenty-front/src/modules/activities/utils/getActivityPreview.ts +++ b/packages/twenty-front/src/modules/activities/utils/getActivityPreview.ts @@ -1,57 +1,35 @@ -// TODO: merge with getFirstNonEmptyLineOfRichText (and one duplicate I saw and also added a note on) - +import type { PartialBlock } from '@blocknote/core'; import { isArray, isNonEmptyString } from '@sniptt/guards'; +import { isDefined } from 'twenty-shared/utils'; -interface BaseNode { - type: string; - content?: RichTextNode[]; - [key: string]: unknown; -} +import { parseInitialBlocknote } from '@/blocknote-editor/utils/parseInitialBlocknote'; -interface TextNode extends BaseNode { - type: 'text'; - text: string; -} +const extractTextFromBlock = (block: PartialBlock): string => { + if (!isDefined(block.content) || !isArray(block.content)) return ''; -interface LinkNode extends BaseNode { - type: 'link'; - href?: string; - content?: RichTextNode[]; -} - -type RichTextNode = TextNode | LinkNode | BaseNode; - -const isTextNode = (node: RichTextNode): node is TextNode => - node.type === 'text'; - -const isLinkNode = (node: RichTextNode): node is LinkNode => - node.type === 'link'; + return ( + block.content as Array<{ + type: string; + text?: string; + content?: Array<{ type: string; text?: string }>; + }> + ) + .map((inline) => { + if (inline.type === 'text') { + return inline.text ?? ''; + } + if (inline.type === 'link' && isArray(inline.content)) { + return inline.content + .map((child) => (child.type === 'text' ? (child.text ?? '') : '')) + .join(' '); + } + return ''; + }) + .join(''); +}; export const getActivityPreview = (activityBody: string | null): string => { - const noteBody: RichTextNode[] = activityBody ? JSON.parse(activityBody) : []; + const blocks = parseInitialBlocknote(activityBody) ?? []; - const extractText = (node: RichTextNode | undefined | null): string => { - if (!node) return ''; - - if (isTextNode(node)) { - return node.text ?? ''; - } - - if (isLinkNode(node)) { - return node.content?.map(extractText).join(' ') ?? ''; - } - - if (isArray(node.content)) { - return node.content.map(extractText).join(' '); - } - - return ''; - }; - - return noteBody.length - ? noteBody - .map((node) => extractText(node)) - .filter(isNonEmptyString) - .join('\n') - : ''; + return blocks.map(extractTextFromBlock).filter(isNonEmptyString).join('\n'); }; diff --git a/packages/twenty-front/src/modules/activities/utils/getActivitySummary.ts b/packages/twenty-front/src/modules/activities/utils/getActivitySummary.ts index 39567487eda..7f95f6c723d 100644 --- a/packages/twenty-front/src/modules/activities/utils/getActivitySummary.ts +++ b/packages/twenty-front/src/modules/activities/utils/getActivitySummary.ts @@ -1,26 +1,8 @@ -import { isArray, isNonEmptyString } from '@sniptt/guards'; +import { parseInitialBlocknote } from '@/blocknote-editor/utils/parseInitialBlocknote'; +import { getFirstNonEmptyLineOfRichText } from '@/blocknote-editor/utils/getFirstNonEmptyLineOfRichText'; -// TODO: merge with getFirstNonEmptyLineOfRichText -export const getActivitySummary = (activityBody: string | null) => { - const noteBody = activityBody ? JSON.parse(activityBody) : []; +export const getActivitySummary = (activityBody: string | null): string => { + const blocks = parseInitialBlocknote(activityBody) ?? null; - if (!noteBody.length) { - return ''; - } - - const firstNoteBlockContent = noteBody[0].content; - - if (!firstNoteBlockContent) { - return ''; - } - - if (isNonEmptyString(firstNoteBlockContent.text)) { - return noteBody[0].content.text; - } - - if (isArray(firstNoteBlockContent)) { - return firstNoteBlockContent.map((content: any) => content.text).join(' '); - } - - return ''; + return getFirstNonEmptyLineOfRichText(blocks); }; diff --git a/packages/twenty-front/src/modules/blocknote-editor/hooks/useReplaceBlockEditorContent.ts b/packages/twenty-front/src/modules/blocknote-editor/hooks/useReplaceBlockEditorContent.ts index 8090a3e8cec..d9e8ea58bc8 100644 --- a/packages/twenty-front/src/modules/blocknote-editor/hooks/useReplaceBlockEditorContent.ts +++ b/packages/twenty-front/src/modules/blocknote-editor/hooks/useReplaceBlockEditorContent.ts @@ -2,8 +2,8 @@ import { useCallback } from 'react'; import { useStore } from 'jotai'; import { type BLOCK_SCHEMA } from '@/blocknote-editor/blocks/Schema'; +import { parseInitialBlocknote } from '@/blocknote-editor/utils/parseInitialBlocknote'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; -import { isNonEmptyString } from '@sniptt/guards'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; export const useReplaceBlockEditorContent = ( @@ -20,12 +20,15 @@ export const useReplaceBlockEditorContent = ( | { blocknote?: string | null } | undefined; - const content = isNonEmptyString(fieldValue?.blocknote) - ? JSON.parse(fieldValue.blocknote) - : [{ type: 'paragraph', content: '' }]; + const content = parseInitialBlocknote(fieldValue?.blocknote) ?? [ + { type: 'paragraph' as const, content: '' }, + ]; - if (!isDeeplyEqual(editor.document, content)) { - editor.replaceBlocks(editor.document, content); + if (!isDeeplyEqual(editor.document, content as typeof editor.document)) { + editor.replaceBlocks( + editor.document, + content as typeof editor.document, + ); } }, [store, editor, fieldName], diff --git a/packages/twenty-front/src/modules/blocknote-editor/utils/prepareBodyWithSignedUrls.ts b/packages/twenty-front/src/modules/blocknote-editor/utils/prepareBodyWithSignedUrls.ts index 64c455322b2..3c54a251394 100644 --- a/packages/twenty-front/src/modules/blocknote-editor/utils/prepareBodyWithSignedUrls.ts +++ b/packages/twenty-front/src/modules/blocknote-editor/utils/prepareBodyWithSignedUrls.ts @@ -1,4 +1,4 @@ -import type { PartialBlock } from '@blocknote/core'; +import { parseInitialBlocknote } from '@/blocknote-editor/utils/parseInitialBlocknote'; // TODO: This function is extracted but its not doing what it is supposed to do. It is not signing the urls. It is just parsing the image urls. // tracking issue - https://github.com/twentyhq/twenty/issues/8351 @@ -7,7 +7,9 @@ export const prepareBodyWithSignedUrls = ( ): string => { if (!newStringifiedBody) return newStringifiedBody; - const body: PartialBlock[] = JSON.parse(newStringifiedBody); + const body = parseInitialBlocknote(newStringifiedBody); + + if (!body) return newStringifiedBody; const bodyWithSignedPayload = body.map((block) => { if (block.type !== 'image' || !block.props?.url) { diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/RichTextFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/RichTextFieldDisplay.tsx index 9aa3b04e891..7234b83b6d2 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/RichTextFieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/RichTextFieldDisplay.tsx @@ -1,16 +1,11 @@ import { useRichTextFieldDisplay } from '@/object-record/record-field/ui/meta-types/hooks/useRichTextFieldDisplay'; import { getFirstNonEmptyLineOfRichText } from '@/blocknote-editor/utils/getFirstNonEmptyLineOfRichText'; -import type { PartialBlock } from '@blocknote/core'; -import { isNonEmptyString } from '@sniptt/guards'; -import { isDefined, parseJson } from 'twenty-shared/utils'; +import { parseInitialBlocknote } from '@/blocknote-editor/utils/parseInitialBlocknote'; export const RichTextFieldDisplay = () => { const { fieldValue } = useRichTextFieldDisplay(); - const blocks = - isDefined(fieldValue) && isNonEmptyString(fieldValue.blocknote) - ? parseJson(fieldValue.blocknote) - : null; + const blocks = parseInitialBlocknote(fieldValue?.blocknote) ?? null; return (
diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/__snapshots__/data-arg-processor.service.spec.ts.snap b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/__snapshots__/data-arg-processor.service.spec.ts.snap index 75585e62c5b..e865efde4fc 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/__snapshots__/data-arg-processor.service.spec.ts.snap +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/__snapshots__/data-arg-processor.service.spec.ts.snap @@ -172,11 +172,11 @@ exports[`DataArgProcessorService failing inputs validation RELATION should throw exports[`DataArgProcessorService failing inputs validation RELATION should throw for invalid input #5: "non-uuid" 1`] = `"Invalid UUID value 'non-uuid' for field "manyToOneRelationFieldId""`; -exports[`DataArgProcessorService failing inputs validation RICH_TEXT should throw for invalid input #1: "not-a-rich-text" 1`] = `"Invalid rich text v2 value 'not-a-rich-text' for field "richTextField" - Should be an object"`; +exports[`DataArgProcessorService failing inputs validation RICH_TEXT should throw for invalid input #1: "not-a-rich-text" 1`] = `"Invalid object value 'not-a-rich-text' for field "richTextField""`; -exports[`DataArgProcessorService failing inputs validation RICH_TEXT should throw for invalid input #2: 1 1`] = `"Invalid rich text v2 value 1 for field "richTextField" - Should be an object"`; +exports[`DataArgProcessorService failing inputs validation RICH_TEXT should throw for invalid input #2: 1 1`] = `"Invalid object value 1 for field "richTextField""`; -exports[`DataArgProcessorService failing inputs validation RICH_TEXT should throw for invalid input #3: true 1`] = `"Invalid rich text v2 value true for field "richTextField" - Should be an object"`; +exports[`DataArgProcessorService failing inputs validation RICH_TEXT should throw for invalid input #3: true 1`] = `"Invalid object value true for field "richTextField""`; exports[`DataArgProcessorService failing inputs validation SELECT should throw for invalid input #1: "not-a-select-option" 1`] = `"Invalid value 'not-a-select-option' for field "selectField""`; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/constants/successful-inputs-by-field-metadata-type.constant.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/constants/successful-inputs-by-field-metadata-type.constant.ts index a773c208bbc..abddc471523 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/constants/successful-inputs-by-field-metadata-type.constant.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/constants/successful-inputs-by-field-metadata-type.constant.ts @@ -385,9 +385,19 @@ export const successfulInputsByFieldMetadataType: { ], [FieldMetadataType.RICH_TEXT]: [ { - input: { richTextField: { blocknote: 'test', markdown: 'test' } }, + input: { + richTextField: { + blocknote: + '[{"type":"paragraph","content":[{"type":"text","text":"test"}]}]', + markdown: 'test', + }, + }, expectedOutput: { - richTextField: { blocknote: 'test', markdown: 'test' }, + richTextField: { + blocknote: + '[{"type":"paragraph","content":[{"type":"text","text":"test"}]}]', + markdown: 'test', + }, }, }, { diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-rich-text-field-or-throw.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-rich-text-field-or-throw.util.spec.ts index 9c6da5098eb..fcf43105236 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-rich-text-field-or-throw.util.spec.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-rich-text-field-or-throw.util.spec.ts @@ -9,21 +9,36 @@ describe('validateRichTextFieldOrThrow', () => { expect(result).toBeNull(); }); - it('should return null when value is an empty object', () => { + it('should return value when value is an empty object', () => { const result = validateRichTextFieldOrThrow({}, 'testField'); - expect(result).toBeNull(); + expect(result).toEqual({}); }); it('should return the value when it has valid subfields', () => { const value = { - blocknote: 'some blocknote content', + blocknote: + '[{"type":"paragraph","content":[{"type":"text","text":"test"}]}]', markdown: '# Heading\nContent', }; const result = validateRichTextFieldOrThrow(value, 'testField'); expect(result).toEqual(value); }); + + it('should return the value when blocknote is null', () => { + const value = { blocknote: null, markdown: 'test' }; + const result = validateRichTextFieldOrThrow(value, 'testField'); + + expect(result).toEqual(value); + }); + + it('should return the value when only markdown is provided', () => { + const value = { markdown: '# Heading' }; + const result = validateRichTextFieldOrThrow(value, 'testField'); + + expect(result).toEqual(value); + }); }); describe('invalid inputs', () => { @@ -46,7 +61,29 @@ describe('validateRichTextFieldOrThrow', () => { CommonQueryRunnerException, ); expect(() => validateRichTextFieldOrThrow(value, 'testField')).toThrow( - /Should have only blocknote, markdown subfields/, + /Invalid subfield.*invalidField.*rich text field/, + ); + }); + + it('should throw when blocknote contains invalid JSON', () => { + const value = { blocknote: 'not-valid-json' }; + + expect(() => validateRichTextFieldOrThrow(value, 'testField')).toThrow( + CommonQueryRunnerException, + ); + expect(() => validateRichTextFieldOrThrow(value, 'testField')).toThrow( + /must contain valid JSON/, + ); + }); + + it('should throw when blocknote is valid JSON but not an array', () => { + const value = { blocknote: '{"type":"paragraph"}' }; + + expect(() => validateRichTextFieldOrThrow(value, 'testField')).toThrow( + CommonQueryRunnerException, + ); + expect(() => validateRichTextFieldOrThrow(value, 'testField')).toThrow( + /must be a JSON array of blocks/, ); }); }); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rich-text-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rich-text-field-or-throw.util.ts index fcbb404ef91..1ae75b5cccb 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rich-text-field-or-throw.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rich-text-field-or-throw.util.ts @@ -1,55 +1,79 @@ import { inspect } from 'util'; import { msg } from '@lingui/core/macro'; -import { isNull, isObject } from '@sniptt/guards'; -import { - compositeTypeDefinitions, - FieldMetadataType, -} from 'twenty-shared/types'; +import { isNonEmptyString, isNull } from '@sniptt/guards'; +import { validateRawJsonFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-raw-json-field-or-throw.util'; +import { validateTextFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-text-field-or-throw.util'; import { CommonQueryRunnerException, CommonQueryRunnerExceptionCode, } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; -export const validateRichTextFieldOrThrow = ( +const validateBlocknoteFieldOrThrow = ( value: unknown, fieldName: string, -): { - blocknote: string | null | undefined; - markdown: string | null | undefined; -} | null => { - if (isNull(value)) return null; +): string | null => { + const textValue = validateTextFieldOrThrow(value, fieldName); + + if (!isNonEmptyString(textValue)) return textValue; + + let parsed: unknown; try { - const parsedValue = JSON.parse(JSON.stringify(value)); - - if (!isObject(parsedValue)) throw new Error('Should be an object'); - - if (Object.keys(parsedValue).length === 0) return null; - - const subfields = Object.keys(parsedValue); - const richTextSubfields = compositeTypeDefinitions - .get(FieldMetadataType.RICH_TEXT) - ?.properties.filter( - (prop) => prop.hidden !== true && prop.hidden !== 'input', - ) - .map((prop) => prop.name); - - if (!subfields.every((subfield) => richTextSubfields?.includes(subfield))) - throw new Error( - `Should have only ${richTextSubfields?.join(', ')} subfields`, - ); - - return value as { - blocknote: string | null | undefined; - markdown: string | null | undefined; - }; - } catch (error) { + parsed = JSON.parse(textValue); + } catch { throw new CommonQueryRunnerException( - `Invalid rich text v2 value ${inspect(value)} for field "${fieldName}" - ${error.message}`, + `Invalid blocknote value for field "${fieldName}" - must contain valid JSON`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, { userFriendlyMessage: msg`Invalid value for rich text.` }, ); } + + if (!Array.isArray(parsed)) { + throw new CommonQueryRunnerException( + `Invalid blocknote value for field "${fieldName}" - must be a JSON array of blocks`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { userFriendlyMessage: msg`Invalid value for rich text.` }, + ); + } + + return textValue; +}; + +export const validateRichTextFieldOrThrow = ( + value: unknown, + fieldName: string, +): { + blocknote?: string | null; + markdown?: string | null; +} | null => { + const preValidatedValue = validateRawJsonFieldOrThrow(value, fieldName); + + if (isNull(preValidatedValue)) return null; + + for (const [subField, subFieldValue] of Object.entries(preValidatedValue)) { + switch (subField) { + case 'blocknote': + validateBlocknoteFieldOrThrow( + subFieldValue, + `${fieldName}.${subField}`, + ); + break; + case 'markdown': + validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`); + break; + default: + throw new CommonQueryRunnerException( + `Invalid subfield ${inspect(subField)} for rich text field "${fieldName}"`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { userFriendlyMessage: msg`Invalid value for rich text.` }, + ); + } + } + + return value as { + blocknote?: string | null; + markdown?: string | null; + }; }; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/rich-text-field-create-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/rich-text-field-create-input-validation.integration-spec.ts.snap index 472974cc78d..1369a17aaff 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/rich-text-field-create-input-validation.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/rich-text-field-create-input-validation.integration-spec.ts.snap @@ -2,4 +2,12 @@ exports[`Create input validation - RICH_TEXT Gql create input - failure RICH_TEXT - should fail with : {"richTextField":"not-a-rich-text"} 1`] = `"Expected type "RichTextCreateInput" to be an object."`; -exports[`Create input validation - RICH_TEXT Rest create input - failure RICH_TEXT - should fail with : {"richTextField":"not-a-rich-text"} 1`] = `"["Invalid rich text v2 value 'not-a-rich-text' for field \\"richTextField\\" - Should be an object"]"`; +exports[`Create input validation - RICH_TEXT Gql create input - failure RICH_TEXT - should fail with : {"richTextField":{"blocknote":"[{\\"id\\":\\"1\\",\\"type\\":\\"paragraph\\",\\"props\\":{},\\"content\\":[{\\"type\\":\\"text\\",\\"text\\":\\"test\\"},\\"children\\":[]}]"}} 1`] = `"Invalid blocknote value for field "richTextField.blocknote" - must contain valid JSON"`; + +exports[`Create input validation - RICH_TEXT Gql create input - failure RICH_TEXT - should fail with : {"richTextField":{"blocknote":"invalid-json"}} 1`] = `"Invalid blocknote value for field "richTextField.blocknote" - must contain valid JSON"`; + +exports[`Create input validation - RICH_TEXT Rest create input - failure RICH_TEXT - should fail with : {"richTextField":"not-a-rich-text"} 1`] = `"["Invalid object value 'not-a-rich-text' for field \\"richTextField\\""]"`; + +exports[`Create input validation - RICH_TEXT Rest create input - failure RICH_TEXT - should fail with : {"richTextField":{"blocknote":"[{\\"id\\":\\"1\\",\\"type\\":\\"paragraph\\",\\"props\\":{},\\"content\\":[{\\"type\\":\\"text\\",\\"text\\":\\"test\\"},\\"children\\":[]}]"}} 1`] = `"["Invalid blocknote value for field \\"richTextField.blocknote\\" - must contain valid JSON"]"`; + +exports[`Create input validation - RICH_TEXT Rest create input - failure RICH_TEXT - should fail with : {"richTextField":{"blocknote":"invalid-json"}} 1`] = `"["Invalid blocknote value for field \\"richTextField.blocknote\\" - must contain valid JSON"]"`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/constants/failing-create-input-by-field-metadata-type.constant.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/constants/failing-create-input-by-field-metadata-type.constant.ts index 9f63663a799..b6f81533f0c 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/constants/failing-create-input-by-field-metadata-type.constant.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/constants/failing-create-input-by-field-metadata-type.constant.ts @@ -230,6 +230,21 @@ export const failingCreateInputByFieldMetadataType: { richTextField: 'not-a-rich-text', }, }, + { + input: { + richTextField: { + blocknote: 'invalid-json', + }, + }, + }, + { + input: { + richTextField: { + blocknote: + '[{"id":"1","type":"paragraph","props":{},"content":[{"type":"text","text":"test"},"children":[]}]', + }, + }, + }, ], [FieldMetadataType.POSITION]: [ { diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/constants/successful-create-input-by-field-metadata-type.constant.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/constants/successful-create-input-by-field-metadata-type.constant.ts index 1c1235feb8e..efcdc440ef6 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/constants/successful-create-input-by-field-metadata-type.constant.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/constants/successful-create-input-by-field-metadata-type.constant.ts @@ -373,13 +373,15 @@ export const successfulCreateInputByFieldMetadataType: { { input: { richTextField: { - blocknote: 'test', + blocknote: + '[{"type":"paragraph","content":[{"type":"text","text":"test"}]}]', markdown: 'test', }, }, validateInput: (record: Record) => { return ( - record.richTextField.blocknote === 'test' && + record.richTextField.blocknote === + '[{"type":"paragraph","content":[{"type":"text","text":"test"}]}]' && record.richTextField.markdown === 'test' ); },