Fixes for HistoryUUID

This commit is contained in:
Magnus Åhall 2026-06-10 08:03:33 +02:00
parent be7f5dbf30
commit 3e8d5b6d9a
7 changed files with 176 additions and 18 deletions

View file

@ -277,12 +277,6 @@ func actionSyncFromServer(w http.ResponseWriter, r *http.Request) { // {{{
return 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 { j, _ := json.Marshal(struct {
OK bool OK bool
Nodes []Node Nodes []Node
@ -383,7 +377,7 @@ func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{
return 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 { if err != nil {
Log.Error("sync", "error", err) Log.Error("sync", "error", err)
httpError(w, err) httpError(w, err)

View file

@ -44,6 +44,7 @@ type Node struct {
UUID string UUID string
UserID int `db:"user_id"` UserID int `db:"user_id"`
ParentUUID string `db:"parent_uuid"` ParentUUID string `db:"parent_uuid"`
HistoryUUID string `db:"history_uuid"`
Name string Name string
Created time.Time Created time.Time
Updated time.Time Updated time.Time
@ -122,7 +123,7 @@ func Nodes(userID, offset int, synced uint64, clientUUID string) (nodes []Node,
rows, err = db.Queryx(` rows, err = db.Queryx(`
SELECT SELECT
uuid, uuid,
COALESCE(parent_uuid, '') AS parent_uuid, COALESCE(parent_uuid, '00000000-0000-0000-0000-000000000000'::uuid) AS parent_uuid,
name, name,
created, created,
updated, updated,
@ -137,7 +138,7 @@ func Nodes(userID, offset int, synced uint64, clientUUID string) (nodes []Node,
public.node public.node
WHERE WHERE
user_id = $1 AND user_id = $1 AND
client != $5 AND client != $5::uuid AND
NOT history AND ( NOT history AND (
created_seq > $4 OR created_seq > $4 OR
updated_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(` rows, err = db.Queryx(`
SELECT SELECT
uuid, uuid,
history_uuid,
user_id, user_id,
name, name,
created, created,

View file

@ -257,7 +257,7 @@ $$;
CREATE TABLE public.client ( CREATE TABLE public.client (
id integer NOT NULL, id integer NOT NULL,
user_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, created timestamp with time zone DEFAULT now() NOT NULL,
description character varying DEFAULT ''::character varying NOT NULL description character varying DEFAULT ''::character varying NOT NULL
); );
@ -302,8 +302,8 @@ CREATE SEQUENCE public.node_updates
CREATE TABLE public.node ( CREATE TABLE public.node (
id integer NOT NULL, id integer NOT NULL,
user_id integer NOT NULL, user_id integer NOT NULL,
uuid character(36) DEFAULT gen_random_uuid() NOT NULL, "uuid" uuid DEFAULT gen_random_uuid() NOT NULL,
parent_uuid character(36), parent_uuid uuid,
created timestamp with time zone DEFAULT now() NOT NULL, created timestamp with time zone DEFAULT now() NOT NULL,
updated timestamp with time zone DEFAULT now() NOT NULL, updated timestamp with time zone DEFAULT now() NOT NULL,
deleted timestamp with time zone, deleted timestamp with time zone,
@ -315,7 +315,7 @@ CREATE TABLE public.node (
content_encrypted text DEFAULT ''::text NOT NULL, content_encrypted text DEFAULT ''::text NOT NULL,
markdown boolean DEFAULT false NOT NULL, markdown boolean DEFAULT false NOT NULL,
history 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, client_sequence integer,
CONSTRAINT name_length CHECK ((length(TRIM(BOTH FROM name)) > 0)) CONSTRAINT name_length CHECK ((length(TRIM(BOTH FROM name)) > 0))
); );
@ -328,7 +328,7 @@ CREATE TABLE public.node (
CREATE TABLE public.node_history ( CREATE TABLE public.node_history (
id integer NOT NULL, id integer NOT NULL,
user_id integer NOT NULL, user_id integer NOT NULL,
uuid character(36) NOT NULL, "uuid" uuid NOT NULL,
parents character varying[], parents character varying[],
created timestamp with time zone NOT NULL, created timestamp with time zone NOT NULL,
updated 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 text NOT NULL,
content_encrypted text NOT NULL, content_encrypted text NOT NULL,
markdown boolean DEFAULT false 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 client_sequence integer
); );

1
sql/00003.sql Normal file
View file

@ -0,0 +1 @@
ALTER TABLE public.node_history ADD history_uuid uuid NULL;

135
sql/00004.sql Normal file
View file

@ -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$
;

View file

@ -57,7 +57,7 @@ export class NodeStore {
break break
case 6: case 6:
nodesHistory = db.createObjectStore('nodes_history', { keyPath: ['UUID', 'Updated'] }) nodesHistory = db.createObjectStore('nodes_history', { keyPath: ['UUID', 'HistoryUUID'] })
break break
case 7: case 7:
@ -471,13 +471,38 @@ class NodeHistoryStore extends SimpleNodeStore {
} }
}) })
}// }}} }// }}}
retrievePage(uuid, perPage, page) {// {{{ test() {
return new Promise((resolve, _reject) => { const uuid = '019ead99-984c-72b6-98f0-814991473ad6'
const lowerBound = [uuid, '']
const upperBound = [uuid, 'z']
const range = IDBKeyRange.bound(lowerBound, upperBound)
const cursor = this.db const cursor = this.db
.transaction(['nodes', this.storeName], 'readonly') .transaction(['nodes', this.storeName], 'readonly')
.objectStore(this.storeName) .objectStore(this.storeName)
.index('byUUID') .openCursor(range, 'prev')
.openCursor(uuid, '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)
.openCursor(range, 'prev')
let retrieved = 0 let retrieved = 0
let first = true let first = true

View file

@ -422,6 +422,7 @@ export class Node {
async save() {//{{{ async save() {//{{{
this.data.Content = this._content this.data.Content = this._content
this.data.Updated = new Date().toISOString() 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 this._modified = false
_mbus.dispatch('NODE_UNMODIFIED') _mbus.dispatch('NODE_UNMODIFIED')