@@ -20,6 +20,7 @@ import {
2020 Block ,
2121 BlockId ,
2222 Blockquote ,
23+ CdaStructuredTextRecord ,
2324 CdaStructuredTextValue ,
2425 Code ,
2526 Document ,
@@ -34,7 +35,6 @@ import {
3435 Node ,
3536 NodeType ,
3637 Paragraph ,
37- Record as DatoCmsRecord ,
3838 Root ,
3939 Span ,
4040 StructuredText ,
@@ -120,6 +120,64 @@ export function isBlock<BlockItemType = BlockId, InlineBlockItemType = BlockId>(
120120 return node . type === blockNodeType ;
121121}
122122
123+ /**
124+ * Narrows a union of block item shapes to the member whose model
125+ * (`item_type`) matches `Id`.
126+ *
127+ * Any object carrying `relationships.item_type.data.id` is narrowable —
128+ * that covers items from `nested: true` responses as well as the object
129+ * variants of request payloads (updated / newly-created blocks). The bare
130+ * string IDs that may appear inside request payloads (to reference
131+ * existing, unchanged blocks) are filtered out of the result.
132+ */
133+ export type NarrowBlockItemByItemType < T , Id extends string > = Extract <
134+ T ,
135+ { relationships : { item_type : { data : { type : 'item_type' ; id : Id } } } }
136+ > ;
137+
138+ /**
139+ * Builds a type guard that narrows a `block` node to the variant whose
140+ * `item` belongs to a specific model.
141+ *
142+ * Call it with the block's `itemTypeId` literal — the ID generic is
143+ * inferred from the argument, so no explicit type parameter is needed.
144+ * Usable directly with `findFirstNode` / `findAllNodes` / `Array#filter`
145+ * over block-bearing DAST trees.
146+ *
147+ * For the literal `Id` to be preserved (and narrowing to work), the
148+ * argument must be typed as a literal — use `as const` on pre-set ID
149+ * constants.
150+ *
151+ * @example
152+ * ```ts
153+ * const needle = findFirstNode(
154+ * body.document,
155+ * isBlockWithItemOfType(WARNING_BLOCK_TYPE_ID),
156+ * );
157+ * if (needle) {
158+ * needle.node.item; // narrowed to the Warning item shape
159+ * }
160+ * ```
161+ */
162+ function itemHasItemTypeId ( item : unknown , itemTypeId : string ) : boolean {
163+ if ( typeof item !== 'object' || item === null ) return false ;
164+ const relationships = ( item as { relationships ?: unknown } ) . relationships ;
165+ if ( typeof relationships !== 'object' || relationships === null ) return false ;
166+ const itemType = ( relationships as { item_type ?: unknown } ) . item_type ;
167+ if ( typeof itemType !== 'object' || itemType === null ) return false ;
168+ const data = ( itemType as { data ?: unknown } ) . data ;
169+ if ( typeof data !== 'object' || data === null ) return false ;
170+ return ( data as { id ?: unknown } ) . id === itemTypeId ;
171+ }
172+
173+ export function isBlockWithItemOfType < Id extends string > ( itemTypeId : Id ) {
174+ return < BlockItemType = BlockId , InlineBlockItemType = BlockId > (
175+ node : Node < BlockItemType , InlineBlockItemType > ,
176+ ) : node is Block < NarrowBlockItemByItemType < BlockItemType , Id > > =>
177+ node . type === blockNodeType &&
178+ itemHasItemTypeId ( ( node as Block < BlockItemType > ) . item , itemTypeId ) ;
179+ }
180+
123181export function isInlineBlock <
124182 BlockItemType = BlockId ,
125183 InlineBlockItemType = BlockId
@@ -129,6 +187,40 @@ export function isInlineBlock<
129187 return node . type === inlineBlockNodeType ;
130188}
131189
190+ /**
191+ * Builds a type guard that narrows an `inlineBlock` node to the variant
192+ * whose `item` belongs to a specific model.
193+ *
194+ * Mirrors {@link isBlockWithItemOfType} for inline blocks. Call it with
195+ * the block's `itemTypeId` literal — the ID generic is inferred from the
196+ * argument, so no explicit type parameter is needed.
197+ *
198+ * For the literal `Id` to be preserved (and narrowing to work), the
199+ * argument must be typed as a literal — use `as const` on pre-set ID
200+ * constants.
201+ *
202+ * @example
203+ * ```ts
204+ * const needle = findFirstNode(
205+ * body.document,
206+ * isInlineBlockWithItemOfType(CALLOUT_BLOCK_TYPE_ID),
207+ * );
208+ * if (needle) {
209+ * needle.node.item; // narrowed to the Callout item shape
210+ * }
211+ * ```
212+ */
213+ export function isInlineBlockWithItemOfType < Id extends string > ( itemTypeId : Id ) {
214+ return < BlockItemType = BlockId , InlineBlockItemType = BlockId > (
215+ node : Node < BlockItemType , InlineBlockItemType > ,
216+ ) : node is InlineBlock < NarrowBlockItemByItemType < InlineBlockItemType , Id > > =>
217+ node . type === inlineBlockNodeType &&
218+ itemHasItemTypeId (
219+ ( node as InlineBlock < InlineBlockItemType > ) . item ,
220+ itemTypeId ,
221+ ) ;
222+ }
223+
132224export function isCode < BlockItemType = BlockId , InlineBlockItemType = BlockId > (
133225 node : Node < BlockItemType , InlineBlockItemType > ,
134226) : node is Code {
@@ -182,9 +274,9 @@ export function isNode<BlockItemType = BlockId, InlineBlockItemType = BlockId>(
182274}
183275
184276export function isCdaStructuredTextValue <
185- BlockRecord extends DatoCmsRecord ,
186- LinkRecord extends DatoCmsRecord ,
187- InlineBlockRecord extends DatoCmsRecord
277+ BlockRecord extends CdaStructuredTextRecord ,
278+ LinkRecord extends CdaStructuredTextRecord ,
279+ InlineBlockRecord extends CdaStructuredTextRecord
188280> (
189281 obj : unknown ,
190282) : obj is CdaStructuredTextValue < BlockRecord , LinkRecord , InlineBlockRecord > {
@@ -195,9 +287,9 @@ export function isCdaStructuredTextValue<
195287 * @deprecated Use isCdaStructuredTextValue instead
196288 */
197289export function isStructuredText <
198- BlockRecord extends DatoCmsRecord ,
199- LinkRecord extends DatoCmsRecord ,
200- InlineBlockRecord extends DatoCmsRecord
290+ BlockRecord extends CdaStructuredTextRecord ,
291+ LinkRecord extends CdaStructuredTextRecord ,
292+ InlineBlockRecord extends CdaStructuredTextRecord
201293> (
202294 obj : unknown ,
203295) : obj is StructuredText < BlockRecord , LinkRecord , InlineBlockRecord > {
@@ -222,7 +314,7 @@ export function isEmptyDocument(obj: unknown): boolean {
222314 }
223315
224316 const document =
225- isStructuredText ( obj ) && isDocument ( obj . value )
317+ isCdaStructuredTextValue ( obj ) && isDocument ( obj . value )
226318 ? obj . value
227319 : isDocument ( obj )
228320 ? obj
0 commit comments