feat: add DataAPI — rich read-only data access for plugins#123
Conversation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ctory Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis pull request introduces a new DataAPI layer providing structured query methods for accessing notes, notebooks, tags, and graph data. The DataAPI is integrated into the plugin system via PluginRegistry and PluginHost, wired through App.tsx, and exposed to plugins through PluginContext, enabling data querying alongside existing APIs. Changes
Sequence Diagram(s)sequenceDiagram
participant App as App.tsx
participant Registry as PluginRegistry
participant Context as PluginContext
participant Plugin as Plugin
participant Bridge as DataAPIBridge
participant Server as Server
App->>App: createDataAPI(bridge)
App->>Registry: activate(id, ..., dataAPI)
Registry->>Registry: trackedData = wrap(dataAPI)
Registry->>Context: data: trackedData
Context-->>Plugin: context.data
Plugin->>Plugin: user calls context.data.getNotes()
Plugin->>Bridge: delegated call
Bridge->>Server: IPC call (window.readied)
Server-->>Bridge: results
Bridge-->>Plugin: transformed results
Plugin->>Plugin: handle data + events
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/plugin-api/tests/registry.test.ts (1)
103-127: 🧹 Nitpick | 🔵 TrivialAssert
context.datain the activation contract test.This PR’s main surface addition is
PluginContext.data, but the context-shape test still never checks for it. A regression that drops or misroutes the DataAPI would still pass here.Suggested assertion
expect(ctx).toHaveProperty('config'); expect(ctx).toHaveProperty('log'); expect(ctx).toHaveProperty('app'); + expect(ctx).toHaveProperty('data', mockDataAPI);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugin-api/tests/registry.test.ts` around lines 103 - 127, The test "it('passes context with all expected APIs')" never asserts PluginContext.data; update the test after receiving receivedContext (cast to ctx) to assert ctx has a data property and that it is the DataAPI passed into registry.activate (use the existing mockDataAPI), e.g. add assertions like expect(ctx).toHaveProperty('data') and expect(ctx.data).toBe(mockDataAPI) so the activation contract verifies the DataAPI is routed through registry.activate/activate.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/desktop/src/renderer/App.tsx`:
- Around line 238-249: getNotebookTree currently hardcodes noteCount to 0 in
mapNode; instead read the real count exposed by the bridge or omit the field.
Update the TreeNode type and the mapNode mapping inside getNotebookTree to use
the bridge-provided value (e.g., node.notebook.noteCount) for noteCount and keep
childCount computed from node.children.length (or if the bridge does not provide
noteCount, remove noteCount from the returned shape). Ensure you reference
getNotebookTree, the local TreeNode type, mapNode and
window.readied.notebooks.tree() when making the change.
- Around line 223-233: searchNotes currently ignores options.notebookId and
countNotes ignores options entirely; update searchNotes (the async
searchNotes(query, options) method) to pass through options.notebookId (and
preserve options?.limit ?? 20) to window.readied.notes.search so the backend is
scoped correctly, and change countNotes to accept an options parameter and
forward it to window.readied.notes.count (e.g.,
window.readied.notes.count(options)) so counts match the same query scope; keep
existing return shapes unchanged.
- Around line 196-216: getNotes is currently applying limit/offset when calling
window.readied.notes.list and then filtering the page, which yields incorrect
paging and total; change getNotes to fetch the unpaged list (call
window.readied.notes.list without limit/offset or with a flag to fetch all),
apply notebookId/status/isPinned filters to produce the full filtered array,
compute total = filtered.length, then apply limit/offset slicing to the filtered
array before mapping to the returned notes shape—refer to getNotes and
window.readied.notes.list to locate the call to adjust.
- Around line 197-203: The code replaces sortBy: 'wordCount' with 'updatedAt'
before calling window.readied.notes.list, changing requested semantics; instead
keep the original options when calling window.readied.notes.list and, if
options.sortBy === 'wordCount', post-process the returned notes array (the notes
variable) to sort by each note's wordCount field according to options.sortOrder
before using it, or alternatively return an error/throw when the bridge doesn't
support wordCount sorting; update the block around window.readied.notes.list and
the variable notes to implement client-side sorting (or rejection) rather than
remapping to 'updatedAt'.
- Around line 325-327: The code only calls dataAPI._notifyNotesChanged on
create/delete; update paths (title edits, content saves, moves, pin/archive
toggles, status changes) must also emit notifications so plugin caches stay in
sync: locate all handlers that mutate note data (the createNote callback and the
other mutation handlers referenced near the create block and around the region
flagged at 434-437), and add calls to dataAPI._notifyNotesChanged({ kind:
'note', action: '<appropriate-action>', id: note.id }) after each successful
mutation — using action values like 'updated' for edits/saves, 'moved' for
moves, 'pinned'/'unpinned', 'archived'/'unarchived' or more specific actions as
appropriate — and ensure appAPI._notifyNoteCreated / deleted patterns are
mirrored for these update handlers so subscribers receive updates consistently.
In `@docs/plans/2026-03-11-data-access-api-design.md`:
- Around line 15-23: The fenced code block showing the data-access flow is
unlabeled and triggers MD040; add a language tag (e.g., "text") to the opening
fence so the block is a labeled plain-text snippet. Update the block that
documents createDataAPI(), DataAPIBridge, window.readied.notes.list,
ipcRenderer.invoke('notes:list'), and SQLiteNoteRepository.list to start with
```text (or another appropriate language) instead of just ``` so the markdown
linter stops flagging it.
In `@docs/plans/2026-03-11-data-access-api-implementation.md`:
- Line 636: Update the wording in the docs to use "same" instead of "exact same"
in the sentence describing the pattern for appAPI; specifically edit the
sentence that reads "Follow the exact same pattern as `appAPI` — it's passed
into `activate()`, tracked for event cleanup, and injected into the `context`
object." to "Follow the same pattern as `appAPI` — it's passed into
`activate()`, tracked for event cleanup, and injected into the `context`
object." Keep references to `PluginRegistry.activate()` and the new `dataAPI`
parameter untouched.
- Around line 1-13: The markdown skips an H2 between the document title and
tasks; update the heading hierarchy by either inserting an H2 before the task
list (e.g., a section like "## Tasks" after "Data Access API Implementation
Plan") or demoting "### Task 1" (and subsequent task headings) to H2 (change
"### Task 1" to "## Task 1") so the heading progression follows H1 → H2 → H3 and
satisfies markdown lint rules.
In `@packages/plugin-api/src/data/createDataAPI.ts`:
- Around line 3-24: Combine the two import statements at the top of
createDataAPI.ts into a single import so types and the runtime value are grouped
together: merge the long type-only import list (NoteInfo, NoteSummaryInfo,
NotebookInfo, NoteQueryOptions, NoteQueryResult, SearchOptions, SearchResult,
NotebookQueryOptions, NotebookDetailInfo, NotebookTreeNode, NotebookResult,
TagQueryOptions, TagInfo, GraphQueryOptions, LinkInfo, OutgoingLinkInfo,
GraphData, DataChangeEvent) with the DataAccessError import so there is no blank
line between import groups and the linter warning is resolved.
- Around line 212-222: The _notify* methods (_notifyNotesChanged,
_notifyNotebooksChanged, _notifyTagsChanged) iterate directly over listener Sets
(notesChangedListeners, notebooksChangedListeners, tagsChangedListeners) which
can misbehave if listeners are added/removed during dispatch; fix by taking a
snapshot of each Set (e.g., Array.from(...) or [...set]) and iterate over that
snapshot so additions/removals during callbacks won’t affect the current
dispatch order or cause skipped callbacks.
In `@packages/plugin-api/src/data/dataTypes.ts`:
- Around line 68-87: GraphQueryOptions.depth is declared but ignored by
getGraphData in createDataAPI.ts; update getGraphData to respect
GraphQueryOptions.depth by performing a breadth‑first traversal from initial
nodes (filtered by notebookId if provided), track visited node IDs, stop
expanding neighbors once the specified depth is reached, and collect nodes and
edges into the GraphData result; alternatively, if you prefer not to implement
traversal now, update the GraphQueryOptions or getGraphData JSDoc comment to
state that depth is reserved for future use so callers know it is currently
ignored. Ensure you reference GraphQueryOptions.depth, getGraphData, and the
GraphData node/edge shapes when making the change.
---
Outside diff comments:
In `@packages/plugin-api/tests/registry.test.ts`:
- Around line 103-127: The test "it('passes context with all expected APIs')"
never asserts PluginContext.data; update the test after receiving
receivedContext (cast to ctx) to assert ctx has a data property and that it is
the DataAPI passed into registry.activate (use the existing mockDataAPI), e.g.
add assertions like expect(ctx).toHaveProperty('data') and
expect(ctx.data).toBe(mockDataAPI) so the activation contract verifies the
DataAPI is routed through registry.activate/activate.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: dbe0d8d5-5555-4109-8c3d-054be1b4a1a1
📒 Files selected for processing (12)
apps/desktop/src/renderer/App.tsxdocs/plans/2026-03-11-data-access-api-design.mddocs/plans/2026-03-11-data-access-api-implementation.mdpackages/plugin-api/src/data/createDataAPI.tspackages/plugin-api/src/data/dataTypes.tspackages/plugin-api/src/index.tspackages/plugin-api/src/lifecycle/PluginHost.tsxpackages/plugin-api/src/lifecycle/PluginRegistry.tspackages/plugin-api/src/types.tspackages/plugin-api/tests/createDataAPI.test.tspackages/plugin-api/tests/dataTypes.test.tspackages/plugin-api/tests/registry.test.ts
| async getNotes(options) { | ||
| const notes = await window.readied.notes.list(options ? { | ||
| limit: options.limit, | ||
| offset: options.offset, | ||
| tag: options.tag, | ||
| sortBy: options.sortBy === 'wordCount' ? 'updatedAt' : options.sortBy, | ||
| sortOrder: options.sortOrder, | ||
| } : undefined); | ||
| let filtered = notes; | ||
| if (options?.notebookId) filtered = filtered.filter(n => n.notebookId === options.notebookId); | ||
| if (options?.status) filtered = filtered.filter(n => n.status === options.status); | ||
| if (options?.isPinned !== undefined) filtered = filtered.filter(n => n.isPinned === options.isPinned); | ||
| return { | ||
| notes: filtered.map(n => ({ | ||
| id: n.id, title: n.title, notebookId: n.notebookId, | ||
| tags: [...n.tags], wordCount: n.wordCount, | ||
| createdAt: n.createdAt, updatedAt: n.updatedAt, | ||
| isPinned: n.isPinned, status: n.status, | ||
| })), | ||
| total: filtered.length, | ||
| }; |
There was a problem hiding this comment.
Don't apply limit/offset before notebookId/status/isPinned filtering.
This paginates the global note list and only then narrows it, so filtered queries can return short or empty pages even when more matches exist later. total also becomes page-local here, which makes hasMore wrong downstream.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/desktop/src/renderer/App.tsx` around lines 196 - 216, getNotes is
currently applying limit/offset when calling window.readied.notes.list and then
filtering the page, which yields incorrect paging and total; change getNotes to
fetch the unpaged list (call window.readied.notes.list without limit/offset or
with a flag to fetch all), apply notebookId/status/isPinned filters to produce
the full filtered array, compute total = filtered.length, then apply
limit/offset slicing to the filtered array before mapping to the returned notes
shape—refer to getNotes and window.readied.notes.list to locate the call to
adjust.
| const notes = await window.readied.notes.list(options ? { | ||
| limit: options.limit, | ||
| offset: options.offset, | ||
| tag: options.tag, | ||
| sortBy: options.sortBy === 'wordCount' ? 'updatedAt' : options.sortBy, | ||
| sortOrder: options.sortOrder, | ||
| } : undefined); |
There was a problem hiding this comment.
Preserve sortBy: 'wordCount' semantics.
Rewriting wordCount to updatedAt means plugins get a different order than they requested. Either sort by wordCount client-side here or reject the option until the bridge can support it.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/desktop/src/renderer/App.tsx` around lines 197 - 203, The code replaces
sortBy: 'wordCount' with 'updatedAt' before calling window.readied.notes.list,
changing requested semantics; instead keep the original options when calling
window.readied.notes.list and, if options.sortBy === 'wordCount', post-process
the returned notes array (the notes variable) to sort by each note's wordCount
field according to options.sortOrder before using it, or alternatively return an
error/throw when the bridge doesn't support wordCount sorting; update the block
around window.readied.notes.list and the variable notes to implement client-side
sorting (or rejection) rather than remapping to 'updatedAt'.
| async searchNotes(query, options) { | ||
| const notes = await window.readied.notes.search(query, options?.limit ?? 20); | ||
| return { | ||
| results: notes.map(n => ({ id: n.id, title: n.title })), | ||
| total: notes.length, | ||
| }; | ||
| }, | ||
| async countNotes() { | ||
| const counts = await window.readied.notes.count(); | ||
| return counts.total; | ||
| }, |
There was a problem hiding this comment.
Honor the query options in searchNotes and countNotes.
searchNotes ignores options.notebookId, and countNotes ignores options entirely. Any plugin that scopes a query and then asks for the matching count will get inconsistent results.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/desktop/src/renderer/App.tsx` around lines 223 - 233, searchNotes
currently ignores options.notebookId and countNotes ignores options entirely;
update searchNotes (the async searchNotes(query, options) method) to pass
through options.notebookId (and preserve options?.limit ?? 20) to
window.readied.notes.search so the backend is scoped correctly, and change
countNotes to accept an options parameter and forward it to
window.readied.notes.count (e.g., window.readied.notes.count(options)) so counts
match the same query scope; keep existing return shapes unchanged.
| async getNotebookTree() { | ||
| type TreeNode = { id: string; name: string; parentId: string | null; noteCount: number; childCount: number; children: TreeNode[] }; | ||
| const tree = await window.readied.notebooks.tree(); | ||
| const mapNode = (node: { notebook: { id: string; name: string; parentId: string | null }; children: unknown[] }): TreeNode => ({ | ||
| id: node.notebook.id, | ||
| name: node.notebook.name, | ||
| parentId: node.notebook.parentId, | ||
| noteCount: 0, | ||
| childCount: node.children.length, | ||
| children: (node.children as typeof tree).map(mapNode), | ||
| }); | ||
| return tree.map(mapNode); |
There was a problem hiding this comment.
Don't hardcode noteCount to zero in tree results.
NotebookTreeNode exposes counts, so returning 0 for every node makes getNotebooks({ tree: true }) lie about notebook metadata. This needs real counts from the bridge, or the field should stay out of the public result until it can be populated.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/desktop/src/renderer/App.tsx` around lines 238 - 249, getNotebookTree
currently hardcodes noteCount to 0 in mapNode; instead read the real count
exposed by the bridge or omit the field. Update the TreeNode type and the
mapNode mapping inside getNotebookTree to use the bridge-provided value (e.g.,
node.notebook.noteCount) for noteCount and keep childCount computed from
node.children.length (or if the bridge does not provide noteCount, remove
noteCount from the returned shape). Ensure you reference getNotebookTree, the
local TreeNode type, mapNode and window.readied.notebooks.tree() when making the
change.
| appAPI._notifyNoteCreated({ id: newNote.id, title: newNote.title, content: newNote.content }); | ||
| }, [createNote, selectedNotebookId, clearSearch, appAPI]); | ||
| dataAPI._notifyNotesChanged({ kind: 'note', action: 'created', id: newNote.id }); | ||
| }, [createNote, selectedNotebookId, clearSearch, appAPI, dataAPI]); |
There was a problem hiding this comment.
Emit onNotesChanged for update paths too.
This file only notifies DataAPI subscribers on create/delete. Title edits, content saves, moves, pin/archive changes, and status changes in the handlers below mutate note data without any corresponding event, so plugin caches will drift out of date.
Also applies to: 434-437
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/desktop/src/renderer/App.tsx` around lines 325 - 327, The code only
calls dataAPI._notifyNotesChanged on create/delete; update paths (title edits,
content saves, moves, pin/archive toggles, status changes) must also emit
notifications so plugin caches stay in sync: locate all handlers that mutate
note data (the createNote callback and the other mutation handlers referenced
near the create block and around the region flagged at 434-437), and add calls
to dataAPI._notifyNotesChanged({ kind: 'note', action: '<appropriate-action>',
id: note.id }) after each successful mutation — using action values like
'updated' for edits/saves, 'moved' for moves, 'pinned'/'unpinned',
'archived'/'unarchived' or more specific actions as appropriate — and ensure
appAPI._notifyNoteCreated / deleted patterns are mirrored for these update
handlers so subscribers receive updates consistently.
| # Data Access API Implementation Plan | ||
|
|
||
| > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. | ||
|
|
||
| **Goal:** Add a dedicated `context.data` namespace to the plugin API with rich query capabilities (filters, sorting, pagination, graph, events, error handling). | ||
|
|
||
| **Architecture:** A `DataAPI` interface with typed query options, backed by a `DataAPIBridge` that maps to existing `window.readied.*` IPC calls. The `createDataAPI()` factory handles option merging, error wrapping, and event dispatch. `AppAPI` stays unchanged for backward compat. | ||
|
|
||
| **Tech Stack:** TypeScript, Zustand patterns, Electron IPC, vitest | ||
|
|
||
| --- | ||
|
|
||
| ### Task 1: Data types — query options and result types |
There was a problem hiding this comment.
Fix heading level progression for markdown lint compliance.
The document jumps from # Data Access API Implementation Plan (h1) directly to ### Task 1 (h3), skipping h2. This violates markdown heading increment rules.
📝 Proposed fix
Either add a section header before the tasks:
---
+## Tasks
+
### Task 1: Data types — query options and result typesOr change tasks to h2 level:
-### Task 1: Data types — query options and result types
+## Task 1: Data types — query options and result types📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Data Access API Implementation Plan | |
| > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. | |
| **Goal:** Add a dedicated `context.data` namespace to the plugin API with rich query capabilities (filters, sorting, pagination, graph, events, error handling). | |
| **Architecture:** A `DataAPI` interface with typed query options, backed by a `DataAPIBridge` that maps to existing `window.readied.*` IPC calls. The `createDataAPI()` factory handles option merging, error wrapping, and event dispatch. `AppAPI` stays unchanged for backward compat. | |
| **Tech Stack:** TypeScript, Zustand patterns, Electron IPC, vitest | |
| --- | |
| ### Task 1: Data types — query options and result types | |
| # Data Access API Implementation Plan | |
| > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. | |
| **Goal:** Add a dedicated `context.data` namespace to the plugin API with rich query capabilities (filters, sorting, pagination, graph, events, error handling). | |
| **Architecture:** A `DataAPI` interface with typed query options, backed by a `DataAPIBridge` that maps to existing `window.readied.*` IPC calls. The `createDataAPI()` factory handles option merging, error wrapping, and event dispatch. `AppAPI` stays unchanged for backward compat. | |
| **Tech Stack:** TypeScript, Zustand patterns, Electron IPC, vitest | |
| --- | |
| ## Task 1: Data types — query options and result types |
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 13-13: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3
(MD001, heading-increment)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/plans/2026-03-11-data-access-api-implementation.md` around lines 1 - 13,
The markdown skips an H2 between the document title and tasks; update the
heading hierarchy by either inserting an H2 before the task list (e.g., a
section like "## Tasks" after "Data Access API Implementation Plan") or demoting
"### Task 1" (and subsequent task headings) to H2 (change "### Task 1" to "##
Task 1") so the heading progression follows H1 → H2 → H3 and satisfies markdown
lint rules.
| - Modify: `packages/plugin-api/src/lifecycle/PluginRegistry.ts:90-98` (add `dataAPI` param to `activate()`) | ||
| - Modify: `packages/plugin-api/src/lifecycle/PluginRegistry.ts:228-290` (add `data: trackedData` to context) | ||
|
|
||
| **Context:** Follow the exact same pattern as `appAPI` — it's passed into `activate()`, tracked for event cleanup, and injected into the `context` object. The `PluginRegistry.activate()` signature gets a new `dataAPI` parameter. |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Minor style suggestion from static analysis.
"exact same" could be simplified to "same" for conciseness:
Follow the same pattern as
appAPI...
This is a very minor style nit.
🧰 Tools
🪛 LanguageTool
[style] ~636-~636: ‘exact same’ might be wordy. Consider a shorter alternative.
Context: ...ato context) **Context:** Follow the exact same pattern asappAPI` — it's passed into ...
(EN_WORDINESS_PREMIUM_EXACT_SAME)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/plans/2026-03-11-data-access-api-implementation.md` at line 636, Update
the wording in the docs to use "same" instead of "exact same" in the sentence
describing the pattern for appAPI; specifically edit the sentence that reads
"Follow the exact same pattern as `appAPI` — it's passed into `activate()`,
tracked for event cleanup, and injected into the `context` object." to "Follow
the same pattern as `appAPI` — it's passed into `activate()`, tracked for event
cleanup, and injected into the `context` object." Keep references to
`PluginRegistry.activate()` and the new `dataAPI` parameter untouched.
| import type { | ||
| NoteInfo, | ||
| NoteSummaryInfo, | ||
| NotebookInfo, | ||
| NoteQueryOptions, | ||
| NoteQueryResult, | ||
| SearchOptions, | ||
| SearchResult, | ||
| NotebookQueryOptions, | ||
| NotebookDetailInfo, | ||
| NotebookTreeNode, | ||
| NotebookResult, | ||
| TagQueryOptions, | ||
| TagInfo, | ||
| GraphQueryOptions, | ||
| LinkInfo, | ||
| OutgoingLinkInfo, | ||
| GraphData, | ||
| DataChangeEvent, | ||
| } from './dataTypes'; | ||
|
|
||
| import { DataAccessError } from './dataTypes'; |
There was a problem hiding this comment.
Address linting warning: Remove empty line between import groups.
Static analysis flagged an empty line between import groups. The type imports and value import should be grouped together.
🔧 Proposed fix
import type {
NoteInfo,
NoteSummaryInfo,
NotebookInfo,
NoteQueryOptions,
NoteQueryResult,
SearchOptions,
SearchResult,
NotebookQueryOptions,
NotebookDetailInfo,
NotebookTreeNode,
NotebookResult,
TagQueryOptions,
TagInfo,
GraphQueryOptions,
LinkInfo,
OutgoingLinkInfo,
GraphData,
DataChangeEvent,
} from './dataTypes';
-
import { DataAccessError } from './dataTypes';🧰 Tools
🪛 GitHub Check: lint
[warning] 3-3:
There should be no empty line between import groups
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/plugin-api/src/data/createDataAPI.ts` around lines 3 - 24, Combine
the two import statements at the top of createDataAPI.ts into a single import so
types and the runtime value are grouped together: merge the long type-only
import list (NoteInfo, NoteSummaryInfo, NotebookInfo, NoteQueryOptions,
NoteQueryResult, SearchOptions, SearchResult, NotebookQueryOptions,
NotebookDetailInfo, NotebookTreeNode, NotebookResult, TagQueryOptions, TagInfo,
GraphQueryOptions, LinkInfo, OutgoingLinkInfo, GraphData, DataChangeEvent) with
the DataAccessError import so there is no blank line between import groups and
the linter warning is resolved.
| _notifyNotesChanged(event) { | ||
| for (const cb of notesChangedListeners) cb(event); | ||
| }, | ||
|
|
||
| _notifyNotebooksChanged(event) { | ||
| for (const cb of notebooksChangedListeners) cb(event); | ||
| }, | ||
|
|
||
| _notifyTagsChanged(event) { | ||
| for (const cb of tagsChangedListeners) cb(event); | ||
| }, |
There was a problem hiding this comment.
Potential issue: Concurrent modification during event dispatch.
If a callback adds or removes listeners during _notify* iteration, JavaScript's Set iteration behavior may cause unexpected results (new additions won't be visited, but removals might cause skips depending on iteration state).
Consider iterating over a snapshot of the listeners:
🛡️ Proposed defensive fix
_notifyNotesChanged(event) {
- for (const cb of notesChangedListeners) cb(event);
+ for (const cb of [...notesChangedListeners]) cb(event);
},
_notifyNotebooksChanged(event) {
- for (const cb of notebooksChangedListeners) cb(event);
+ for (const cb of [...notebooksChangedListeners]) cb(event);
},
_notifyTagsChanged(event) {
- for (const cb of tagsChangedListeners) cb(event);
+ for (const cb of [...tagsChangedListeners]) cb(event);
},🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/plugin-api/src/data/createDataAPI.ts` around lines 212 - 222, The
_notify* methods (_notifyNotesChanged, _notifyNotebooksChanged,
_notifyTagsChanged) iterate directly over listener Sets (notesChangedListeners,
notebooksChangedListeners, tagsChangedListeners) which can misbehave if
listeners are added/removed during dispatch; fix by taking a snapshot of each
Set (e.g., Array.from(...) or [...set]) and iterate over that snapshot so
additions/removals during callbacks won’t affect the current dispatch order or
cause skipped callbacks.
| export interface GraphQueryOptions { | ||
| notebookId?: string; | ||
| depth?: number; | ||
| } | ||
|
|
||
| export interface LinkInfo { | ||
| noteId: string; | ||
| noteTitle: string; | ||
| } | ||
|
|
||
| export interface OutgoingLinkInfo { | ||
| targetId: string | null; | ||
| targetTitle: string; | ||
| resolved: boolean; | ||
| } | ||
|
|
||
| export interface GraphData { | ||
| nodes: Array<{ id: string; title: string; notebookId: string }>; | ||
| edges: Array<{ source: string; target: string }>; | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Note: depth option is declared but not implemented.
The GraphQueryOptions.depth property is defined but the createDataAPI.ts implementation only filters by notebookId. Consider either:
- Documenting that
depthis reserved for future use - Implementing depth-limited traversal in
getGraphData
This isn't blocking since the type is forward-compatible.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/plugin-api/src/data/dataTypes.ts` around lines 68 - 87,
GraphQueryOptions.depth is declared but ignored by getGraphData in
createDataAPI.ts; update getGraphData to respect GraphQueryOptions.depth by
performing a breadth‑first traversal from initial nodes (filtered by notebookId
if provided), track visited node IDs, stop expanding neighbors once the
specified depth is reached, and collect nodes and edges into the GraphData
result; alternatively, if you prefer not to implement traversal now, update the
GraphQueryOptions or getGraphData JSDoc comment to state that depth is reserved
for future use so callers know it is currently ignored. Ensure you reference
GraphQueryOptions.depth, getGraphData, and the GraphData node/edge shapes when
making the change.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix pagination: filter before applying limit/offset for correct totals - Add client-side sort for wordCount (not supported by bridge) - Use notebook.noteCount from bridge when available in tree view Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts: # packages/storage-sqlite/src/repositories/SQLiteNoteRepository.ts
Summary
context.data): Dedicated rich query interface for plugins, separate from the existingAppAPIonNotesChanged,onNotebooksChanged,onTagsChangedwith typed change events (created/updated/deleted/renamed)DataAccessErrorwith method name for clean plugin-side error handlingAppAPIunchanged — existing plugins keep workingArchitecture
Key types
NoteQueryOptions— filter by notebook, tag, status, pin; sort by title/date/wordCount; paginateTagQueryOptions— include colors, case-insensitive substring filter, paginationGraphQueryOptions— scope to notebook (client-side filtering)DataChangeEvent<T>— typed events with action + previousName for renamesTest plan
pnpm typecheck— 18/18 packages passpnpm test— 174 plugin-api tests pass (21 new)🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Tests
Documentation