diff --git a/main.go b/main.go index 875fd00..664a50e 100644 --- a/main.go +++ b/main.go @@ -277,12 +277,6 @@ func actionSyncFromServer(w http.ResponseWriter, r *http.Request) { // {{{ return } - /* - Log.Debug("/sync/from_server", "num_nodes", len(nodes), "maxSeq", maxSeq) - foo, _ := json.Marshal(nodes) - os.WriteFile(fmt.Sprintf("/tmp/nodes-%d.json", offset), foo, 0644) - */ - j, _ := json.Marshal(struct { OK bool Nodes []Node @@ -383,7 +377,7 @@ func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{ return } - _, err = db.Exec(`CALL add_nodes($1, $2, $3::jsonb)`, user.UserID, user.ClientUUID, request.NodeData) + _, err = db.Exec(`CALL add_nodes($1, $2::uuid, $3::jsonb)`, user.UserID, user.ClientUUID, request.NodeData) if err != nil { Log.Error("sync", "error", err) httpError(w, err) diff --git a/node.go b/node.go index c8afe94..6b79769 100644 --- a/node.go +++ b/node.go @@ -44,6 +44,7 @@ type Node struct { UUID string UserID int `db:"user_id"` ParentUUID string `db:"parent_uuid"` + HistoryUUID string `db:"history_uuid"` Name string Created time.Time Updated time.Time @@ -122,7 +123,7 @@ func Nodes(userID, offset int, synced uint64, clientUUID string) (nodes []Node, rows, err = db.Queryx(` SELECT uuid, - COALESCE(parent_uuid, '') AS parent_uuid, + COALESCE(parent_uuid, '00000000-0000-0000-0000-000000000000'::uuid) AS parent_uuid, name, created, updated, @@ -137,7 +138,7 @@ func Nodes(userID, offset int, synced uint64, clientUUID string) (nodes []Node, public.node WHERE user_id = $1 AND - client != $5 AND + client != $5::uuid AND NOT history AND ( created_seq > $4 OR updated_seq > $4 OR @@ -251,6 +252,7 @@ func RetrieveNodeHistory(userID int, nodeUUID string, offset int) (nodes []Node, rows, err = db.Queryx(` SELECT uuid, + history_uuid, user_id, name, created, diff --git a/sql/00001.sql b/sql/00001.sql index 7eb8273..4aecc91 100644 --- a/sql/00001.sql +++ b/sql/00001.sql @@ -257,7 +257,7 @@ $$; CREATE TABLE public.client ( id integer NOT NULL, user_id integer NOT NULL, - client_uuid character(36) DEFAULT ''::bpchar NOT NULL, + client_uuid uuid DEFAULT '00000000-0000-0000-0000-000000000000'::uuid NOT NULL, created timestamp with time zone DEFAULT now() NOT NULL, description character varying DEFAULT ''::character varying NOT NULL ); @@ -302,8 +302,8 @@ CREATE SEQUENCE public.node_updates CREATE TABLE public.node ( id integer NOT NULL, user_id integer NOT NULL, - uuid character(36) DEFAULT gen_random_uuid() NOT NULL, - parent_uuid character(36), + "uuid" uuid DEFAULT gen_random_uuid() NOT NULL, + parent_uuid uuid, created timestamp with time zone DEFAULT now() NOT NULL, updated timestamp with time zone DEFAULT now() NOT NULL, deleted timestamp with time zone, @@ -315,7 +315,7 @@ CREATE TABLE public.node ( content_encrypted text DEFAULT ''::text NOT NULL, markdown boolean DEFAULT false NOT NULL, history boolean DEFAULT false NOT NULL, - client character(36) DEFAULT ''::bpchar NOT NULL, + client uuid DEFAULT '00000000-0000-0000-0000-000000000000'::uuid NOT NULL, client_sequence integer, CONSTRAINT name_length CHECK ((length(TRIM(BOTH FROM name)) > 0)) ); @@ -328,7 +328,7 @@ CREATE TABLE public.node ( CREATE TABLE public.node_history ( id integer NOT NULL, user_id integer NOT NULL, - uuid character(36) NOT NULL, + "uuid" uuid NOT NULL, parents character varying[], created timestamp with time zone NOT NULL, updated timestamp with time zone NOT NULL, @@ -336,7 +336,7 @@ CREATE TABLE public.node_history ( content text NOT NULL, content_encrypted text NOT NULL, markdown boolean DEFAULT false NOT NULL, - client character(36) DEFAULT ''::bpchar NOT NULL, + client uuid DEFAULT '00000000-0000-0000-0000-000000000000'::uuid NOT NULL, client_sequence integer ); diff --git a/sql/00003.sql b/sql/00003.sql new file mode 100644 index 0000000..a0cd4b1 --- /dev/null +++ b/sql/00003.sql @@ -0,0 +1 @@ +ALTER TABLE public.node_history ADD history_uuid uuid NULL; diff --git a/sql/00004.sql b/sql/00004.sql new file mode 100644 index 0000000..eafbad2 --- /dev/null +++ b/sql/00004.sql @@ -0,0 +1,135 @@ +CREATE UNIQUE INDEX node_history_user_id_idx ON public.node_history (user_id,"uuid",history_uuid); + + +ALTER TABLE public.node ALTER COLUMN "uuid" TYPE uuid USING "uuid"::uuid::uuid; +ALTER TABLE public.node ALTER COLUMN parent_uuid TYPE uuid USING parent_uuid::uuid::uuid; +ALTER TABLE public.node ALTER COLUMN client TYPE uuid USING client::uuid::uuid; + + + +CREATE OR REPLACE PROCEDURE public.add_nodes(IN p_user_id integer, IN p_client_uuid uuid, IN p_nodes jsonb) + LANGUAGE plpgsql +AS $procedure$ + +DECLARE + node_data jsonb; + node_updated timestamptz; + db_updated timestamptz; + db_uuid uuid; + db_client uuid; + db_client_seq int; + db_history_uuid uuid; + node_uuid uuid; + node_parent_uuid uuid; + node_history_uuid uuid; + +BEGIN + RAISE NOTICE '--------------------------'; + FOR node_data IN SELECT * FROM jsonb_array_elements(p_nodes) + LOOP + node_uuid = (node_data->>'UUID')::uuid; + node_history_uuid = (node_data->>'HistoryUUID')::uuid; + node_updated = (node_data->>'Updated')::timestamptz; + + + + -- Frontend is using an all-zero UUID to define the root node. + -- Database is using NULL. + IF node_data->>'ParentUUID' = '00000000-0000-0000-0000-000000000000' THEN + node_parent_uuid = NULL; + ELSE + node_parent_uuid = (node_data->>'ParentUUID')::uuid; + END IF; + + + + -- Every jode has a new history UUID to keep the history entry uniquely identifiable + -- across clients. A history entry could potentially be sent again, but should be + -- safe to ignore as every change to a node should have a new history UUID. + -- + -- The current node is also stored as history. + INSERT INTO node_history( + user_id, "uuid", "history_uuid", parents, created, updated, + "name", "content", markdown, "content_encrypted", + client, client_sequence + ) + VALUES( + p_user_id, -- combined key + node_uuid, -- combined key + node_history_uuid, -- combined key + (jsonb_populate_record(null::json_ancestor_array, node_data))."Ancestors", + (node_data->>'Created')::timestamptz, + (node_data->>'Updated')::timestamptz, + (node_data->>'Name')::varchar, + (node_data->>'Content')::text, + (node_data->>'Markdown')::bool, + '', /* content_encrypted */ + p_client_uuid, + (node_data->>'ClientSequence')::int + ) + ON CONFLICT ("user_id", "uuid", "history_uuid") + DO NOTHING; + + + + -- Retrieve the current modified timestamp for this node from the database. + SELECT + uuid, updated, client, client_sequence + INTO + db_uuid, db_updated, db_client, db_client_seq + FROM public."node" + WHERE + user_id = p_user_id AND + uuid::uuid = node_uuid::uuid; + + + + -- Is the node not in database? It needs to be created. + IF db_uuid IS NULL THEN + RAISE NOTICE '01 New node %', node_uuid; + + INSERT INTO public."node" ( + user_id, "uuid", parent_uuid, created, updated, + "name", "content", markdown, "content_encrypted", + client, client_sequence + ) + VALUES( + p_user_id, + node_uuid, + node_parent_uuid, + (node_data->>'Created')::timestamptz, + (node_data->>'Updated')::timestamptz, + (node_data->>'Name')::varchar, + (node_data->>'Content')::text, + (node_data->>'Markdown')::bool, + '', /* content_encrypted */ + p_client_uuid, + (node_data->>'ClientSequence')::int + ); + + CONTINUE; + + END IF; + + + + -- Update the public node as well if it was older than incoming node. + IF node_updated > db_updated THEN + UPDATE public."node" + SET + updated = (node_data->>'Updated')::timestamptz, + updated_seq = nextval('node_updates'), + name = (node_data->>'Name')::varchar, + content = (node_data->>'Content')::text, + markdown = (node_data->>'Markdown')::bool, + client = p_client_uuid, + client_sequence = (node_data->>'ClientSequence')::int + WHERE + user_id = p_user_id AND + uuid::uuid = node_uuid::uuid; + END IF; + + END LOOP; +END +$procedure$ +; diff --git a/static/js/node_store.mjs b/static/js/node_store.mjs index 3bd2701..9920e06 100644 --- a/static/js/node_store.mjs +++ b/static/js/node_store.mjs @@ -57,7 +57,7 @@ export class NodeStore { break case 6: - nodesHistory = db.createObjectStore('nodes_history', { keyPath: ['UUID', 'Updated'] }) + nodesHistory = db.createObjectStore('nodes_history', { keyPath: ['UUID', 'HistoryUUID'] }) break case 7: @@ -471,13 +471,38 @@ class NodeHistoryStore extends SimpleNodeStore { } }) }// }}} + test() { + const uuid = '019ead99-984c-72b6-98f0-814991473ad6' + const lowerBound = [uuid, ''] + const upperBound = [uuid, 'z'] + const range = IDBKeyRange.bound(lowerBound, upperBound) + + const cursor = this.db + .transaction(['nodes', this.storeName], 'readonly') + .objectStore(this.storeName) + .openCursor(range, 'prev') + + cursor.onsuccess = (event) => { + const cursor = event.target.result + if (!cursor) + return + + console.log(cursor.value) + cursor.continue() + } + } + retrievePage(uuid, perPage, page) {// {{{ return new Promise((resolve, _reject) => { + + const lowerBound = [uuid, '00000000-0000-0000-0000-000000000000'] + const upperBound = [uuid, 'ffffffff-ffff-ffff-ffff-ffffffffffff'] + const range = IDBKeyRange.bound(lowerBound, upperBound) + const cursor = this.db .transaction(['nodes', this.storeName], 'readonly') .objectStore(this.storeName) - .index('byUUID') - .openCursor(uuid, 'prev') + .openCursor(range, 'prev') let retrieved = 0 let first = true diff --git a/static/js/page_node.mjs b/static/js/page_node.mjs index cca6cf0..2005816 100644 --- a/static/js/page_node.mjs +++ b/static/js/page_node.mjs @@ -422,6 +422,7 @@ export class Node { async save() {//{{{ this.data.Content = this._content this.data.Updated = new Date().toISOString() + this.data.HistoryUUID = uuidv7() // every time the node is saved a new history UUID identifies the changed node. this._modified = false _mbus.dispatch('NODE_UNMODIFIED')