Skip to content

Commit f573f80

Browse files
committed
Narrow request-shape blocks via id + relax mapNodes mapper return
NarrowBlockItemByItemType now requires `id: string` and accepts an optional `relationships?` constraint. That picks both the response form (ItemInNestedResponse, id always present) and the updated request form (UpdatedBlockInRequest, relationships optional), while excluding bare string ids and freshly-created New blocks — finding those via item-type isn't the typical use case anyway. mapNodes / mapNodesAsync gain a free `R` parameter on the mapper so the return type isn't constrained to `MapNodesMapperResult<T>`. Callers can return nodes whose `item` payload is shaped differently from the input tree (e.g. swapping a nested-response block for a request-shape block built via buildBlockRecord) without fighting the inference.
1 parent 798d5c9 commit f573f80

4 files changed

Lines changed: 84 additions & 263 deletions

File tree

packages/utils/README.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,8 @@ await forEachNodeAsync(structuredText, async (node, parent, path) => {
237237

238238
| Function | Description |
239239
| --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
240-
| [`mapNodes`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L297) | Transform nodes in the tree synchronously (1:1, splat, or remove) |
241-
| [`mapNodesAsync`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L340) | Transform nodes in the tree asynchronously (1:1, splat, or remove) |
240+
| [`mapNodes`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L309) | Transform nodes in the tree synchronously (1:1, splat, or remove) |
241+
| [`mapNodesAsync`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L355) | Transform nodes in the tree asynchronously (1:1, splat, or remove) |
242242

243243
`mapNodes` walks the tree **bottom-up**: when the mapper sees a node, its
244244
descendants have already been transformed, and the mapper's return for that
@@ -306,10 +306,10 @@ const processed = await mapNodesAsync(structuredText, async (node) => {
306306
307307
| Function | Description |
308308
| -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
309-
| [`collectNodes`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L387) | Collect all nodes that match a predicate function |
310-
| [`collectNodesAsync`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L445) | Collect all nodes that match an async predicate function |
311-
| [`findFirstNode`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L484) | Find the first node that matches a predicate function |
312-
| [`findFirstNodeAsync`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L562) | Find the first node that matches an async predicate function |
309+
| [`collectNodes`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L402) | Collect all nodes that match a predicate function |
310+
| [`collectNodesAsync`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L460) | Collect all nodes that match an async predicate function |
311+
| [`findFirstNode`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L499) | Find the first node that matches a predicate function |
312+
| [`findFirstNodeAsync`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L577) | Find the first node that matches an async predicate function |
313313
314314
Find specific nodes using predicates or type guards:
315315
@@ -344,8 +344,8 @@ const strongText = collectNodes(
344344
345345
| Function | Description |
346346
| ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------- |
347-
| [`filterNodes`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L611) | Remove nodes that don't match a predicate synchronously |
348-
| [`filterNodesAsync`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L694) | Remove nodes that don't match an async predicate |
347+
| [`filterNodes`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L626) | Remove nodes that don't match a predicate synchronously |
348+
| [`filterNodesAsync`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L709) | Remove nodes that don't match an async predicate |
349349
350350
Remove nodes that don't match a predicate:
351351
@@ -373,8 +373,8 @@ const validated = await filterNodesAsync(structuredText, async (node) => {
373373
374374
| Function | Description |
375375
| ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- |
376-
| [`reduceNodes`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L781) | Reduce the tree to a single value using a synchronous reducer function |
377-
| [`reduceNodesAsync`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L826) | Reduce the tree to a single value using an async reducer function |
376+
| [`reduceNodes`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L796) | Reduce the tree to a single value using a synchronous reducer function |
377+
| [`reduceNodesAsync`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L841) | Reduce the tree to a single value using an async reducer function |
378378
379379
Reduce the entire tree to a single value:
380380
@@ -408,10 +408,10 @@ const nodeCounts = reduceNodes(
408408
409409
| Function | Description |
410410
| ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
411-
| [`someNode`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L868) | Check if any node in the tree matches a predicate (short-circuit evaluation) |
412-
| [`someNodeAsync`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L910) | Check if any node in the tree matches an async predicate (short-circuit evaluation) |
413-
| [`everyNode`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L952) | Check if every node in the tree matches a predicate (short-circuit evaluation) |
414-
| [`everyNodeAsync`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L983) | Check if every node in the tree matches an async predicate (short-circuit evaluation) |
411+
| [`someNode`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L883) | Check if any node in the tree matches a predicate (short-circuit evaluation) |
412+
| [`someNodeAsync`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L925) | Check if any node in the tree matches an async predicate (short-circuit evaluation) |
413+
| [`everyNode`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L967) | Check if every node in the tree matches a predicate (short-circuit evaluation) |
414+
| [`everyNodeAsync`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L998) | Check if every node in the tree matches an async predicate (short-circuit evaluation) |
415415
416416
Test if any or all nodes match a condition:
417417

packages/utils/__tests__/guards.test.ts

Lines changed: 0 additions & 193 deletions
This file was deleted.

packages/utils/src/guards.ts

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -122,29 +122,28 @@ export function isBlock<BlockItemType = BlockId, InlineBlockItemType = BlockId>(
122122

123123
/**
124124
* Narrows a union of block item shapes to the member whose model
125-
* (`item_type`) matches `Id`. Bare string IDs (the unchanged-block form
126-
* inside request payloads) are filtered out — they have no shape to narrow.
125+
* (`item_type`) matches `Id`.
127126
*
128-
* Discriminates on the `__itemTypeId` phantom field carried by every
129-
* object-shaped block variant exposed by `@datocms/cma-client-node`
130-
* (nested response items and both request-side variants — updated and
131-
* newly created). It deliberately does NOT key off
132-
* `relationships.item_type.data.id`: on the request-side "updated" shape
133-
* `relationships` is declared optional, which would cause an
134-
* `Extract`-based narrow to silently drop that variant.
127+
* Matches existing (persisted) blocks — items that carry an `id` and
128+
* whose `relationships.item_type.data.id` matches. That covers both the
129+
* `nested: true` response form and the "updated block" variant of
130+
* request payloads. Filtered out:
131+
* - bare string IDs (no `id` property to read)
132+
* - newly-created blocks in request payloads (they have no `id` yet —
133+
* the caller already knows the type, so finding them by model is
134+
* not the typical use case)
135135
*
136-
* For the per-D narrowing to fully resolve when the input was
137-
* parameterized over a *union* of item-type definitions, the upstream
138-
* `BlockInRequest<D>` / `BlockInNestedResponse<D>` types must distribute
139-
* over `D` (i.e. be defined as `D extends unknown ? ... : never`).
136+
* `relationships` is constrained as optional so the updated-block
137+
* request shape (where `relationships?` may be omitted when patching an
138+
* existing block) is still picked up.
140139
*/
141-
export type NarrowBlockItemByItemType<T, Id extends string> = T extends string
142-
? never
143-
: T extends { __itemTypeId?: infer ItemTypeIds }
144-
? Id extends ItemTypeIds & string
145-
? T
146-
: never
147-
: never;
140+
export type NarrowBlockItemByItemType<T, Id extends string> = Extract<
141+
T,
142+
{
143+
id: string;
144+
relationships?: { item_type: { data: { type: 'item_type'; id: Id } } };
145+
}
146+
>;
148147

149148
function itemHasItemTypeId(item: unknown, itemTypeId: string): boolean {
150149
if (typeof item !== 'object' || item === null) return false;

0 commit comments

Comments
 (0)