Fixes for HistoryUUID
This commit is contained in:
parent
be7f5dbf30
commit
3e8d5b6d9a
7 changed files with 176 additions and 18 deletions
8
main.go
8
main.go
|
|
@ -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)
|
||||||
|
|
|
||||||
6
node.go
6
node.go
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
1
sql/00003.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE public.node_history ADD history_uuid uuid NULL;
|
||||||
135
sql/00004.sql
Normal file
135
sql/00004.sql
Normal 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$
|
||||||
|
;
|
||||||
|
|
@ -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 {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}// }}}
|
}// }}}
|
||||||
|
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) {// {{{
|
retrievePage(uuid, perPage, page) {// {{{
|
||||||
return new Promise((resolve, _reject) => {
|
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
|
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')
|
|
||||||
|
|
||||||
let retrieved = 0
|
let retrieved = 0
|
||||||
let first = true
|
let first = true
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue