import type { Kysely } from 'kysely'; import type { BatchStatement } from '../db/batch.js'; import type { ContentStatus, Database, FieldRow, RevisionReason, RevisionRow } from '../db/schema.js'; import type { SeoData } from './items.js'; /** * Append-only revision history. * * This module deliberately holds no item-mutating code. `restoreRevision` lives in `items.ts` * instead, because restoring is an item write that has to go through `updateItem` to recompute * paths and redirects — putting it here would make `items.ts` and this file import each other. * Everything below is either a read or a statement builder, so the dependency runs one way: * `items.ts` imports from here, never the reverse. */ export declare class RevisionError extends Error { readonly code: 'not_found' | 'wrong_item'; name: string; constructor(message: string, code?: 'not_found' | 'wrong_item'); } export interface Revision extends Omit { data: Record; seo: SeoData; } /** A revision with the display name of whoever saved it, for the history panel. */ export interface RevisionWithAuthor extends Revision { author_name: string | null; author_email: string | null; } export declare function hydrateRevision(row: RevisionRow): Revision; export declare function listRevisions(db: Kysely, contentItemId: string, options?: { limit?: number; offset?: number; }): Promise<{ revisions: RevisionWithAuthor[]; total: number; }>; export declare function getRevision(db: Kysely, id: string): Promise; /** * The highest revision number an item has, and how many it has at all. * * Both come from one query because callers need both: the number to allocate the next revision, * the count to notice an item that predates its history and backfill a snapshot for it. */ export declare function revisionSequence(db: Kysely, contentItemId: string): Promise<{ count: number; latest: number; }>; export interface RevisionSnapshot { contentItemId: string; revisionNumber: number; title: string; slug: string; path: string; status: ContentStatus; data: Record; seo: SeoData; reason: RevisionReason; restoredFrom?: number | null; userId?: string | null; timestamp: string; } /** * Build the insert for one revision, to be appended to the caller's batch. * * Returned as a statement rather than executed, so the snapshot lands in the same atomic write as * the item change it describes. A revision that could be written independently of its item would * eventually disagree with it. */ export declare function buildRevisionStatement(db: Kysely, snapshot: RevisionSnapshot): BatchStatement; export interface RevisionChange { /** Human-readable name of what changed — a field's label, or Title/Slug/Status. */ label: string; /** `api_id` for content fields; null for the item's own columns. */ fieldApiId: string | null; } /** * What changed between two snapshots, as a list of labels for the history panel. * * Values are compared by their JSON form rather than by identity, which is what the database * stores and therefore what actually round-trips. A field whose value is structurally identical * but differently ordered would read as changed — acceptable, since the alternative is a deep * comparison whose notion of equality would drift from the one the database uses. */ export declare function revisionChanges(previous: Pick | undefined, current: Pick, fields: FieldRow[]): RevisionChange[]; /** * True when a save would produce a snapshot identical to the item's current state. * * Saving an unchanged form is common — an editor opens an item, thinks better of it, and hits save * anyway — and a history full of identical entries is worse than no history, because it buries the * saves that meant something. */ export declare function snapshotIsUnchanged(before: Pick, after: Pick): boolean;