Why: Review found unsupported node detail calls, discarded child pages, shallow trace selection, stale detail responses, invalid operation filters, and missing audit identity. Evidence: 35 frontend test files with 285 tests pass; production build, knip baseline, and diff check pass. Impact: Both trees paginate recursively, trace selection covers every depth, and audit rows preserve valid operation, object, and source semantics. Refs: rdm-two-trees-latest-templates
60 lines
2.0 KiB
TypeScript
60 lines
2.0 KiB
TypeScript
export interface AuditTreeItem {
|
|
kind: string
|
|
id: string
|
|
title: string
|
|
state?: string
|
|
hasChildren?: boolean
|
|
order?: number
|
|
templateBlockId?: string
|
|
}
|
|
|
|
export interface AuditTreePage {
|
|
items: AuditTreeItem[]
|
|
nextCursor?: string
|
|
hasMore: boolean
|
|
truncated: boolean
|
|
}
|
|
|
|
type AuditTreePageFetcher = (parent: string, cursor: string) => Promise<AuditTreePage>
|
|
|
|
export function mergeTreePage(current: AuditTreePage, next: AuditTreePage): AuditTreePage {
|
|
return { ...next, items: [...current.items, ...next.items] }
|
|
}
|
|
|
|
async function collectParentPages(fetchPage: AuditTreePageFetcher, parent: string): Promise<AuditTreeItem[]> {
|
|
const items: AuditTreeItem[] = []
|
|
const seenCursors = new Set<string>()
|
|
let cursor = ''
|
|
for (let pageNo = 0; pageNo < 100; pageNo++) {
|
|
if (seenCursors.has(cursor)) throw new Error('树分页游标重复,已停止加载')
|
|
seenCursors.add(cursor)
|
|
const page = await fetchPage(parent, cursor)
|
|
items.push(...page.items)
|
|
if (!page.hasMore) return items
|
|
if (!page.nextCursor) throw new Error('树分页缺少后续游标')
|
|
cursor = page.nextCursor
|
|
}
|
|
throw new Error('树分页超过安全上限')
|
|
}
|
|
|
|
export async function collectRecursiveTree(fetchPage: AuditTreePageFetcher): Promise<AuditTreeItem[]> {
|
|
const roots = await collectParentPages(fetchPage, '')
|
|
const out = [...roots]
|
|
const queue = roots.filter(item => item.hasChildren === true)
|
|
const seenParents = new Set<string>()
|
|
while (queue.length > 0) {
|
|
const parent = queue.shift()!
|
|
if (seenParents.has(parent.id)) continue
|
|
seenParents.add(parent.id)
|
|
const children = await collectParentPages(fetchPage, parent.id)
|
|
out.push(...children)
|
|
if (out.length > 5000) throw new Error('树节点超过安全上限')
|
|
queue.push(...children.filter(item => item.hasChildren === true))
|
|
}
|
|
return out
|
|
}
|
|
|
|
export function isTreeItemInspectable(item: AuditTreeItem): boolean {
|
|
return item.kind === 'section' && !item.templateBlockId && !item.id.startsWith('dn-')
|
|
}
|