fix(core): drill-down lists descendants under untracked intermediates
listEntries({ parentId: X }) now applies the top-level rule relative to X: this user's
live entries on descendants of X (up to three levels) with no live-entry work strictly
between them and X, so it agrees with childSummary. Restoring an engagement's re-parenting
onto a soft-deleted entry throws ValidationError("entryId", "target entry is deleted").
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
00fc0180b0
commit
c6865b2522
@@ -264,11 +264,12 @@ export async function restoreEngagementFromHistory(
|
||||
// Undo a merge's re-parenting; the old entry must be this user's.
|
||||
const owner = await k
|
||||
.selectFrom("entries")
|
||||
.select("id")
|
||||
.select(["id", "deleted_at"])
|
||||
.where("id", "=", before.entryId)
|
||||
.where("user_id", "=", userId)
|
||||
.executeTakeFirst();
|
||||
if (!owner) throw new NotFoundError("entry");
|
||||
if (owner.deleted_at !== null) throw new ValidationError("entryId", "target entry is deleted");
|
||||
extra.entry_id = before.entryId;
|
||||
}
|
||||
await write(k, userId, eg, mediaType, patch as Partial<EngagementPatch>, "user", extra);
|
||||
|
||||
@@ -730,6 +730,21 @@ const HAS_ENTRY_ANCESTOR = sql<boolean>`exists (select 1 from works a1
|
||||
and (pe.work_id = a1.id or pe.work_id = a2.id or pe.work_id = a3.id)
|
||||
where a1.id = w.parent_id)`;
|
||||
|
||||
/**
|
||||
* True when the entry's work is a descendant of `parentId` (up to three levels) with no work strictly
|
||||
* between them carrying a live entry of this user: the top-level rule, relative to `parentId`.
|
||||
*/
|
||||
function underParent(parentId: string) {
|
||||
const liveEntryOn = (ids: ReturnType<typeof sql>) =>
|
||||
sql`exists (select 1 from entries pe where pe.user_id = e.user_id and pe.deleted_at is null and pe.work_id in (${ids}))`;
|
||||
return sql<boolean>`(w.parent_id = ${parentId}
|
||||
or exists (select 1 from works p1 where p1.id = w.parent_id and p1.parent_id = ${parentId}
|
||||
and not ${liveEntryOn(sql`p1.id`)})
|
||||
or exists (select 1 from works p1 inner join works p2 on p2.id = p1.parent_id
|
||||
where p1.id = w.parent_id and p2.parent_id = ${parentId}
|
||||
and not ${liveEntryOn(sql`p1.id, p2.id`)}))`;
|
||||
}
|
||||
|
||||
function viewQuery(k: Executor, userId: string) {
|
||||
return k
|
||||
.selectFrom("entries as e")
|
||||
@@ -801,7 +816,8 @@ export interface EntryFilter {
|
||||
q?: string;
|
||||
/**
|
||||
* Default `null`: the top of the user's library, i.e. entries with no ancestor work (up to three
|
||||
* levels) carrying a live entry of this user. A work id lists that work's direct children.
|
||||
* levels) carrying a live entry of this user. A work id applies the same rule relative to that work:
|
||||
* its descendants (up to three levels) with no live-entry work strictly between them and it.
|
||||
*/
|
||||
parentId?: string | null;
|
||||
sort?: "updated" | "title" | "rating";
|
||||
@@ -829,7 +845,7 @@ export async function listEntries(
|
||||
|
||||
let q = viewQuery(db.k, userId);
|
||||
const parentId = filter.parentId === undefined ? null : filter.parentId;
|
||||
q = parentId === null ? q.where(sql<boolean>`not ${HAS_ENTRY_ANCESTOR}`) : q.where("w.parent_id", "=", parentId);
|
||||
q = q.where(parentId === null ? sql<boolean>`not ${HAS_ENTRY_ANCESTOR}` : underParent(parentId));
|
||||
if (filter.mediaType) q = q.where(sql<string>`coalesce(e.media_type_override, w.media_type)`, "=", filter.mediaType);
|
||||
if (filter.status) q = q.where("e.status", "=", filter.status);
|
||||
if (filter.tag) {
|
||||
|
||||
Reference in New Issue
Block a user