082edc204b
* docs: design for syncing updated book data (cover + file) (#4544) Cover-change sync via a content hash (coverHash = partial MD5 of cover.png) plus a cover_updated_at field-level merge timestamp; file updates ride the existing re-import / metaHash dedupe. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sync): sync updated book covers across devices (#4544) Editing a book's cover wrote cover.png locally but changed no hash (the cover is keyed by the file hash), so peers had no signal to re-download it and the change never propagated. Give the cover its own content-addressed version: - coverHash = partial MD5 of cover.png; a peer re-downloads the cover iff the synced hash differs from the local one (idempotent, no churn on identical/re-extracted covers — compatible with the metaHash dedupe). - coverUpdatedAt = field-level LWW timestamp so a page-turn that wins whole-row LWW on updated_at can't clobber a cover edit (mirrors the reading_status_updated_at fix for #4634). Editing a cover recomputes the hash, bumps coverUpdatedAt, and re-uploads only the cover; the server merges cover fields independently; peers re-download on a hash diff. File updates continue to ride the existing re-import / metaHash dedupe (changed file -> changed hash -> re-key). Migration 016 adds cover_hash / cover_updated_at to books. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
136 lines
5.8 KiB
PL/PgSQL
136 lines
5.8 KiB
PL/PgSQL
ALTER FUNCTION auth.uid() OWNER TO supabase_auth_admin;
|
|
ALTER FUNCTION auth.role() OWNER TO supabase_auth_admin;
|
|
|
|
CREATE TABLE public.books (
|
|
user_id uuid NOT NULL,
|
|
book_hash text NOT NULL,
|
|
meta_hash text NULL,
|
|
format text NULL,
|
|
title text NULL,
|
|
source_title text NULL,
|
|
author text NULL,
|
|
"group" text NULL,
|
|
tags text[] NULL,
|
|
created_at timestamp with time zone NULL DEFAULT now(),
|
|
updated_at timestamp with time zone NULL DEFAULT now(),
|
|
deleted_at timestamp with time zone NULL,
|
|
uploaded_at timestamp with time zone NULL,
|
|
synced_at timestamp with time zone NOT NULL DEFAULT now(),
|
|
progress integer[] NULL,
|
|
reading_status text NULL,
|
|
reading_status_updated_at timestamp with time zone NULL,
|
|
cover_hash text NULL,
|
|
cover_updated_at timestamp with time zone NULL,
|
|
group_id text NULL,
|
|
group_name text NULL,
|
|
metadata json NULL,
|
|
CONSTRAINT books_pkey PRIMARY KEY (user_id, book_hash),
|
|
CONSTRAINT books_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users (id) ON DELETE CASCADE
|
|
);
|
|
|
|
-- Server-assigned incremental-pull cursor, decoupled from updated_at (the
|
|
-- client event time / sort key). A trigger stamps it on every write so a
|
|
-- server-resolved merge propagates without reordering the date-read library.
|
|
-- See migration 016_add_books_synced_at.sql (issue #4678).
|
|
CREATE INDEX idx_books_user_synced ON public.books (user_id, synced_at);
|
|
|
|
CREATE OR REPLACE FUNCTION public.set_books_synced_at()
|
|
RETURNS trigger
|
|
LANGUAGE plpgsql
|
|
AS $$
|
|
BEGIN
|
|
NEW.synced_at := now();
|
|
RETURN NEW;
|
|
END;
|
|
$$;
|
|
|
|
CREATE TRIGGER books_set_synced_at
|
|
BEFORE INSERT OR UPDATE ON public.books
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.set_books_synced_at();
|
|
|
|
ALTER TABLE public.books ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY select_books ON public.books FOR SELECT TO authenticated USING ((SELECT auth.uid()) = user_id);
|
|
CREATE POLICY insert_books ON public.books FOR INSERT TO authenticated WITH CHECK ((SELECT auth.uid()) = user_id);
|
|
CREATE POLICY update_books ON public.books FOR UPDATE TO authenticated USING ((SELECT auth.uid()) = user_id);
|
|
CREATE POLICY delete_books ON public.books FOR DELETE TO authenticated USING ((SELECT auth.uid()) = user_id);
|
|
|
|
CREATE TABLE public.book_configs (
|
|
user_id uuid NOT NULL,
|
|
book_hash text NOT NULL,
|
|
meta_hash text NULL,
|
|
location text NULL,
|
|
xpointer text NULL,
|
|
progress jsonb NULL,
|
|
rsvp_position text NULL,
|
|
search_config jsonb NULL,
|
|
view_settings jsonb NULL,
|
|
created_at timestamp with time zone NULL DEFAULT now(),
|
|
updated_at timestamp with time zone NULL DEFAULT now(),
|
|
deleted_at timestamp with time zone NULL,
|
|
CONSTRAINT book_configs_pkey PRIMARY KEY (user_id, book_hash),
|
|
CONSTRAINT book_configs_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users (id) ON DELETE CASCADE
|
|
);
|
|
|
|
ALTER TABLE public.book_configs ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY select_book_configs ON public.book_configs FOR SELECT TO authenticated USING ((SELECT auth.uid()) = user_id);
|
|
CREATE POLICY insert_book_configs ON public.book_configs FOR INSERT TO authenticated WITH CHECK ((SELECT auth.uid()) = user_id);
|
|
CREATE POLICY update_book_configs ON public.book_configs FOR UPDATE TO authenticated USING ((SELECT auth.uid()) = user_id);
|
|
CREATE POLICY delete_book_configs ON public.book_configs FOR DELETE TO authenticated USING ((SELECT auth.uid()) = user_id);
|
|
|
|
CREATE TABLE public.book_notes (
|
|
user_id uuid NOT NULL,
|
|
book_hash text NOT NULL,
|
|
meta_hash text NULL,
|
|
id text NOT NULL,
|
|
type text NULL,
|
|
cfi text NULL,
|
|
xpointer0 text NULL,
|
|
xpointer1 text NULL,
|
|
text text NULL,
|
|
style text NULL,
|
|
color text NULL,
|
|
note text NULL,
|
|
page integer NULL,
|
|
global boolean NULL,
|
|
created_at timestamp with time zone NULL DEFAULT now(),
|
|
updated_at timestamp with time zone NULL DEFAULT now(),
|
|
deleted_at timestamp with time zone NULL,
|
|
CONSTRAINT book_notes_pkey PRIMARY KEY (user_id, book_hash, id),
|
|
CONSTRAINT book_notes_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users (id) ON DELETE CASCADE
|
|
);
|
|
|
|
ALTER TABLE public.book_notes ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY select_book_notes ON public.book_notes FOR SELECT TO authenticated USING ((SELECT auth.uid()) = user_id);
|
|
CREATE POLICY insert_book_notes ON public.book_notes FOR INSERT TO authenticated WITH CHECK ((SELECT auth.uid()) = user_id);
|
|
CREATE POLICY update_book_notes ON public.book_notes FOR UPDATE TO authenticated USING ((SELECT auth.uid()) = user_id);
|
|
CREATE POLICY delete_book_notes ON public.book_notes FOR DELETE TO authenticated USING ((SELECT auth.uid()) = user_id);
|
|
|
|
CREATE TABLE public.files (
|
|
id uuid NOT NULL DEFAULT gen_random_uuid(),
|
|
user_id uuid NOT NULL,
|
|
book_hash text NULL,
|
|
file_key text NOT NULL,
|
|
file_size bigint NOT NULL,
|
|
created_at timestamp with time zone NULL DEFAULT now(),
|
|
updated_at timestamp with time zone NULL DEFAULT now(),
|
|
deleted_at timestamp with time zone NULL,
|
|
CONSTRAINT files_pkey PRIMARY KEY (id),
|
|
CONSTRAINT files_file_key_key UNIQUE (file_key),
|
|
CONSTRAINT files_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users (id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX idx_files_user_id_deleted_at ON public.files (user_id, deleted_at);
|
|
CREATE INDEX idx_files_file_key ON public.files (file_key);
|
|
CREATE INDEX idx_files_file_key_deleted_at ON public.files (file_key, deleted_at);
|
|
|
|
ALTER TABLE public.files ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY files_insert ON public.files FOR INSERT WITH CHECK (auth.uid() = user_id);
|
|
CREATE POLICY files_select ON public.files FOR SELECT USING (auth.uid() = user_id AND deleted_at IS NULL);
|
|
CREATE POLICY files_update ON public.files FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (deleted_at IS NULL OR deleted_at > now());
|
|
CREATE POLICY files_delete ON public.files FOR DELETE USING (auth.uid() = user_id);
|
|
|
|
GRANT ALL ON public.books TO authenticated;
|
|
GRANT ALL ON public.book_configs TO authenticated;
|
|
GRANT ALL ON public.book_notes TO authenticated;
|
|
GRANT ALL ON public.files TO authenticated; |