@datocms/cma-client
Version:
JS client for DatoCMS REST Content Management API
290 lines (260 loc) • 10.2 kB
text/typescript
import {
type Block,
type Document,
type InlineBlock,
type Node,
everyNode,
isBlock,
isDocument,
isInlineBlock,
} from 'datocms-structured-text-utils';
import { isValidId } from '../utilities/id';
import type { ItemTypeDefinition } from '../utilities/itemDefinition';
import {
type LocalizedFieldValue,
isLocalizedFieldValue,
} from '../utilities/normalizedFieldValues';
import type { StructuredTextEditorConfiguration } from './appearance/structured_text';
import {
type BlockInNestedResponse,
type BlockInRequest,
isItemId,
isItemWithOptionalIdAndMeta,
isItemWithOptionalMeta,
} from './single_block';
import type { RequiredValidator } from './validators';
import type { LengthValidator } from './validators/length';
import type { StructuredTextBlocksValidator } from './validators/structured_text_blocks';
import type { StructuredTextInlineBlocksValidator } from './validators/structured_text_inline_blocks';
import type { StructuredTextLinksValidator } from './validators/structured_text_links';
/**
* STRUCTURED TEXT TYPE SYSTEM FOR DATOCMS
*
* This module defines a comprehensive type system for handling DatoCMS structured text fields,
* which are rich text documents that can contain embedded blocks and inline elements.
*
* The challenge we're solving:
* - DatoCMS structured text can contain "blocks" (embedded content items) in bloth 'block'
* and 'inlineBlock' nodes
* - By default, CMA responses contain blocks as string IDs (lightweight references)
* - With ?nested=true parameter though, API responses contain blocks as full item objects
* (which in turn can contain other blocks)
* - For CMA requests, blocks can be represented as:
* 1. String IDs (referencing existing items)
* 2. Full item objects with IDs (for updates)
* 3. Item objects without IDs (for creation)
*
* This creates a need for different type variants for the same conceptual data structure.
*/
/**
* =============================================================================
* REQUEST VARIANTS - Types for sending data TO the DatoCMS API
* =============================================================================
*
* When making API requests, we need flexibility in how we represent embedded blocks:
* - Use string IDs to reference existing blocks that do not need to change
* - Include full block objects for updates
* - Omit IDs for new blocks being created
*/
/**
* Variant of 'block' structured text node for API requests
*/
export type BlockNodeInRequest<
D extends ItemTypeDefinition = ItemTypeDefinition,
> = Block<BlockInRequest<D>>;
/**
* Variant of 'inlineBlock' structured text node for API requests
*/
export type InlineBlockNodeInRequest<
D extends ItemTypeDefinition = ItemTypeDefinition,
> = InlineBlock<BlockInRequest<D>>;
/**
* Variant of Structured Text document for API requests
*/
export type DocumentInRequest<
BlockItemTypeDefinition extends ItemTypeDefinition = ItemTypeDefinition,
InlineBlockItemTypeDefinition extends ItemTypeDefinition = ItemTypeDefinition,
> = Document<
BlockInRequest<BlockItemTypeDefinition>,
BlockInRequest<InlineBlockItemTypeDefinition>
>;
/**
* =============================================================================
* NESTED VARIANTS - Types for API responses with ?nested=true parameter
* =============================================================================
*
* When using the GET /items?nested=true, the CMA returns Structured Text documents
* with embedded blocks fully populated as complete RawApiTypes.Item objects instead
* of just string IDs.
*/
/**
* Variant of 'block' structured text node for ?nested=true API responses
*/
export type BlockNodeInNestedResponse<
D extends ItemTypeDefinition = ItemTypeDefinition,
> = Block<BlockInNestedResponse<D>>;
/**
* Variant of 'inlineBlock' structured text node for ?nested=true API responses
*/
export type InlineBlockNodeInNestedResponse<
D extends ItemTypeDefinition = ItemTypeDefinition,
> = InlineBlock<BlockInNestedResponse<D>>;
/**
* Variant of Structured Text document for ?nested=true API responses
*/
export type DocumentInNestedResponse<
BlockItemTypeDefinition extends ItemTypeDefinition = ItemTypeDefinition,
InlineBlockItemTypeDefinition extends ItemTypeDefinition = ItemTypeDefinition,
> = Document<
BlockInNestedResponse<BlockItemTypeDefinition>,
BlockInNestedResponse<InlineBlockItemTypeDefinition>
>;
/**
* =============================================================================
* MAIN APPLICATION TYPES
* =============================================================================
*/
/**
* The main type for structured text field values in our application.
* Can be null (empty field) or a document with blocks as string IDs
*/
export type StructuredTextFieldValue = Document | null;
export type StructuredTextFieldValueInRequest<
BlockItemTypeDefinition extends ItemTypeDefinition = ItemTypeDefinition,
InlineBlockItemTypeDefinition extends ItemTypeDefinition = ItemTypeDefinition,
> = DocumentInRequest<
BlockItemTypeDefinition,
InlineBlockItemTypeDefinition
> | null;
export type StructuredTextFieldValueInNestedResponse<
BlockItemTypeDefinition extends ItemTypeDefinition = ItemTypeDefinition,
InlineBlockItemTypeDefinition extends ItemTypeDefinition = ItemTypeDefinition,
> = DocumentInNestedResponse<
BlockItemTypeDefinition,
InlineBlockItemTypeDefinition
> | null;
/**
* Utility function to validate all block/inlineBlock nodes in a structured text document tree.
* Calls the provided callback for each block/inlineBlock node found and returns true only if all pass.
*/
function validateAllBlockNodes<B, I>(
node: Node<B, I>,
callback: (node: Block<B> | InlineBlock<I>) => boolean,
): boolean {
return everyNode(node, (currentNode) => {
// If this is a block or inlineBlock node, validate it with the callback
if (isBlock(currentNode) || isInlineBlock(currentNode)) {
return callback(currentNode);
}
// For all other node types, they're valid by default
return true;
});
}
/**
* Type guard for basic structured text field values (blocks as string IDs only).
* Checks for the expected structure and ensures all block/inlineBlock nodes have string IDs.
*/
export function isStructuredTextFieldValue(
value: unknown,
): value is StructuredTextFieldValue {
if (value === null) return true;
if (!isDocument<unknown, unknown>(value)) {
return false;
}
// Check that all block/inlineBlock nodes have string item IDs
return validateAllBlockNodes(value.document, (node) => {
return typeof node.item === 'string' && isValidId(node.item);
});
}
export function isLocalizedStructuredTextFieldValue(
value: unknown,
): value is LocalizedFieldValue<StructuredTextFieldValue> {
return (
isLocalizedFieldValue(value) &&
Object.values(value).every(isStructuredTextFieldValue)
);
}
/**
* Type guard for structured text field values in API request format.
* Allows blocks as string IDs, full objects with IDs, or objects without IDs.
*/
export function isStructuredTextFieldValueInRequest<
BlockItemTypeDefinition extends ItemTypeDefinition = ItemTypeDefinition,
InlineBlockItemTypeDefinition extends ItemTypeDefinition = ItemTypeDefinition,
>(
value: unknown,
): value is StructuredTextFieldValueInRequest<
BlockItemTypeDefinition,
InlineBlockItemTypeDefinition
> {
if (value === null) return true;
if (!isDocument<unknown>(value)) {
return false;
}
// Check that all block/inlineBlock nodes have valid request format items
return validateAllBlockNodes(value.document, (node) => {
const item = node.item;
// String ID
if (isItemId(item)) return true;
// Object (either with or without ID)
return isItemWithOptionalIdAndMeta(item);
});
}
export function isLocalizedStructuredTextFieldValueInRequest<
D extends ItemTypeDefinition = ItemTypeDefinition,
>(
value: unknown,
): value is LocalizedFieldValue<StructuredTextFieldValueInRequest<D>> {
return (
isLocalizedFieldValue(value) &&
Object.values(value).every(isStructuredTextFieldValueInRequest)
);
}
/**
* Type guard for structured text field values with nested blocks (?nested=true format).
* Ensures all block/inlineBlock nodes have full RawApiTypes.Item objects.
*/
export function isStructuredTextFieldValueInNestedResponse<
D extends ItemTypeDefinition = ItemTypeDefinition,
>(value: unknown): value is StructuredTextFieldValueInNestedResponse<D> {
if (value === null) return true;
if (!isDocument<unknown>(value)) {
return false;
}
// Check that all block/inlineBlock nodes have full item objects
return validateAllBlockNodes(value.document, (node) => {
const item = node.item;
// Must be a full object with ID (nested format always includes full items)
return isItemWithOptionalMeta(item);
});
}
export function isLocalizedStructuredTextFieldValueInNestedResponse<
D extends ItemTypeDefinition = ItemTypeDefinition,
>(
value: unknown,
): value is LocalizedFieldValue<StructuredTextFieldValueInNestedResponse<D>> {
return (
isLocalizedFieldValue(value) &&
Object.values(value).every(isStructuredTextFieldValueInNestedResponse)
);
}
export type StructuredTextFieldValidators = {
/** Value must be specified or it won't be valid */
required?: RequiredValidator;
/** Only accept references to block records of the specified block models */
structured_text_blocks: StructuredTextBlocksValidator;
/** Only accept itemLink to inlineItem nodes for records of the specified models */
structured_text_links: StructuredTextLinksValidator;
/** Accept strings only with a specified number of characters */
length?: LengthValidator;
/** Only accept references to block records of the specified block models for inline blocks */
structured_text_inline_blocks?: StructuredTextInlineBlocksValidator;
};
export type StructuredTextFieldAppearance =
| { editor: 'structured_text'; parameters: StructuredTextEditorConfiguration }
| {
/** Plugin ID */
editor: string;
/** Plugin configuration */
parameters: Record<string, unknown>;
};