Files
mnote/wolai-frontend/convex/_utils/documentVisibility.ts
T

118 lines
3.5 KiB
TypeScript
Raw Normal View History

2026-05-13 22:43:16 +08:00
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<string, { includeDescendants: boolean }> {
const result = new Map<string, { includeDescendants: boolean }>();
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<string, string | null>;
directShareMap: Map<string, { includeDescendants: boolean }>;
cache: Map<string, boolean>;
}): 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<string> {
const candidateDocs = input.includeDeletedDocuments
? [...input.docs]
: input.docs.filter((doc) => doc.deleted_at == null);
const parentById = new Map<string, string | null>();
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<string, boolean>();
const groupAccessCache = new Map<string, boolean>();
const visible = new Set<string>();
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;
}