UNPKG

@datocms/cma-client

Version:
676 lines (615 loc) 24 kB
import { type BlockInRequest, type RichTextFieldValue, type RichTextFieldValueInNestedResponse, type RichTextFieldValueInRequest, type SingleBlockFieldValue, type SingleBlockFieldValueInNestedResponse, type SingleBlockFieldValueInRequest, type StructuredTextFieldValue, type StructuredTextFieldValueInNestedResponse, type StructuredTextFieldValueInRequest, isItemWithOptionalIdAndMeta, } from '../fieldTypes'; import type * as ApiTypes from '../generated/ApiTypes'; import type { ExtractNestedBlocksFromFieldValue } from './itemDefinition'; import { nonRecursiveFilterBlocksInNonLocalizedFieldValueAsync, nonRecursiveFindAllBlocksInNonLocalizedFieldValueAsync, nonRecursiveMapBlocksInNonLocalizedFieldValueAsync, nonRecursiveReduceBlocksInNonLocalizedFieldValueAsync, nonRecursiveSomeBlocksInNonLocalizedFieldValueAsync, nonRecursiveVisitBlocksInNonLocalizedFieldValueAsync, } from './nonRecursiveBlocks'; import type { SchemaRepository } from './schemaRepository'; type RecognizableFieldValue = | RichTextFieldValueInNestedResponse | SingleBlockFieldValueInNestedResponse | StructuredTextFieldValueInNestedResponse | RichTextFieldValueInRequest | SingleBlockFieldValueInRequest | StructuredTextFieldValueInRequest | RichTextFieldValue | SingleBlockFieldValue | StructuredTextFieldValue; /** * Path through a non-localized field value (ie. ['content', 0, 'attributes', 'title']) */ export type TreePath = readonly (string | number)[]; /** * Traversal direction for recursive operations */ export type TraversalDirection = 'top-down' | 'bottom-up'; /** * Recursively visit every block in a non-localized field value and all nested blocks within those blocks. * This function traverses not only the direct blocks in the non-localized field value but also recursively * visits blocks contained within the attributes of each block, creating a complete traversal * of the entire block hierarchy. * * @param nonLocalizedFieldValue - The non-localized field value containing blocks to visit * @param fieldType - The type field (determines how the value is processed) * @param schemaRepository - Repository for accessing DatoCMS schema information (to resolve block structures) * @param visitor - Asynchronous function called for each block found, including nested blocks * @returns Promise that resolves when all blocks and nested blocks have been visited */ export async function visitBlocksInNonLocalizedFieldValue< T extends RecognizableFieldValue, >( nonLocalizedFieldValue: T, fieldType: ApiTypes.Field['field_type'], schemaRepository: SchemaRepository, visitor: ( item: ExtractNestedBlocksFromFieldValue<T>, path: TreePath, ) => void | Promise<void>, path?: TreePath, ): Promise<void>; export async function visitBlocksInNonLocalizedFieldValue( nonLocalizedFieldValue: unknown, fieldType: ApiTypes.Field['field_type'], schemaRepository: SchemaRepository, visitor: (item: BlockInRequest, path: TreePath) => void | Promise<void>, path?: TreePath, ): Promise<void>; export async function visitBlocksInNonLocalizedFieldValue( nonLocalizedFieldValue: unknown, fieldType: ApiTypes.Field['field_type'], schemaRepository: SchemaRepository, visitor: (item: BlockInRequest, path: TreePath) => void | Promise<void>, path: TreePath = [], ): Promise<void> { await nonRecursiveVisitBlocksInNonLocalizedFieldValueAsync( fieldType, nonLocalizedFieldValue, async (block, innerPath) => { await visitor(block, [...path, ...innerPath]); if (!isItemWithOptionalIdAndMeta(block)) { return; } const itemType = await schemaRepository.getRawItemTypeById( block.relationships.item_type.data.id, ); const fields = await schemaRepository.getRawItemTypeFields(itemType); for (const field of fields) { await visitBlocksInNonLocalizedFieldValue( block.attributes[field.attributes.api_key], field.attributes.field_type, schemaRepository, visitor, [...path, ...innerPath, 'attributes', field.attributes.api_key], ); } }, ); } /** * Recursively find all blocks that match the predicate function in a non-localized field value. * Searches through all direct blocks and recursively through nested blocks within * the attributes of each block, returning all matches found throughout the hierarchy. * * @param nonLocalizedFieldValue - The non-localized field value containing blocks to search * @param fieldType - The type field (determines how the value is processed) * @param schemaRepository - Repository for accessing DatoCMS schema information (to resolve block structures) * @param predicate - Asynchronous function that tests each block, including nested ones * @returns Promise that resolves to an array of objects, each containing a matching block and its full path */ export async function findAllBlocksInNonLocalizedFieldValue< T extends RecognizableFieldValue, >( nonLocalizedFieldValue: T, fieldType: ApiTypes.Field['field_type'], schemaRepository: SchemaRepository, predicate: ( item: ExtractNestedBlocksFromFieldValue<T>, path: TreePath, ) => boolean | Promise<boolean>, path?: TreePath, ): Promise< Array<{ item: ExtractNestedBlocksFromFieldValue<T>; path: TreePath }> >; export async function findAllBlocksInNonLocalizedFieldValue( nonLocalizedFieldValue: unknown, fieldType: ApiTypes.Field['field_type'], schemaRepository: SchemaRepository, predicate: ( item: BlockInRequest, path: TreePath, ) => boolean | Promise<boolean>, path?: TreePath, ): Promise<Array<{ item: BlockInRequest; path: TreePath }>>; export async function findAllBlocksInNonLocalizedFieldValue( nonLocalizedFieldValue: unknown, fieldType: ApiTypes.Field['field_type'], schemaRepository: SchemaRepository, predicate: ( item: BlockInRequest, path: TreePath, ) => boolean | Promise<boolean>, path: TreePath = [], ): Promise<Array<{ item: BlockInRequest; path: TreePath }>> { const results: Array<{ item: BlockInRequest; path: TreePath }> = []; const directMatches = await nonRecursiveFindAllBlocksInNonLocalizedFieldValueAsync( fieldType, nonLocalizedFieldValue, async (block, innerPath) => await predicate(block, [...path, ...innerPath]), ); results.push( ...directMatches.map(({ item, path: innerPath }) => ({ item, path: [...path, ...innerPath], })), ); await nonRecursiveVisitBlocksInNonLocalizedFieldValueAsync( fieldType, nonLocalizedFieldValue, async (block, innerPath) => { if (!isItemWithOptionalIdAndMeta(block)) { return; } const itemType = await schemaRepository.getRawItemTypeById( block.relationships.item_type.data.id, ); const fields = await schemaRepository.getRawItemTypeFields(itemType); for (const field of fields) { const nestedResults = await findAllBlocksInNonLocalizedFieldValue( block.attributes[field.attributes.api_key], field.attributes.field_type, schemaRepository, predicate, [...path, ...innerPath, 'attributes', field.attributes.api_key], ); results.push(...nestedResults); } }, ); return results; } /** * Recursively filter blocks in a non-localized field value, removing those that don't match the predicate. * Creates a new non-localized field value structure containing only blocks that pass the predicate test, * including recursive filtering of nested blocks within block attributes. The filtering * preserves the original non-localized field value structure and hierarchy. * * @param nonLocalizedFieldValue - The non-localized field value containing blocks to filter * @param fieldType - The type field (determines how the value is processed) * @param schemaRepository - Repository for accessing DatoCMS schema information (to resolve block structures) * @param predicate - Asynchronous function that tests each block, including nested ones * @param options - Optional configuration object * @param options.traversalDirection - Direction of traversal: 'top-down' (default) applies predicate before processing children, 'bottom-up' processes children first * @returns Promise that resolves to the new non-localized field value with recursively filtered blocks */ export async function filterBlocksInNonLocalizedFieldValue< T extends RecognizableFieldValue, >( nonLocalizedFieldValue: T, fieldType: ApiTypes.Field['field_type'], schemaRepository: SchemaRepository, predicate: ( item: ExtractNestedBlocksFromFieldValue<T>, path: TreePath, ) => boolean | Promise<boolean>, options?: { traversalDirection?: TraversalDirection }, path?: TreePath, ): Promise<T>; export async function filterBlocksInNonLocalizedFieldValue( nonLocalizedFieldValue: unknown, fieldType: ApiTypes.Field['field_type'], schemaRepository: SchemaRepository, predicate: ( item: BlockInRequest, path: TreePath, ) => boolean | Promise<boolean>, options?: { traversalDirection?: TraversalDirection }, path?: TreePath, ): Promise<unknown>; export async function filterBlocksInNonLocalizedFieldValue( nonLocalizedFieldValue: unknown, fieldType: ApiTypes.Field['field_type'], schemaRepository: SchemaRepository, predicate: ( item: BlockInRequest, path: TreePath, ) => boolean | Promise<boolean>, options: { traversalDirection?: TraversalDirection } = {}, path: TreePath = [], ): Promise<unknown> { const { traversalDirection = 'top-down' } = options; const mapperFunc = async (block: BlockInRequest, innerPath: TreePath) => { const blockPath = [...path, ...innerPath]; if (!isItemWithOptionalIdAndMeta(block)) { return block; } const itemType = await schemaRepository.getRawItemTypeById( block.relationships.item_type.data.id, ); const fields = await schemaRepository.getRawItemTypeFields(itemType); if (traversalDirection === 'top-down') { const blockCopy = { ...block, attributes: { ...block.attributes } }; for (const field of fields) { blockCopy.attributes[field.attributes.api_key] = await filterBlocksInNonLocalizedFieldValue( blockCopy.attributes[field.attributes.api_key], field.attributes.field_type, schemaRepository, predicate, options, [...blockPath, 'attributes', field.attributes.api_key], ); } return blockCopy; } const blockCopy = { ...block, attributes: { ...block.attributes } }; for (const field of fields) { blockCopy.attributes[field.attributes.api_key] = await filterBlocksInNonLocalizedFieldValue( blockCopy.attributes[field.attributes.api_key], field.attributes.field_type, schemaRepository, predicate, options, [...blockPath, 'attributes', field.attributes.api_key], ); } return blockCopy; }; const mappedValue = await nonRecursiveMapBlocksInNonLocalizedFieldValueAsync( fieldType, nonLocalizedFieldValue, mapperFunc, ); return nonRecursiveFilterBlocksInNonLocalizedFieldValueAsync( fieldType, mappedValue, async (block, innerPath) => { const blockPath = [...path, ...innerPath]; return await predicate(block, blockPath); }, ); } /** * Recursively reduce all blocks in a non-localized field value to a single value by applying a reducer function. * Processes each direct block and recursively processes nested blocks within block attributes, * accumulating results from the entire block hierarchy into a single value. * * @template R - The type of the accumulated result * @param nonLocalizedFieldValue - The non-localized field value containing blocks to reduce * @param fieldType - The type field (determines how the value is processed) * @param schemaRepository - Repository for accessing DatoCMS schema information (to resolve block structures) * @param reducer - Asynchronous function that processes each block and updates the accumulator * @param initialNonLocalizedFieldValue - The initial value for the accumulator * @returns Promise that resolves to the final accumulated value from all blocks in the hierarchy */ export async function reduceBlocksInNonLocalizedFieldValue<T, R>( nonLocalizedFieldValue: T, fieldType: ApiTypes.Field['field_type'], schemaRepository: SchemaRepository, reducer: ( accumulator: R, item: ExtractNestedBlocksFromFieldValue<T>, path: TreePath, ) => R | Promise<R>, initialValue: R, path?: TreePath, ): Promise<R>; export async function reduceBlocksInNonLocalizedFieldValue<R>( nonLocalizedFieldValue: unknown, fieldType: ApiTypes.Field['field_type'], schemaRepository: SchemaRepository, reducer: ( accumulator: R, item: BlockInRequest, path: TreePath, ) => R | Promise<R>, initialValue: R, path?: TreePath, ): Promise<R>; export async function reduceBlocksInNonLocalizedFieldValue<R>( nonLocalizedFieldValue: unknown, fieldType: ApiTypes.Field['field_type'], schemaRepository: SchemaRepository, reducer: ( accumulator: R, item: BlockInRequest, path: TreePath, ) => R | Promise<R>, initialValue: R, path: TreePath = [], ): Promise<R> { let accumulator = await nonRecursiveReduceBlocksInNonLocalizedFieldValueAsync( fieldType, nonLocalizedFieldValue, async (acc, block, innerPath) => await reducer(acc, block, [...path, ...innerPath]), initialValue, ); await nonRecursiveVisitBlocksInNonLocalizedFieldValueAsync( fieldType, nonLocalizedFieldValue, async (block, innerPath) => { if (!isItemWithOptionalIdAndMeta(block)) { return; } const itemType = await schemaRepository.getRawItemTypeById( block.relationships.item_type.data.id, ); const fields = await schemaRepository.getRawItemTypeFields(itemType); for (const field of fields) { accumulator = await reduceBlocksInNonLocalizedFieldValue( block.attributes[field.attributes.api_key], field.attributes.field_type, schemaRepository, reducer, accumulator, [...path, ...innerPath, 'attributes', field.attributes.api_key], ); } }, ); return accumulator; } /** * Recursively check if any block in the non-localized field value matches the predicate function. * Tests both direct blocks and recursively tests nested blocks within block attributes. * Returns true as soon as the first matching block is found anywhere in the hierarchy * (short-circuit evaluation). * * @param nonLocalizedFieldValue - The non-localized field value containing blocks to test * @param fieldType - The type field (determines how the value is processed) * @param schemaRepository - Repository for accessing DatoCMS schema information (to resolve block structures) * @param predicate - Asynchronous function that tests each block, including nested ones * @returns Promise that resolves to true if any block in the hierarchy matches, false otherwise */ export async function someBlocksInNonLocalizedFieldValue< T extends RecognizableFieldValue, >( nonLocalizedFieldValue: T, fieldType: ApiTypes.Field['field_type'], schemaRepository: SchemaRepository, predicate: ( item: ExtractNestedBlocksFromFieldValue<T>, path: TreePath, ) => boolean | Promise<boolean>, path?: TreePath, ): Promise<boolean>; export async function someBlocksInNonLocalizedFieldValue( nonLocalizedFieldValue: unknown, fieldType: ApiTypes.Field['field_type'], schemaRepository: SchemaRepository, predicate: ( item: BlockInRequest, path: TreePath, ) => boolean | Promise<boolean>, path?: TreePath, ): Promise<boolean>; export async function someBlocksInNonLocalizedFieldValue( nonLocalizedFieldValue: unknown, fieldType: ApiTypes.Field['field_type'], schemaRepository: SchemaRepository, predicate: ( item: BlockInRequest, path: TreePath, ) => boolean | Promise<boolean>, path: TreePath = [], ): Promise<boolean> { const directMatch = await nonRecursiveSomeBlocksInNonLocalizedFieldValueAsync( fieldType, nonLocalizedFieldValue, async (block, innerPath) => await predicate(block, [...path, ...innerPath]), ); if (directMatch) { return true; } let found = false; await nonRecursiveVisitBlocksInNonLocalizedFieldValueAsync( fieldType, nonLocalizedFieldValue, async (block, innerPath) => { if (found || !isItemWithOptionalIdAndMeta(block)) { return; } const itemType = await schemaRepository.getRawItemTypeById( block.relationships.item_type.data.id, ); const fields = await schemaRepository.getRawItemTypeFields(itemType); for (const field of fields) { if (found) break; const nestedMatch = await someBlocksInNonLocalizedFieldValue( block.attributes[field.attributes.api_key], field.attributes.field_type, schemaRepository, predicate, [...path, ...innerPath, 'attributes', field.attributes.api_key], ); if (nestedMatch) { found = true; } } }, ); return found; } /** * Recursively check if every block in the non-localized field value matches the predicate function. * Tests both direct blocks and recursively tests nested blocks within block attributes. * Returns false as soon as the first non-matching block is found anywhere in the hierarchy * (short-circuit evaluation). * * @param nonLocalizedFieldValue - The non-localized field value containing blocks to test * @param fieldType - The type field (determines how the value is processed) * @param schemaRepository - Repository for accessing DatoCMS schema information (to resolve block structures) * @param predicate - Asynchronous function that tests each block, including nested ones * @returns Promise that resolves to true if all blocks in the hierarchy match, false otherwise */ export async function everyBlockInNonLocalizedFieldValue< T extends RecognizableFieldValue, >( nonLocalizedFieldValue: T, fieldType: ApiTypes.Field['field_type'], schemaRepository: SchemaRepository, predicate: ( item: ExtractNestedBlocksFromFieldValue<T>, path: TreePath, ) => boolean | Promise<boolean>, path?: TreePath, ): Promise<boolean>; export async function everyBlockInNonLocalizedFieldValue( nonLocalizedFieldValue: unknown, fieldType: ApiTypes.Field['field_type'], schemaRepository: SchemaRepository, predicate: ( item: BlockInRequest, path: TreePath, ) => boolean | Promise<boolean>, path?: TreePath, ): Promise<boolean>; export async function everyBlockInNonLocalizedFieldValue( nonLocalizedFieldValue: unknown, fieldType: ApiTypes.Field['field_type'], schemaRepository: SchemaRepository, predicate: ( item: BlockInRequest, path: TreePath, ) => boolean | Promise<boolean>, path: TreePath = [], ): Promise<boolean> { return !(await someBlocksInNonLocalizedFieldValue( nonLocalizedFieldValue, fieldType, schemaRepository, async (item, path) => !(await predicate(item, path)), path, )); } // Converts fields // RichTextFieldValueInNestedResponse<Block1> // into their as-request variant // RichTextFieldValueInRequest<Block1> type FieldValueInRequest<T> = T extends RichTextFieldValueInNestedResponse< infer D > ? RichTextFieldValueInRequest<D> : T extends SingleBlockFieldValueInNestedResponse<infer D> ? SingleBlockFieldValueInRequest<D> : T extends StructuredTextFieldValueInNestedResponse<infer DB, infer DI> ? StructuredTextFieldValueInRequest<DB, DI> : T extends StructuredTextFieldValueInNestedResponse<infer DB> ? StructuredTextFieldValueInRequest<DB> : T; /** * Recursively transform blocks in a non-localized field value by applying a mapping function to each block. * Creates a new non-localized field value structure with transformed blocks while preserving the original * structure. Applies the mapping function to both direct blocks and recursively to nested * blocks within block attributes throughout the entire hierarchy. * * @param nonLocalizedFieldValue - The non-localized field value containing blocks to transform * @param fieldType - The type field (determines how the value is processed) * @param schemaRepository - Repository for accessing DatoCMS schema information (to resolve block structures) * @param mapper - Asynchronous function that transforms each block, including nested ones * @param options - Optional configuration object * @param options.traversalDirection - Direction of traversal: 'top-down' (default) applies mapper before processing children, 'bottom-up' processes children first * @returns Promise that resolves to the new non-localized field value with recursively transformed blocks */ export async function mapBlocksInNonLocalizedFieldValue< T extends RecognizableFieldValue, >( nonLocalizedFieldValue: T, fieldType: ApiTypes.Field['field_type'], schemaRepository: SchemaRepository, mapper: ( item: ExtractNestedBlocksFromFieldValue<T>, path: TreePath, ) => BlockInRequest | Promise<BlockInRequest>, options?: { traversalDirection?: TraversalDirection }, path?: TreePath, ): Promise<FieldValueInRequest<T>>; export async function mapBlocksInNonLocalizedFieldValue( nonLocalizedFieldValue: unknown, fieldType: ApiTypes.Field['field_type'], schemaRepository: SchemaRepository, mapper: ( item: BlockInRequest, path: TreePath, ) => BlockInRequest | Promise<BlockInRequest>, options?: { traversalDirection?: TraversalDirection }, path?: TreePath, ): Promise<unknown>; export async function mapBlocksInNonLocalizedFieldValue( nonLocalizedFieldValue: unknown, fieldType: ApiTypes.Field['field_type'], schemaRepository: SchemaRepository, mapper: ( item: BlockInRequest, path: TreePath, ) => BlockInRequest | Promise<BlockInRequest>, options: { traversalDirection?: TraversalDirection } = {}, path: TreePath = [], ): Promise<unknown> { const { traversalDirection = 'top-down' } = options; return nonRecursiveMapBlocksInNonLocalizedFieldValueAsync( fieldType, nonLocalizedFieldValue, async (block, innerPath) => { const blockPath = [...path, ...innerPath]; if (!isItemWithOptionalIdAndMeta(block)) { return await mapper(block, blockPath); } const itemType = await schemaRepository.getRawItemTypeById( block.relationships.item_type.data.id, ); const fields = await schemaRepository.getRawItemTypeFields(itemType); if (traversalDirection === 'top-down') { const newBlock = await mapper(block, blockPath); if (!isItemWithOptionalIdAndMeta(newBlock)) { return newBlock; } for (const field of fields) { newBlock.attributes[field.attributes.api_key] = await mapBlocksInNonLocalizedFieldValue( newBlock.attributes[field.attributes.api_key], field.attributes.field_type, schemaRepository, mapper, options, [...blockPath, 'attributes', field.attributes.api_key], ); } return newBlock; } const blockCopy = { ...block }; for (const field of fields) { blockCopy.attributes[field.attributes.api_key] = await mapBlocksInNonLocalizedFieldValue( blockCopy.attributes[field.attributes.api_key], field.attributes.field_type, schemaRepository, mapper, options, [...blockPath, 'attributes', field.attributes.api_key], ); } return await mapper(blockCopy, blockPath); }, ); }