Skip to content
← Public packages

@charlesmatte/granola

Search and read Granola meeting notes, summaries and transcripts from any agent; manage webhooks and legal holds safely.

src/folders/list.ts

48 lines · 2.6 KB · TypeScript
Raw
import { listEndpoint, pageSpec, parseWithAccount } from '../lib/api.ts'

type Folder = { id: string; object: 'folder'; name: string; parent_folder_id: string | null }
type FolderNode = Folder & { path: string; depth: number; children: Array<FolderNode> }

/** Attach `path` ("Parent / Child") and `depth` to each folder and build the tree (roots first). */
export function folderTree(folders: Array<Folder>) {
	const byId = new Map<string, FolderNode>(folders.map((f) => [f.id, { ...f, path: f.name, depth: 0, children: [] }]))
	const roots: Array<FolderNode> = []
	for (const node of byId.values()) {
		const parent = node.parent_folder_id ? byId.get(node.parent_folder_id) : undefined
		if (parent) parent.children.push(node)
		else roots.push(node)
	}
	const walk = (node: FolderNode, prefix: string, depth: number) => {
		node.path = prefix ? `${prefix} / ${node.name}` : node.name
		node.depth = depth
		for (const child of node.children) walk(child, node.path, depth + 1)
	}
	for (const root of roots) walk(root, '', 0)
	return { roots, flat: [...byId.values()].map(({ children, ...f }) => ({ ...f, childCount: children.length })) }
}

/**
 * List the folders the API key can access (`GET /v1/folders`, alphabetical), following cursors.
 * Each folder gets a readable `path` and `depth` computed from `parent_folder_id`; `tree: true`
 * also returns the nested tree. Use a folder `id` with ./notes/list `folderId`.
 *
 * @param input.account - Account name (secret `granola-<account>`). Required. @param input.secretName - Secret override.
 * @param input.tree - Also return `tree` (nested roots → children). Default false.
 * @param input.limit - Max folders (1-5000, default 5000). @param input.paginate - Follow cursors (default true). @param input.cursor - Resume.
 * @returns `{ account, count, folders: [{ id, name, parent_folder_id, path, depth, childCount }], tree?, hasMore, cursor }`
 *
 * @example
 * import listFolders from 'kody:@charlesmatte/granola/folders/list'
 *
 * const { folders } = await listFolders({ account: 'work' })
 */
export default async function listGranolaFolders(input: unknown) {
	const p = parseWithAccount<{ tree?: boolean; limit?: number; cursor?: string; paginate?: boolean }>(
		input,
		{ tree: { type: 'boolean' }, ...pageSpec(5000) },
		'folders/list',
	)
	const r = await listEndpoint<Folder>(p.acct, { path: '/v1/folders', itemsKey: 'folders', limit: p.limit ?? 5000, maxPageSize: 30, cursor: p.cursor, paginate: p.paginate ?? true })
	const t = folderTree(r.items)
	return { account: p.acct.account, count: r.count, folders: t.flat, ...(p.tree ? { tree: t.roots } : {}), hasMore: r.hasMore, cursor: r.cursor, pages: r.pages }
}