fix: validate blocknote JSON in rich text fields (#18902)
## 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)
This commit is contained in:
+9
-6
@@ -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],
|
||||
|
||||
+4
-2
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user