export type DocumentVisibilityScope = "private" | "shared" | "public" | null | undefined; export type DocumentVisibilityRecord = { id: string; user_id?: string | null; parent_id?: string | null; access_scope?: DocumentVisibilityScope; deleted_at?: string | null; }; export type DocumentVisibilityShareRecord = { document_id: string; include_descendants?: boolean | null; }; type BuildVisibleDocumentIdsInput = { userId: string; docs: readonly DocumentVisibilityRecord[]; directShares: readonly DocumentVisibilityShareRecord[]; directGroupShares: readonly DocumentVisibilityShareRecord[]; includeDeletedDocuments?: boolean; }; function normalizeDocumentId(value: string | null | undefined): string | null { const trimmed = String(value ?? "").trim(); return trimmed.length > 0 ? trimmed : null; } function buildInheritedShareMap( shares: readonly DocumentVisibilityShareRecord[], ): Map { const result = new Map(); for (const share of shares) { const documentId = normalizeDocumentId(share.document_id); if (!documentId) continue; const existing = result.get(documentId); if (!existing) { result.set(documentId, { includeDescendants: Boolean(share.include_descendants), }); continue; } existing.includeDescendants = existing.includeDescendants || Boolean(share.include_descendants); } return result; } function canAccessByInheritedShare(input: { docId: string; parentById: Map; directShareMap: Map; cache: Map; }): boolean { const cached = input.cache.get(input.docId); if (typeof cached === "boolean") return cached; if (input.directShareMap.has(input.docId)) { input.cache.set(input.docId, true); return true; } let parentId = input.parentById.get(input.docId) ?? null; for (let depth = 0; depth < 60 && parentId; depth += 1) { const parentShare = input.directShareMap.get(parentId); if (parentShare && parentShare.includeDescendants) { input.cache.set(input.docId, true); return true; } parentId = input.parentById.get(parentId) ?? null; } input.cache.set(input.docId, false); return false; } export function buildVisibleDocumentIds(input: BuildVisibleDocumentIdsInput): Set { const candidateDocs = input.includeDeletedDocuments ? [...input.docs] : input.docs.filter((doc) => doc.deleted_at == null); const parentById = new Map(); for (const doc of candidateDocs) { parentById.set(doc.id, normalizeDocumentId(doc.parent_id) ?? null); } const directShares = buildInheritedShareMap(input.directShares); const directGroupShares = buildInheritedShareMap(input.directGroupShares); const shareAccessCache = new Map(); const groupAccessCache = new Map(); const visible = new Set(); for (const doc of candidateDocs) { if (String(doc.user_id ?? "") === input.userId) { visible.add(doc.id); continue; } if (doc.access_scope === "public") { visible.add(doc.id); continue; } if ( canAccessByInheritedShare({ docId: doc.id, parentById, directShareMap: directShares, cache: shareAccessCache, }) || canAccessByInheritedShare({ docId: doc.id, parentById, directShareMap: directGroupShares, cache: groupAccessCache, }) ) { visible.add(doc.id); } } return visible; }