refactor(web+backend): consolidate types, optimize N+1 queries [D1, A3, A4, D2]

- types.ts: consolidate NoteDoc, WorkspaceDoc, NoteAgentActionDoc etc. from client files
- notes-client.ts: import from types.ts, optimize getNoteDetail with direct GET /notes/:id
- review-client.ts: import from types.ts, use /note-agent-actions/pending (eliminates N+1)
- notes-client.ts: use /workspaces/summaries (eliminates fetch-all-notes for counts)
- backend: add GET /workspaces/summaries with noteCount per workspace
- backend: add GET /note-agent-actions/pending (cross-workspace)
- backend: add countNotesByWorkspaces + listPendingActions repository functions
- Add createNote, archiveNote, restoreNote, createNoteRelationship client functions
- Fix existing tests for new route counts and mock order
This commit is contained in:
saravanakumardb1 2026-03-19 07:32:54 -07:00
parent dbb1a84dba
commit ee586065dd
11 changed files with 302 additions and 199 deletions

View File

@ -41,6 +41,29 @@ export async function createNoteAgentAction(doc: NoteAgentActionDoc): Promise<No
return collection().create(doc);
}
export async function listPendingActions(
userId: string,
productId: string,
limit = 50,
offset = 0,
): Promise<{ items: NoteAgentActionDoc[]; total: number }> {
const filter: FilterMap = {
userId,
productId,
state: { $in: ['draft', 'proposed'] },
};
const total = await collection().count(filter);
const items = await collection().findMany({
filter,
sort: { updatedAt: -1 },
offset,
limit,
});
return { items, total };
}
export async function updateNoteAgentAction(
id: string,
workspaceId: string,

View File

@ -9,6 +9,7 @@ vi.mock('../../lib/auth.js', () => ({ extractAuth: extractAuthMock }));
vi.mock('../../lib/product-config.js', () => ({ PRODUCT_ID: 'notelett' }));
vi.mock('./repository.js', () => ({
listNoteAgentActions: vi.fn(async () => ({ items: [], total: 0 })),
listPendingActions: vi.fn(async () => ({ items: [], total: 0 })),
getNoteAgentAction: vi.fn(async () => null),
createNoteAgentAction: vi.fn(async (doc: unknown) => doc),
updateNoteAgentAction: vi.fn(async () => null),
@ -28,7 +29,7 @@ describe('noteAgentActionRoutes', () => {
await noteAgentActionRoutes(app as never);
expect(app.get).toHaveBeenCalledTimes(1);
expect(app.get).toHaveBeenCalledTimes(2);
expect(app.post).toHaveBeenCalledTimes(2);
expect(app.patch).toHaveBeenCalledTimes(1);
});

View File

@ -23,6 +23,16 @@ export async function noteAgentActionRoutes(app: FastifyInstance) {
return { ...result, limit: parsed.data.limit, offset: parsed.data.offset };
});
app.get('/note-agent-actions/pending', async (req: FastifyRequest) => {
const auth = await extractAuth(req);
const query = req.query as { limit?: string; offset?: string };
const limit = Math.min(Math.max(Number(query.limit) || 50, 1), 100);
const offset = Math.max(Number(query.offset) || 0, 0);
const result = await repo.listPendingActions(auth.sub, PRODUCT_ID, limit, offset);
return { ...result, limit, offset };
});
app.post('/note-agent-actions', async (req: FastifyRequest, reply: FastifyReply) => {
const auth = await extractAuth(req);
const parsed = CreateNoteAgentActionSchema.safeParse(req.body);

View File

@ -29,6 +29,19 @@ export async function listNotes(
return { items, total };
}
export async function countNotesByWorkspaces(
userId: string,
productId: string,
workspaceIds: string[],
): Promise<Map<string, number>> {
const counts = new Map<string, number>();
for (const wsId of workspaceIds) {
const count = await collection().count({ userId, productId, workspaceId: wsId });
counts.set(wsId, count);
}
return counts;
}
export async function getNote(id: string, workspaceId: string): Promise<NoteDoc | null> {
return collection().findById(id, workspaceId);
}

View File

@ -13,6 +13,9 @@ vi.mock('./repository.js', () => ({
createWorkspace: vi.fn(async (doc: unknown) => doc),
updateWorkspace: vi.fn(async () => null),
}));
vi.mock('../notes/repository.js', () => ({
countNotesByWorkspaces: vi.fn(async () => new Map()),
}));
describe('workspaceRoutes', () => {
beforeEach(() => {
@ -28,7 +31,7 @@ describe('workspaceRoutes', () => {
await workspaceRoutes(app as never);
expect(app.get).toHaveBeenCalledTimes(2);
expect(app.get).toHaveBeenCalledTimes(3);
expect(app.post).toHaveBeenCalledTimes(1);
expect(app.patch).toHaveBeenCalledTimes(1);
});

View File

@ -3,6 +3,7 @@ import { BadRequestError, NotFoundError } from '../../lib/errors.js';
import { extractAuth } from '../../lib/auth.js';
import { PRODUCT_ID } from '../../lib/product-config.js';
import * as repo from './repository.js';
import { countNotesByWorkspaces } from '../notes/repository.js';
import {
CreateWorkspaceSchema,
ListWorkspacesQuerySchema,
@ -11,6 +12,21 @@ import {
} from './types.js';
export async function workspaceRoutes(app: FastifyInstance) {
app.get('/workspaces/summaries', async req => {
const auth = await extractAuth(req);
const result = await repo.listWorkspaces(auth.sub, PRODUCT_ID, { limit: 100, offset: 0 });
const wsIds = result.items.map(ws => ws.id);
const noteCounts = await countNotesByWorkspaces(auth.sub, PRODUCT_ID, wsIds);
return {
items: result.items.map(ws => ({
...ws,
noteCount: noteCounts.get(ws.id) ?? 0,
})),
total: result.total,
};
});
app.get('/workspaces', async req => {
const auth = await extractAuth(req);
const parsed = ListWorkspacesQuerySchema.safeParse(req.query);

View File

@ -27,7 +27,7 @@
---
## Phase 1 — Bug Fixes (Gaps A1A6, D3D4) [ ]
## Phase 1 — Bug Fixes (Gaps A1A6, D3D4) [x]
**Goal:** Eliminate all runtime bugs and latent crash risks. Clean dead code.
**Estimated effort:** 23 hours
@ -35,31 +35,31 @@
### Tasks
- [ ] **1.1** Lazy-init `extractionApi` in `web/src/lib/extraction-client.ts` (Gap A1)
- [x] **1.1** Lazy-init `extractionApi` in `web/src/lib/extraction-client.ts` (Gap A1) (`dbb1a84`)
- Replace `const extractionApi = createApiClient(...)` with lazy singleton `function getExtractionApi()`
- Pattern: same as NomGap `protocol-client.ts` / `social-client.ts`
- File: `web/src/lib/extraction-client.ts`
- [ ] **1.2** Lazy-init `blobClient` in `web/src/lib/blob-client.ts` (Gap A1)
- [x] **1.2** Lazy-init `blobClient` in `web/src/lib/blob-client.ts` (Gap A1) (`dbb1a84`)
- Replace `const blobClient = createBlobClient(...)` with lazy singleton `function getBlobClient()`
- Update all 4 call sites within the file (`blobClient.getSasUrl(...)` → `getBlobClient().getSasUrl(...)`)
- Remove dead re-export `export { blobClient }` on line 48 (zero external imports)
- File: `web/src/lib/blob-client.ts`
- [ ] **1.3** Add `"use client"` directive to `web/src/lib/notes-client.ts` (Gap A6)
- [x] **1.3** Add `"use client"` directive to `web/src/lib/notes-client.ts` (Gap A6) (`dbb1a84`)
- This file imports `extraction-client.ts` (module-scope API client). Without `"use client"`, any future server-component import would crash.
- Add `"use client";` as the first line of the file.
- File: `web/src/lib/notes-client.ts`
- [ ] **1.4** Add `output: "standalone"` to `web/next.config.ts` (Gap A2)
- [x] **1.4** Add `output: "standalone"` to `web/next.config.ts` (Gap A2) (`dbb1a84`)
- Required for Docker builds. `outputFileTracingRoot` is already set.
- File: `web/next.config.ts`
- [ ] **1.5** Delete dead code: `web/src/lib/mock-data.ts` (Gap D3)
- [x] **1.5** Delete dead code: `web/src/lib/mock-data.ts` (Gap D3) (`dbb1a84`)
- Confirmed zero imports. 228 lines of unused scaffold-era mock data.
- Delete: `web/src/lib/mock-data.ts`
- [ ] **1.6** Delete dead code: `web/src/lib/review-data.ts` (Gap D4)
- [x] **1.6** Delete dead code: `web/src/lib/review-data.ts` (Gap D4) (`dbb1a84`)
- Confirmed zero imports. Superseded by `review-client.ts`.
- Delete: `web/src/lib/review-data.ts`
@ -460,7 +460,7 @@ Track completed phases and commits here as work progresses.
| Date | Phase | Commit | Summary |
|------|-------|--------|---------|
| | | | |
| 2026-03-19 | Phase 1 | `dbb1a84` | Bug fixes: lazy-init SSR clients, use-client directive, standalone output, delete dead code |
---

View File

@ -22,6 +22,22 @@ describe("getNoteDetail", () => {
});
it("merges backend tasks with extracted suggestions, preserves artifact blob metadata, and normalizes review state", async () => {
const noteItem = {
id: "note-1",
workspaceId: "workspace-1",
title: "Launch note",
body: "Sarah agreed to handle the testing by Friday.",
status: "active",
tags: ["launch"],
updatedAt: "2026-03-10T12:01:00.000Z",
updatedBy: "editor-1",
createdBy: "editor-1",
sourceType: "manual",
};
// 1. GET /notes (fallback path — no knownWorkspaceId)
fetchMock.mockResolvedValueOnce({ items: [noteItem] });
// 26. Parallel: /workspaces, /notes?workspaceId=..., /note-tasks, /note-artifacts, /note-agent-actions
fetchMock.mockResolvedValueOnce({
items: [
{
@ -33,22 +49,7 @@ describe("getNoteDetail", () => {
},
],
});
fetchMock.mockResolvedValueOnce({
items: [
{
id: "note-1",
workspaceId: "workspace-1",
title: "Launch note",
body: "Sarah agreed to handle the testing by Friday.",
status: "active",
tags: ["launch"],
updatedAt: "2026-03-10T12:01:00.000Z",
updatedBy: "editor-1",
createdBy: "editor-1",
sourceType: "manual",
},
],
});
fetchMock.mockResolvedValueOnce({ items: [noteItem] });
fetchMock.mockResolvedValueOnce({
items: [
{
@ -98,6 +99,8 @@ describe("getNoteDetail", () => {
},
],
});
// 7. GET /note-relationships (sequential after parallel batch)
fetchMock.mockResolvedValueOnce({ items: [] });
extractSuggestedTasksMock.mockResolvedValue([
{

View File

@ -2,94 +2,27 @@
import { extractSuggestedTasks } from "@/lib/extraction-client";
import { createNotesApiClient } from "@/lib/api-helpers";
import type { AgentTimelineItem, ArtifactSummary, LinkedNote, NoteDetail, NoteSummary, NoteTask, WorkspaceSummary } from "@/lib/types";
type NoteDoc = {
id: string;
workspaceId: string;
title: string;
body: string;
status: "draft" | "active" | "archived";
tags: string[];
updatedAt: string;
updatedBy: string;
createdBy: string;
sourceType?: string;
};
type WorkspaceDoc = {
id: string;
name: string;
description?: string;
members: Array<{ userId: string; role: string }>;
updatedAt: string;
updatedBy: string;
};
type NoteListResponse = {
items: NoteDoc[];
};
type WorkspaceListResponse = {
items: WorkspaceDoc[];
};
type NoteTaskDoc = {
id: string;
noteId: string;
title: string;
description?: string;
status: "open" | "in_progress" | "completed" | "canceled";
source: "manual" | "extracted";
};
type NoteArtifactDoc = {
id: string;
noteId: string;
artifactType: "file" | "summary" | "extraction" | "citation" | "export";
title: string;
description?: string;
blobPath?: string;
contentType?: string;
sizeBytes?: number;
};
type NoteAgentActionDoc = {
id: string;
noteId: string;
actorId: string;
actorType: "agent" | "human";
actionType: "create" | "update" | "summarize" | "extract_tasks" | "attach_citation";
state: "draft" | "proposed" | "approved" | "rejected" | "applied";
reason?: string;
beforeSummary?: string;
afterSummary?: string;
updatedAt: string;
};
type NoteTaskListResponse = {
items: NoteTaskDoc[];
};
type NoteArtifactListResponse = {
items: NoteArtifactDoc[];
};
type NoteAgentActionListResponse = {
items: NoteAgentActionDoc[];
};
type NoteRelationshipDoc = {
id: string;
workspaceId: string;
fromNoteId: string;
toNoteId: string;
relationshipType: string;
};
type NoteRelationshipListResponse = {
items: NoteRelationshipDoc[];
};
import type {
AgentTimelineItem,
ArtifactSummary,
LinkedNote,
NoteArtifactDoc,
NoteArtifactListResponse,
NoteAgentActionDoc,
NoteAgentActionListResponse,
NoteDetail,
NoteDoc,
NoteListResponse,
NoteRelationshipDoc,
NoteRelationshipListResponse,
NoteSummary,
NoteTask,
NoteTaskDoc,
NoteTaskListResponse,
WorkspaceDoc,
WorkspaceListResponse,
WorkspaceSummary,
} from "@/lib/types";
function buildWorkspaceMap(workspaces: WorkspaceDoc[]) {
@ -208,14 +141,26 @@ function toLinkedNotes(
});
}
type WorkspaceSummaryDoc = WorkspaceDoc & { noteCount: number };
type WorkspaceSummaryListResponse = { items: WorkspaceSummaryDoc[]; total: number };
export async function listWorkspaceSummaries(): Promise<WorkspaceSummary[]> {
const api = createNotesApiClient();
const [workspaceResponse, noteResponse] = await Promise.all([
api.fetch<WorkspaceListResponse>("/workspaces"),
api.fetch<NoteListResponse>("/notes"),
]);
const response = await api.fetch<WorkspaceSummaryListResponse>("/workspaces/summaries");
return workspaceResponse.items.map((workspace) => toWorkspaceSummary(workspace, noteResponse.items));
return response.items.map((ws) => {
const owner = ws.members.find((m) => m.role === "owner")?.userId ?? ws.updatedBy;
return {
id: ws.id,
name: ws.name,
description: ws.description ?? "",
owner,
noteCount: ws.noteCount,
visibility: ws.members.length > 1 ? "shared" : "private",
updatedAt: ws.updatedAt,
tags: [],
};
});
}
export async function listNoteSummaries(): Promise<NoteSummary[]> {
@ -290,42 +235,49 @@ export async function createNoteTask(input: {
});
}
export async function getNoteDetail(noteId: string): Promise<NoteDetail | null> {
export async function getNoteDetail(noteId: string, knownWorkspaceId?: string): Promise<NoteDetail | null> {
const api = createNotesApiClient();
const [workspaceResponse, noteResponse] = await Promise.all([
api.fetch<WorkspaceListResponse>("/workspaces"),
api.fetch<NoteListResponse>("/notes"),
]);
const note = noteResponse.items.find((item) => item.id === noteId);
if (!note) {
return null;
let note: NoteDoc | undefined;
let workspaceId: string;
if (knownWorkspaceId) {
try {
note = await api.fetch<NoteDoc>(
`/notes/${encodeURIComponent(noteId)}?workspaceId=${encodeURIComponent(knownWorkspaceId)}`
);
workspaceId = knownWorkspaceId;
} catch {
return null;
}
} else {
const noteResponse = await api.fetch<NoteListResponse>("/notes");
note = noteResponse.items.find((item) => item.id === noteId);
if (!note) return null;
workspaceId = note.workspaceId;
}
const workspaceMap = buildWorkspaceMap(workspaceResponse.items);
const noteMap = buildNoteMap(noteResponse.items);
const workspace = workspaceMap.get(note.workspaceId);
const [taskResponse, artifactResponse, actionResponse] = await Promise.all([
api.fetch<NoteTaskListResponse>(
`/note-tasks?workspaceId=${encodeURIComponent(note.workspaceId)}&noteId=${encodeURIComponent(note.id)}`
),
api.fetch<NoteArtifactListResponse>(
`/note-artifacts?workspaceId=${encodeURIComponent(note.workspaceId)}&noteId=${encodeURIComponent(note.id)}`
),
api.fetch<NoteAgentActionListResponse>(
`/note-agent-actions?workspaceId=${encodeURIComponent(note.workspaceId)}&noteId=${encodeURIComponent(note.id)}`
),
]);
let relationshipResponse: NoteRelationshipListResponse = { items: [] };
const wsId = encodeURIComponent(workspaceId);
const nId = encodeURIComponent(noteId);
const [workspaceResponse, notesForLinked, taskResponse, artifactResponse, actionResponse] = await Promise.all([
api.fetch<WorkspaceListResponse>("/workspaces"),
api.fetch<NoteListResponse>(`/notes?workspaceId=${wsId}`),
api.fetch<NoteTaskListResponse>(`/note-tasks?workspaceId=${wsId}&noteId=${nId}`),
api.fetch<NoteArtifactListResponse>(`/note-artifacts?workspaceId=${wsId}&noteId=${nId}`),
api.fetch<NoteAgentActionListResponse>(`/note-agent-actions?workspaceId=${wsId}&noteId=${nId}`),
]);
const workspaceMap = buildWorkspaceMap(workspaceResponse.items);
const noteMap = buildNoteMap(notesForLinked.items);
const workspace = workspaceMap.get(workspaceId);
let relationshipResponse: NoteRelationshipListResponse = { items: [] };
try {
const fetchedRelationships = await api.fetch<NoteRelationshipListResponse>(
`/note-relationships?workspaceId=${encodeURIComponent(note.workspaceId)}&noteId=${encodeURIComponent(note.id)}`
const fetched = await api.fetch<NoteRelationshipListResponse>(
`/note-relationships?workspaceId=${wsId}&noteId=${nId}`
);
relationshipResponse =
fetchedRelationships && Array.isArray(fetchedRelationships.items)
? fetchedRelationships
: { items: [] };
relationshipResponse = fetched && Array.isArray(fetched.items) ? fetched : { items: [] };
} catch {
relationshipResponse = { items: [] };
}
@ -358,3 +310,48 @@ export async function getNoteDetail(noteId: string): Promise<NoteDetail | null>
timeline,
};
}
export async function createNote(input: {
id: string;
workspaceId: string;
title: string;
body: string;
tags?: string[];
sourceType?: string;
}): Promise<NoteDoc> {
const api = createNotesApiClient();
return api.fetch<NoteDoc>("/notes", {
method: "POST",
body: JSON.stringify(input),
});
}
export async function archiveNote(noteId: string, workspaceId: string): Promise<void> {
const api = createNotesApiClient();
await api.fetch(`/notes/${encodeURIComponent(noteId)}/archive`, {
method: "POST",
body: JSON.stringify({ workspaceId }),
});
}
export async function restoreNote(noteId: string, workspaceId: string): Promise<void> {
const api = createNotesApiClient();
await api.fetch(`/notes/${encodeURIComponent(noteId)}/restore`, {
method: "POST",
body: JSON.stringify({ workspaceId }),
});
}
export async function createNoteRelationship(input: {
id: string;
workspaceId: string;
fromNoteId: string;
toNoteId: string;
relationshipType: string;
}): Promise<void> {
const api = createNotesApiClient();
await api.fetch("/note-relationships", {
method: "POST",
body: JSON.stringify(input),
});
}

View File

@ -1,28 +1,5 @@
import { createNotesApiClient } from "@/lib/api-helpers";
import type { AgentTimelineItem, ApprovalQueueItem } from "@/lib/types";
import { listWorkspaceSummaries } from "@/lib/notes-client";
type NoteAgentActionDoc = {
id: string;
workspaceId: string;
noteId: string;
actorId: string;
actorType: "agent" | "human";
toolName: string;
actionType: "create" | "update" | "summarize" | "extract_tasks" | "attach_citation";
state: "draft" | "proposed" | "approved" | "rejected" | "applied";
reason?: string;
beforeSummary?: string;
afterSummary?: string;
reviewedBy?: string;
reviewedAt?: string;
reviewNote?: string;
updatedAt: string;
};
type NoteAgentActionListResponse = {
items: NoteAgentActionDoc[];
};
import type { AgentTimelineItem, ApprovalQueueItem, NoteAgentActionDoc, NoteAgentActionListResponse } from "@/lib/types";
function toSeverity(actionType: NoteAgentActionDoc["actionType"]): ApprovalQueueItem["severity"] {
@ -44,7 +21,7 @@ function toTimelineItem(action: NoteAgentActionDoc): AgentTimelineItem {
action: `${action.actorType} ${action.actionType.replaceAll("_", " ")}`,
timestamp: action.updatedAt,
status: action.state,
summary: action.afterSummary ?? action.reason ?? action.toolName,
summary: action.afterSummary ?? action.reason ?? action.toolName ?? action.actionType,
};
}
@ -83,37 +60,20 @@ async function updateAgentActionState(
);
}
async function listAgentActionsForWorkspace(workspaceId: string): Promise<NoteAgentActionDoc[]> {
const api = createNotesApiClient();
const response = await api.fetch<NoteAgentActionListResponse>(
`/note-agent-actions?workspaceId=${encodeURIComponent(workspaceId)}`
);
return response.items;
}
export async function listApprovalQueue(): Promise<ApprovalQueueItem[]> {
const workspaces = await listWorkspaceSummaries();
const actionGroups = await Promise.all(
workspaces.map((workspace) => listAgentActionsForWorkspace(workspace.id))
);
return actionGroups
.flat()
.filter((action) => action.state === "draft" || action.state === "proposed")
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
const api = createNotesApiClient();
const response = await api.fetch<NoteAgentActionListResponse>("/note-agent-actions/pending");
return response.items
.sort((a: NoteAgentActionDoc, b: NoteAgentActionDoc) => b.updatedAt.localeCompare(a.updatedAt))
.map(toApprovalQueueItem);
}
export async function listAgentTimeline(noteId?: string): Promise<AgentTimelineItem[]> {
const workspaces = await listWorkspaceSummaries();
const actionGroups = await Promise.all(
workspaces.map((workspace) => listAgentActionsForWorkspace(workspace.id))
);
return actionGroups
.flat()
.filter((action) => (noteId ? action.noteId === noteId : true))
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
const api = createNotesApiClient();
const response = await api.fetch<NoteAgentActionListResponse>("/note-agent-actions/pending");
return response.items
.filter((action: NoteAgentActionDoc) => (noteId ? action.noteId === noteId : true))
.sort((a: NoteAgentActionDoc, b: NoteAgentActionDoc) => b.updatedAt.localeCompare(a.updatedAt))
.map(toTimelineItem);
}

View File

@ -104,3 +104,80 @@ export interface ProductUser {
workspaceId: string;
[key: string]: unknown;
}
// ── Backend document types (consolidated from client files) ──
export type NoteDoc = {
id: string;
workspaceId: string;
title: string;
body: string;
status: "draft" | "active" | "archived";
tags: string[];
updatedAt: string;
updatedBy: string;
createdBy: string;
sourceType?: string;
};
export type WorkspaceDoc = {
id: string;
name: string;
description?: string;
members: Array<{ userId: string; role: string }>;
updatedAt: string;
updatedBy: string;
};
export type NoteTaskDoc = {
id: string;
noteId: string;
title: string;
description?: string;
status: "open" | "in_progress" | "completed" | "canceled";
source: "manual" | "extracted";
};
export type NoteArtifactDoc = {
id: string;
noteId: string;
artifactType: "file" | "summary" | "extraction" | "citation" | "export";
title: string;
description?: string;
blobPath?: string;
contentType?: string;
sizeBytes?: number;
};
export type NoteAgentActionDoc = {
id: string;
workspaceId: string;
noteId: string;
actorId: string;
actorType: "agent" | "human";
toolName?: string;
actionType: "create" | "update" | "summarize" | "extract_tasks" | "attach_citation";
state: "draft" | "proposed" | "approved" | "rejected" | "applied";
reason?: string;
beforeSummary?: string;
afterSummary?: string;
reviewedBy?: string;
reviewedAt?: string;
reviewNote?: string;
updatedAt: string;
};
export type NoteRelationshipDoc = {
id: string;
workspaceId: string;
fromNoteId: string;
toNoteId: string;
relationshipType: string;
};
export type NoteListResponse = { items: NoteDoc[] };
export type WorkspaceListResponse = { items: WorkspaceDoc[] };
export type NoteTaskListResponse = { items: NoteTaskDoc[] };
export type NoteArtifactListResponse = { items: NoteArtifactDoc[] };
export type NoteAgentActionListResponse = { items: NoteAgentActionDoc[] };
export type NoteRelationshipListResponse = { items: NoteRelationshipDoc[] };