diff --git a/.changeset/poor-cooks-protect.md b/.changeset/poor-cooks-protect.md new file mode 100644 index 0000000000..b31fdda1bc --- /dev/null +++ b/.changeset/poor-cooks-protect.md @@ -0,0 +1,7 @@ +--- +"@comet/blocks-api": patch +--- + +Fix `RichTextBlock` draft content validation + +Extend validation to validate inline links in draft content. diff --git a/packages/api/blocks-api/src/blocks/createRichTextBlock.ts b/packages/api/blocks-api/src/blocks/createRichTextBlock.ts index d7cce998f1..6c5c5bcb75 100644 --- a/packages/api/blocks-api/src/blocks/createRichTextBlock.ts +++ b/packages/api/blocks-api/src/blocks/createRichTextBlock.ts @@ -1,5 +1,5 @@ import { instanceToPlain, plainToInstance } from "class-transformer"; -import { Allow, IsObject } from "class-validator"; +import { registerDecorator, validate, ValidationArguments, ValidationOptions } from "class-validator"; import type { DraftBlockType, DraftEntityMutability, DraftInlineStyleType, RawDraftContentState, RawDraftEntityRange } from "draft-js"; import { createAppliedMigrationsBlockDataFactoryDecorator } from "../migrations/createAppliedMigrationsBlockDataFactoryDecorator"; @@ -106,8 +106,7 @@ export function createRichTextBlock( } class RichTextBlockInput implements RichTextBlockInputInterface> { - @Allow() - @IsObject() + @IsDraftContent(LinkBlock) @BlockField({ type: "json" }) draftContent: DraftJsInput>; @@ -192,3 +191,51 @@ export function createRichTextBlock( return RichTextBlock; } + +function IsDraftContent(link: Block, validationOptions?: ValidationOptions) { + // eslint-disable-next-line @typescript-eslint/ban-types + return function (object: Object, propertyName: string) { + registerDecorator({ + name: "isDraftContent", + target: object.constructor, + propertyName, + constraints: [link], + options: validationOptions, + validator: { + async validate(value: unknown, args: ValidationArguments) { + const LinkBlock = args.constraints[0] as Block; + + if (isDraftJsInput(value)) { + for (const entity of Object.values(value.entityMap)) { + const validationErrors = await validate(LinkBlock.blockInputFactory(entity.data), { + forbidNonWhitelisted: true, + whitelist: true, + }); + + if (validationErrors.length > 0) { + return false; + } + } + + return true; + } + + return false; + }, + }, + }); + }; +} + +function isDraftJsInput(value: unknown): value is DraftJsInput { + return ( + typeof value === "object" && + value !== null && + "blocks" in value && + "entityMap" in value && + Array.isArray(value.blocks) && + typeof value.entityMap === "object" && + value.entityMap !== null && + Object.values(value.entityMap).every((entity) => typeof entity === "object" && entity !== null && entity.type === "LINK") + ); +}