Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions apps/cli/src/__tests__/lib/validate-type-spec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,42 @@ describe('validateValueAgainstTypeSpec – oneOf with mixed schemas', () => {
});
});

describe('validateValueAgainstTypeSpec – repeated actionable oneOf errors', () => {
const repeatedUnknownKeySchema: CliTypeSpec = {
oneOf: [
{
type: 'object',
properties: {
id: { type: 'string' },
op: { const: 'text.rewrite' },
},
required: ['id', 'op'],
},
{
type: 'object',
properties: {
id: { type: 'string' },
op: { const: 'text.insert' },
},
required: ['id', 'op'],
},
],
};

test('surfaces the shared nested schema error instead of the generic oneOf message', () => {
try {
validateValueAgainstTypeSpec({ id: 'r1', op: 'text.rewrite', '},{': ':' }, repeatedUnknownKeySchema, 'steps[0]');
throw new Error('Expected CliError to be thrown');
} catch (error) {
const cliError = error as CliError;
expect(cliError.message).toBe('steps[0].},{ is not allowed by schema.');
expect((cliError.details as { selectedError?: string }).selectedError).toBe(
'steps[0].},{ is not allowed by schema.',
);
}
});
});

describe('validateValueAgainstTypeSpec – enum branch', () => {
const enumSchema: CliTypeSpec = {
type: 'string',
Expand Down
42 changes: 39 additions & 3 deletions apps/cli/src/lib/operation-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,37 @@ function extractConstValues(variants: CliTypeSpec[]): string[] {
return values;
}

function isNestedValidationMessage(path: string, message: string): boolean {
return message.startsWith(`${path}.`) || message.startsWith(`${path}[`);
}

function selectRepeatedActionableOneOfError(path: string, errors: string[]): string | null {
const counts = new Map<string, number>();
for (const error of errors) {
counts.set(error, (counts.get(error) ?? 0) + 1);
}

let bestMessage: string | null = null;
let bestScore = 0;

for (const [message, count] of counts.entries()) {
if (count < 2) continue;

const nested = isNestedValidationMessage(path, message);
const isShapeError = message.includes(' is not allowed by schema.') || message.includes(' is required.');

if (!nested && !isShapeError) continue;

const score = count * 10 + (nested ? 2 : 0) + (isShapeError ? 1 : 0);
if (score > bestScore) {
bestScore = score;
bestMessage = message;
}
}

return bestMessage;
}

export function validateValueAgainstTypeSpec(value: unknown, schema: CliTypeSpec, path: string): void {
if ('const' in schema) {
if (value !== schema.const) {
Expand All @@ -136,11 +167,12 @@ export function validateValueAgainstTypeSpec(value: unknown, schema: CliTypeSpec
}

const allowedValues = extractConstValues(variants);
const selectedError = selectRepeatedActionableOneOfError(path, errors);
const message =
allowedValues.length > 0
? `${path} must be one of: ${allowedValues.join(', ')}.`
: `${path} must match one of the allowed schema variants.`;
throw new CliError('VALIDATION_ERROR', message, { errors });
: (selectedError ?? `${path} must match one of the allowed schema variants.`);
throw new CliError('VALIDATION_ERROR', message, { errors, selectedError });
}

if (schema.type === 'json') return;
Expand Down Expand Up @@ -236,7 +268,11 @@ function validateResponseValueAgainstTypeSpec(value: unknown, schema: CliTypeSpe
errors.push(error instanceof Error ? error.message : String(error));
}
}
throw new CliError('VALIDATION_ERROR', `${path} must match one of the allowed schema variants.`, { errors });
const selectedError = selectRepeatedActionableOneOfError(path, errors);
throw new CliError('VALIDATION_ERROR', selectedError ?? `${path} must match one of the allowed schema variants.`, {
errors,
selectedError,
});
}

if (schema.type === 'json') return;
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/document-api/reference/_generated-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -1016,5 +1016,5 @@
}
],
"marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}",
"sourceHash": "ea6e458d5c1476621d84f5bde9f85df9c3d620d0e226f206c57456f5ed6d20b3"
"sourceHash": "b61fad6a3a330af8a57b78ded260c8d8918486c9829b50804227fbeb15e8bf53"
}
24 changes: 20 additions & 4 deletions apps/docs/document-api/reference/blocks/list.mdx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
---
title: blocks.list
sidebarTitle: blocks.list
description: List top-level blocks in document order with IDs, types, and text previews. Supports pagination via offset/limit and optional nodeType filtering.
description: "List top-level blocks in document order with IDs, types, text previews, and optional full text when includeText:true. Supports pagination via offset/limit and optional nodeType filtering."
---

{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}

## Summary

List top-level blocks in document order with IDs, types, and text previews. Supports pagination via offset/limit and optional nodeType filtering.
List top-level blocks in document order with IDs, types, text previews, and optional full text when includeText:true. Supports pagination via offset/limit and optional nodeType filtering.

- Operation ID: `blocks.list`
- API member path: `editor.doc.blocks.list(...)`
Expand All @@ -20,12 +20,13 @@ List top-level blocks in document order with IDs, types, and text previews. Supp

## Expected result

Returns a BlocksListResult with total block count, an ordered array of block entries (ordinal, nodeId, nodeType, textPreview, isEmpty), and the current document revision.
Returns a BlocksListResult with total block count, an ordered array of block entries (ordinal, nodeId, nodeType, textPreview, optional text, isEmpty), and the current document revision.

## Input fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `includeText` | boolean | no | |
| `limit` | number | no | |
| `nodeTypes` | enum[] | no | |
| `offset` | number | no | |
Expand Down Expand Up @@ -53,10 +54,10 @@ Returns a BlocksListResult with total block count, an ordered array of block ent
{
"blocks": [
{
"isEmpty": true,
"nodeId": "node-def456",
"nodeType": "paragraph",
"ordinal": 1,
"text": "Hello, world.",
"textPreview": "example"
}
],
Expand All @@ -80,6 +81,10 @@ Returns a BlocksListResult with total block count, an ordered array of block ent
{
"additionalProperties": false,
"properties": {
"includeText": {
"description": "When true, includes the full flattened block text in each block entry.",
"type": "boolean"
},
"limit": {
"description": "Maximum blocks to return. Omit for all blocks.",
"minimum": 1,
Expand Down Expand Up @@ -184,6 +189,17 @@ Returns a BlocksListResult with total block count, an ordered array of block ent
}
]
},
"text": {
"description": "Full flattened block text when requested with includeText.",
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"textPreview": {
"oneOf": [
{
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/document-api/reference/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ The tables below are grouped by namespace.

| Operation | API member path | Description |
| --- | --- | --- |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><a href="/document-api/reference/blocks/list"><code>blocks.list</code></a></span> | <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.blocks.list(...)</code></span> | List top-level blocks in document order with IDs, types, and text previews. Supports pagination via offset/limit and optional nodeType filtering. |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><a href="/document-api/reference/blocks/list"><code>blocks.list</code></a></span> | <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.blocks.list(...)</code></span> | List top-level blocks in document order with IDs, types, text previews, and optional full text when includeText:true. Supports pagination via offset/limit and optional nodeType filtering. |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><a href="/document-api/reference/blocks/delete"><code>blocks.delete</code></a></span> | <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.blocks.delete(...)</code></span> | Delete an entire block node (paragraph, heading, list item, table, image, or sdt) deterministically. |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><a href="/document-api/reference/blocks/delete-range"><code>blocks.deleteRange</code></a></span> | <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.blocks.deleteRange(...)</code></span> | Delete a contiguous range of top-level blocks between two endpoints (inclusive). Both endpoints must be direct children of the document node. Supports dry-run preview. |

Expand Down
Loading
Loading