Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell. | 1 | /** |
| 2 | * The trash's two ends, over D1 alone so they are tested on SQLite | |
| 3 | * (trash.test.ts): what deleting folios for good does to the database | |
| 4 | * (`eraseFolios`: one artifact, the whole trash, the daily purge), what | |
| 5 | * deleting a space does to its rows (`deleteSpaceRows`), and where a folio | |
| 6 | * restored from the trash lands when its space is gone (`landingFor`). | |
| 7 | * Rooms, events and the index's vectors are the callers' (service.ts). | |
| 8 | */ | |
| 9 | import { atLeast } from "../access.ts"; | |
| 10 | import { forgetFolios, type IndexEnv } from "../indexer.ts"; | |
| 11 | import type { Space } from "../who.ts"; | |
| 12 | import { json, runBatches } from "./access-store.ts"; | |
| 13 | ||
| 14 | /** | |
| 15 | * Deletes folios for good: their passages leave the index, children still | |
| 16 | * alive under them go to the top of where they were, and their search rows | |
| 17 | * and rows go, deepest first, so no parent goes before its children. Returns | |
| 18 | * the ids that went, in that order. Callers destroy the rooms. | |
| 19 | */ | |
| 20 | export async function eraseFolios(env: IndexEnv, rows: readonly { id: string; path: string }[]): Promise<string[]> { | |
| 21 | if (!rows.length) return []; | |
| 22 | const unique = [...new Map(rows.map((r) => [r.id, r])).values()]; | |
| 23 | const ids = unique.sort((x, y) => y.path.length - x.path.length).map((r) => r.id); | |
| 24 | await env.DB.prepare("UPDATE folios SET parent_id = NULL WHERE parent_id IN (SELECT value FROM json_each(?)) AND id NOT IN (SELECT value FROM json_each(?))").bind(json(ids), json(ids)).run(); | |
| 25 | await forgetFolios(env, ids); | |
| 26 | await runBatches( | |
| 27 | env.DB, | |
| 28 | ids.flatMap((id) => [env.DB.prepare("DELETE FROM folios_fts WHERE folio_id = ?").bind(id), env.DB.prepare("DELETE FROM folios WHERE id = ?").bind(id)]), | |
| 29 | ); | |
| 30 | return ids; | |
| 31 | } | |
| 32 | ||
| 33 | /** | |
| 34 | * What deleting a space does to the database, at `at` by `by`: every folio | |
| 35 | * in it not already in the trash goes there together (so each top restores | |
| 36 | * as usual), the space's pages (Docs' older model) too, members' sidebars | |
| 37 | * drop it, and its row is marked deleted with its address freed for a new | |
| 38 | * space (`<slug>~<id>`, which no address can be; see migration 0007). The | |
| 39 | * row stays, so the trash keeps the space's name and rules and a deleted | |
| 40 | * General is never made again. Returns the ids of the folios trashed. | |
| 41 | */ | |
| 42 | export async function deleteSpaceRows(db: D1Database, space: { id: string; slug: string }, at: string, by: string): Promise<string[]> { | |
| 43 | const rows = (await db.prepare("SELECT id FROM folios WHERE space_id = ? AND trashed_at IS NULL").bind(space.id).all<{ id: string }>()).results; | |
| 44 | await db.batch([ | |
| 45 | db.prepare("UPDATE folios SET trashed_at = ?, trashed_by = ? WHERE space_id = ? AND trashed_at IS NULL").bind(at, by, space.id), | |
| 46 | db.prepare("UPDATE pages SET archived_at = ?, archived_by = ? WHERE space_id = ? AND archived_at IS NULL").bind(at, by, space.id), | |
| 47 | db.prepare("DELETE FROM space_joins WHERE space_id = ?").bind(space.id), | |
| 48 | db.prepare("UPDATE spaces SET deleted_at = ?, deleted_by = ?, slug = ? WHERE id = ? AND deleted_at IS NULL").bind(at, by, `${space.slug}~${space.id}`, space.id), | |
| 49 | ]); | |
| 50 | return rows.map((r) => r.id); | |
| 51 | } | |
| 52 | ||
| 53 | /** | |
| 54 | * Where a folio restored from the trash lands when the space it was in has | |
| 55 | * been deleted: the General space, when there is one that is live and the | |
| 56 | * restorer may add to; else null, its owner's Private. | |
| 57 | */ | |
| 58 | export function landingFor(spaces: readonly Space[]): Space | null { | |
| 59 | return spaces.find((s) => s.row.is_default && !s.row.deleted_at && !s.row.archived_at && atLeast(s.role, "edit")) ?? null; | |
| 60 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.