Compare commits

..

4 commits

Author SHA1 Message Date
Magnus Åhall
662053e750 Bumped to v3 2026-05-26 13:54:47 +02:00
Magnus Åhall
5a67c638c6 Regriession fix for title update causing trouble for content edit. 2026-05-26 13:54:22 +02:00
Magnus Åhall
4c513a5106 Reset SQL 2026-05-26 11:02:56 +02:00
Magnus Åhall
75d242c041 Read config from file in command line arguments 2026-05-26 10:09:24 +02:00
15 changed files with 545 additions and 576 deletions

View file

@ -3,7 +3,6 @@ package main
import ( import (
// Standard // Standard
"encoding/json" "encoding/json"
"fmt"
"os" "os"
) )
@ -27,9 +26,8 @@ type Config struct {
} }
} }
func readConfig() (err error) { func readConfig(fname string) (err error) {
var configData []byte var configData []byte
fname := fmt.Sprintf("%s/.config/notes2.json", os.Getenv("HOME"))
configData, err = os.ReadFile(fname) configData, err = os.ReadFile(fname)
if err != nil { if err != nil {
return return

View file

@ -23,7 +23,7 @@ import (
"text/template" "text/template"
) )
const VERSION = "v2" const VERSION = "v3"
const CONTEXT_USER = 1 const CONTEXT_USER = 1
const SYNC_PAGINATION = 200 const SYNC_PAGINATION = 200
@ -75,7 +75,7 @@ func initLog() { // {{{
} // }}} } // }}}
func main() { // {{{ func main() { // {{{
initLog() initLog()
err := readConfig() err := readConfig(FlagConfig)
if err != nil { if err != nil {
Log.Error("config", "error", err) Log.Error("config", "error", err)
os.Exit(1) os.Exit(1)

View file

@ -1,48 +1,218 @@
CREATE EXTENSION IF NOT EXISTS pg_trgm; --
CREATE EXTENSION IF NOT EXISTS pgcrypto; -- Name: pg_trgm; Type: EXTENSION; Schema: -; Owner: -
--
CREATE SEQUENCE node_updates; CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public;
CREATE TABLE public."user" (
id serial4 NOT NULL, --
username varchar NOT NULL, -- Name: EXTENSION pg_trgm; Type: COMMENT; Schema: -; Owner:
"name" varchar NOT NULL, --
"password" varchar NOT NULL,
last_login timestamp DEFAULT now() NOT NULL, COMMENT ON EXTENSION pg_trgm IS 'text similarity measurement and index searching based on trigrams';
timezone varchar DEFAULT 'UTC'::character varying NOT NULL,
CONSTRAINT user_pk PRIMARY KEY (id)
--
-- Name: pgcrypto; Type: EXTENSION; Schema: -; Owner: -
--
CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public;
--
-- Name: EXTENSION pgcrypto; Type: COMMENT; Schema: -; Owner:
--
COMMENT ON EXTENSION pgcrypto IS 'cryptographic functions';
--
-- Name: json_ancestor_array; Type: TYPE; Schema: public; Owner: postgres
--
CREATE TYPE public.json_ancestor_array AS (
"Ancestors" character varying[]
); );
CREATE TABLE public.node ( --
id serial4 NOT NULL, -- Name: add_nodes(integer, character varying, jsonb); Type: PROCEDURE; Schema: public; Owner: postgres
user_id int4 NOT NULL, --
"uuid" bpchar(36) DEFAULT gen_random_uuid() NOT NULL,
parent_uuid bpchar(36) NULL,
created timestamptz DEFAULT NOW() NOT NULL, CREATE PROCEDURE public.add_nodes(IN p_user_id integer, IN p_client_uuid character varying, IN p_nodes jsonb)
updated timestamptz DEFAULT NOW() NOT NULL, LANGUAGE plpgsql
deleted timestamptz NULL, AS $$
created_seq bigint NOT NULL DEFAULT nextval('node_updates'), DECLARE
updated_seq bigint NOT NULL DEFAULT nextval('node_updates'), node_data jsonb;
deleted_seq bigint NULL, node_updated timestamptz;
db_updated timestamptz;
db_uuid bpchar;
db_client bpchar;
db_client_seq int;
node_uuid bpchar;
parent_uuid_nullable bpchar;
"name" varchar(256) DEFAULT ''::character varying NOT NULL, BEGIN
"content" text DEFAULT ''::text NOT NULL, RAISE NOTICE '--------------------------';
content_encrypted text DEFAULT ''::text NOT NULL, FOR node_data IN SELECT * FROM jsonb_array_elements(p_nodes)
markdown bool DEFAULT false NOT NULL, LOOP
node_uuid = (node_data->>'UUID')::bpchar;
node_updated = (node_data->>'Updated')::timestamptz;
CONSTRAINT name_length CHECK ((length(TRIM(BOTH FROM name)) > 0)), IF node_data->>'ParentUUID' = '00000000-0000-0000-0000-000000000000' THEN
CONSTRAINT node_pk PRIMARY KEY (id), parent_uuid_nullable = NULL;
CONSTRAINT user_fk FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE RESTRICT ON UPDATE RESTRICT ELSE
); parent_uuid_nullable = node_data->>'ParentUUID';
CREATE UNIQUE INDEX node_uuid_idx ON public.node USING btree (uuid); END IF;
CREATE INDEX node_search_index ON public.node USING gin (name gin_trgm_ops, content gin_trgm_ops);
CREATE OR REPLACE FUNCTION node_update_timestamp() /* Retrieve the current modified timestamp for this node from the database. */
RETURNS TRIGGER SELECT
LANGUAGE PLPGSQL uuid, updated, client, client_sequence
AS $$ INTO
db_uuid, db_updated, db_client, db_client_seq
FROM public."node"
WHERE
user_id = p_user_id AND
uuid = node_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,
parent_uuid_nullable,
(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;
/* The client could send a specific node again if it didn't receive the OK from this procedure before. */
IF db_updated = node_updated AND db_client = p_client_uuid AND db_client_seq = (node_data->>'ClientSequence')::int THEN
RAISE NOTICE '04, already recorded, %, %', db_client, db_client_seq;
CONTINUE;
END IF;
/* Determine if the incoming node data is to go into history or replace the current node. */
IF db_updated > node_updated THEN
RAISE NOTICE '02 DB newer, % > % (%))', db_updated, node_updated, node_uuid;
/* Incoming node is going straight to history since it is older than the current node. */
INSERT INTO node_history(
user_id, "uuid", parents, created, updated,
"name", "content", markdown, "content_encrypted",
client, client_sequence
)
VALUES(
p_user_id,
node_uuid,
(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 (client, client_sequence)
DO NOTHING;
ELSE
RAISE NOTICE '03 Client newer, % > % (%, %)', node_updated, db_updated, node_uuid, (node_data->>'ClientSequence');
/* Incoming node is newer and will replace the current node.
*
* The current node is copied to the node_history table and then modified in place
* with the incoming data. */
INSERT INTO node_history(
user_id, "uuid", parents,
created, updated, "name", "content", markdown, "content_encrypted",
client, client_sequence
)
SELECT
user_id,
"uuid",
(
WITH RECURSIVE nodes AS (
SELECT
uuid,
COALESCE(parent_uuid, '') AS parent_uuid,
name,
0 AS depth
FROM node
WHERE
uuid = node_uuid
UNION
SELECT
n.uuid,
COALESCE(n.parent_uuid, '') AS parent_uuid,
n.name,
nr.depth+1 AS depth
FROM node n
INNER JOIN nodes nr ON n.uuid = nr.parent_uuid
)
SELECT ARRAY (
SELECT name
FROM nodes
ORDER BY depth DESC
OFFSET 1 /* discard itself */
)
),
created,
updated,
name,
content,
markdown,
content_encrypted,
client,
client_sequence
FROM public."node"
WHERE
user_id = p_user_id AND
uuid = node_uuid
ON CONFLICT (client, client_sequence)
DO NOTHING;
/* Current node in database is updated with incoming data. */
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 = node_uuid;
END IF;
END LOOP;
END
$$;
--
-- Name: node_update_timestamp(); Type: FUNCTION; Schema: public; Owner: postgres
--
CREATE FUNCTION public.node_update_timestamp() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN BEGIN
IF NEW.updated = OLD.updated THEN IF NEW.updated = OLD.updated THEN
UPDATE node UPDATE node
@ -56,6 +226,335 @@ BEGIN
END; END;
$$; $$;
CREATE OR REPLACE TRIGGER node_update AFTER UPDATE ON node
FOR EACH ROW --
EXECUTE PROCEDURE node_update_timestamp(); -- Name: password_hash(character, bytea); Type: FUNCTION; Schema: public; Owner: postgres
--
CREATE FUNCTION public.password_hash(salt_hex character, pass bytea) RETURNS character
LANGUAGE plpgsql
AS $$
BEGIN
RETURN (
SELECT
salt_hex ||
encode(
sha256(
decode(salt_hex, 'hex') || /* salt in binary */
pass /* password */
),
'hex'
)
);
END;
$$;
--
-- Name: client; Type: TABLE; Schema: public; Owner: postgres
--
CREATE TABLE public.client (
id integer NOT NULL,
user_id integer NOT NULL,
client_uuid character(36) DEFAULT ''::bpchar NOT NULL,
created timestamp with time zone DEFAULT now() NOT NULL,
description character varying DEFAULT ''::character varying NOT NULL
);
--
-- Name: client_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
--
CREATE SEQUENCE public.client_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: client_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
--
ALTER SEQUENCE public.client_id_seq OWNED BY public.client.id;
--
-- Name: node_updates; Type: SEQUENCE; Schema: public; Owner: postgres
--
CREATE SEQUENCE public.node_updates
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: node; Type: TABLE; Schema: public; Owner: postgres
--
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),
created timestamp with time zone DEFAULT now() NOT NULL,
updated timestamp with time zone DEFAULT now() NOT NULL,
deleted timestamp with time zone,
created_seq bigint DEFAULT nextval('public.node_updates'::regclass) NOT NULL,
updated_seq bigint DEFAULT nextval('public.node_updates'::regclass) NOT NULL,
deleted_seq bigint,
name character varying(256) DEFAULT ''::character varying NOT NULL,
content text DEFAULT ''::text NOT NULL,
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_sequence integer,
CONSTRAINT name_length CHECK ((length(TRIM(BOTH FROM name)) > 0))
);
--
-- Name: node_history; Type: TABLE; Schema: public; Owner: postgres
--
CREATE TABLE public.node_history (
id integer NOT NULL,
user_id integer NOT NULL,
uuid character(36) NOT NULL,
parents character varying[],
created timestamp with time zone NOT NULL,
updated timestamp with time zone NOT NULL,
name character varying(256) NOT NULL,
content text NOT NULL,
content_encrypted text NOT NULL,
markdown boolean DEFAULT false NOT NULL,
client character(36) DEFAULT ''::bpchar NOT NULL,
client_sequence integer
);
--
-- Name: node_history_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
--
CREATE SEQUENCE public.node_history_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: node_history_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
--
ALTER SEQUENCE public.node_history_id_seq OWNED BY public.node_history.id;
--
-- Name: node_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
--
CREATE SEQUENCE public.node_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: node_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
--
ALTER SEQUENCE public.node_id_seq OWNED BY public.node.id;
--
-- Name: test_data; Type: SEQUENCE; Schema: public; Owner: postgres
--
CREATE SEQUENCE public.test_data
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: user; Type: TABLE; Schema: public; Owner: postgres
--
CREATE TABLE public."user" (
id integer NOT NULL,
username character varying NOT NULL,
name character varying NOT NULL,
password character varying NOT NULL,
last_login timestamp without time zone DEFAULT now() NOT NULL,
timezone character varying DEFAULT 'UTC'::character varying NOT NULL
);
--
-- Name: user_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
--
CREATE SEQUENCE public.user_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: user_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
--
ALTER SEQUENCE public.user_id_seq OWNED BY public."user".id;
--
-- Name: client id; Type: DEFAULT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.client ALTER COLUMN id SET DEFAULT nextval('public.client_id_seq'::regclass);
--
-- Name: node id; Type: DEFAULT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.node ALTER COLUMN id SET DEFAULT nextval('public.node_id_seq'::regclass);
--
-- Name: node_history id; Type: DEFAULT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.node_history ALTER COLUMN id SET DEFAULT nextval('public.node_history_id_seq'::regclass);
--
-- Name: user id; Type: DEFAULT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public."user" ALTER COLUMN id SET DEFAULT nextval('public.user_id_seq'::regclass);
--
-- Name: client client_pk; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.client
ADD CONSTRAINT client_pk PRIMARY KEY (id);
--
-- Name: node_history node_history_pk; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.node_history
ADD CONSTRAINT node_history_pk PRIMARY KEY (id);
--
-- Name: node node_pk; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.node
ADD CONSTRAINT node_pk PRIMARY KEY (id);
--
-- Name: user user_pk; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public."user"
ADD CONSTRAINT user_pk PRIMARY KEY (id);
--
-- Name: client_uuid_idx; Type: INDEX; Schema: public; Owner: postgres
--
CREATE UNIQUE INDEX client_uuid_idx ON public.client USING btree (client_uuid);
--
-- Name: node_history_client_idx; Type: INDEX; Schema: public; Owner: postgres
--
CREATE UNIQUE INDEX node_history_client_idx ON public.node_history USING btree (client, client_sequence);
--
-- Name: node_history_idx; Type: INDEX; Schema: public; Owner: postgres
--
CREATE INDEX node_history_idx ON public.node USING btree (history);
--
-- Name: node_history_uuid_idx; Type: INDEX; Schema: public; Owner: postgres
--
CREATE INDEX node_history_uuid_idx ON public.node USING btree (uuid);
--
-- Name: node_search_index; Type: INDEX; Schema: public; Owner: postgres
--
CREATE INDEX node_search_index ON public.node USING gin (name public.gin_trgm_ops, content public.gin_trgm_ops);
--
-- Name: node_uuid_idx; Type: INDEX; Schema: public; Owner: postgres
--
CREATE UNIQUE INDEX node_uuid_idx ON public.node USING btree (uuid);
--
-- Name: node node_update; Type: TRIGGER; Schema: public; Owner: postgres
--
CREATE TRIGGER node_update AFTER UPDATE ON public.node FOR EACH ROW EXECUTE FUNCTION public.node_update_timestamp();
--
-- Name: node_history node_history_user_fk; Type: FK CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.node_history
ADD CONSTRAINT node_history_user_fk FOREIGN KEY (user_id) REFERENCES public."user"(id) ON UPDATE RESTRICT ON DELETE RESTRICT;
--
-- Name: node node_node_fk; Type: FK CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.node
ADD CONSTRAINT node_node_fk FOREIGN KEY (parent_uuid) REFERENCES public.node(uuid) ON UPDATE SET NULL ON DELETE SET NULL;
--
-- Name: node user_fk; Type: FK CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.node
ADD CONSTRAINT user_fk FOREIGN KEY (user_id) REFERENCES public."user"(id) ON UPDATE RESTRICT ON DELETE RESTRICT;

View file

@ -1,19 +0,0 @@
CREATE FUNCTION public.password_hash(salt_hex char(32), pass bytea)
RETURNS char(96)
LANGUAGE plpgsql
AS
$$
BEGIN
RETURN (
SELECT
salt_hex ||
encode(
sha256(
decode(salt_hex, 'hex') || /* salt in binary */
pass /* password */
),
'hex'
)
);
END;
$$;

View file

@ -1 +0,0 @@
ALTER TABLE public.node ADD CONSTRAINT node_node_fk FOREIGN KEY (parent_uuid) REFERENCES public.node("uuid") ON DELETE SET NULL ON UPDATE SET NULL;

View file

@ -1,2 +0,0 @@
ALTER TABLE public.node ADD COLUMN history bool NOT NULL DEFAULT false;
CREATE INDEX node_history_idx ON public.node (history);

View file

@ -1 +0,0 @@
ALTER TABLE public.node ADD COLUMN client bpchar(36) NOT NULL DEFAULT '';

View file

@ -1 +0,0 @@
DROP INDEX public.node_uuid_idx;

View file

@ -1,162 +0,0 @@
CREATE TYPE json_ancestor_array as ("Ancestors" varchar[]);
CREATE OR REPLACE PROCEDURE add_nodes(p_user_id int4, p_client_uuid varchar, p_nodes jsonb)
LANGUAGE PLPGSQL AS $$
DECLARE
node_data jsonb;
node_updated timestamptz;
db_updated timestamptz;
db_uuid bpchar;
db_client bpchar;
db_client_seq int;
node_uuid bpchar;
BEGIN
RAISE NOTICE '--------------------------';
FOR node_data IN SELECT * FROM jsonb_array_elements(p_nodes)
LOOP
node_uuid = (node_data->>'UUID')::bpchar;
node_updated = (node_data->>'Updated')::timestamptz;
/* 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 = node_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_data->>'ParentUUID')::bpchar,
(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;
/* The client could send a specific node again if it didn't receive the OK from this procedure before. */
IF db_updated = node_updated AND db_client = p_client_uuid AND db_client_seq = (node_data->>'ClientSequence')::int THEN
RAISE NOTICE '04, already recorded, %, %', db_client, db_client_seq;
CONTINUE;
END IF;
/* Determine if the incoming node data is to go into history or replace the current node. */
IF db_updated > node_updated THEN
RAISE NOTICE '02 DB newer, % > % (%))', db_updated, node_updated, node_uuid;
/* Incoming node is going straight to history since it is older than the current node. */
INSERT INTO node_history(
user_id, "uuid", parents, created, updated,
"name", "content", markdown, "content_encrypted",
client, client_sequence
)
VALUES(
p_user_id,
node_uuid,
(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 (client, client_sequence)
DO NOTHING;
ELSE
RAISE NOTICE '03 Client newer, % > % (%, %)', node_updated, db_updated, node_uuid, (node_data->>'ClientSequence');
/* Incoming node is newer and will replace the current node.
*
* The current node is copied to the node_history table and then modified in place
* with the incoming data. */
INSERT INTO node_history(
user_id, "uuid", parents,
created, updated, "name", "content", markdown, "content_encrypted",
client, client_sequence
)
SELECT
user_id,
"uuid",
(
WITH RECURSIVE nodes AS (
SELECT
uuid,
COALESCE(parent_uuid, '') AS parent_uuid,
name,
0 AS depth
FROM node
WHERE
uuid = node_uuid
UNION
SELECT
n.uuid,
COALESCE(n.parent_uuid, '') AS parent_uuid,
n.name,
nr.depth+1 AS depth
FROM node n
INNER JOIN nodes nr ON n.uuid = nr.parent_uuid
)
SELECT ARRAY (
SELECT name
FROM nodes
ORDER BY depth DESC
OFFSET 1 /* discard itself */
)
),
created,
updated,
name,
content,
markdown,
content_encrypted,
client,
client_sequence
FROM public."node"
WHERE
user_id = p_user_id AND
uuid = node_uuid
ON CONFLICT (client, client_sequence)
DO NOTHING;
/* Current node in database is updated with incoming data. */
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 = node_uuid;
END IF;
END LOOP;
END
$$;

View file

@ -1,2 +0,0 @@
ALTER TABLE node ADD COLUMN Client_sequence int NULL;
ALTER TABLE node_history ADD COLUMN Client_sequence int NULL;

View file

@ -1 +0,0 @@
CREATE UNIQUE INDEX node_history_client_idx ON public.node_history (client,client_sequence);

View file

@ -1,10 +0,0 @@
CREATE TABLE public.client (
id serial NOT NULL,
user_id int4 NOT NULL,
client_uuid bpchar(36) DEFAULT '' NOT NULL,
created timestamptz DEFAULT NOW() NOT NULL,
description varchar DEFAULT '' NOT NULL,
CONSTRAINT client_pk PRIMARY KEY (id)
);
CREATE UNIQUE INDEX client_uuid_idx ON public.client (client_uuid);

View file

@ -1,166 +0,0 @@
CREATE OR REPLACE PROCEDURE add_nodes(p_user_id int4, p_client_uuid varchar, p_nodes jsonb)
LANGUAGE PLPGSQL AS $$
DECLARE
node_data jsonb;
node_updated timestamptz;
db_updated timestamptz;
db_uuid bpchar;
db_client bpchar;
db_client_seq int;
node_uuid bpchar;
parent_uuid bpchar;
BEGIN
RAISE NOTICE '--------------------------';
FOR node_data IN SELECT * FROM jsonb_array_elements(p_nodes)
LOOP
node_uuid = (node_data->>'UUID')::bpchar;
node_updated = (node_data->>'Updated')::timestamptz;
IF node_data->>'ParentUUID' = '00000000-0000-0000-0000-000000000000' THEN
parent_uuid = NULL;
ELSE
parent_uuid = node_data->>'ParentUUID';
END IF;
/* 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 = node_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,
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;
/* The client could send a specific node again if it didn't receive the OK from this procedure before. */
IF db_updated = node_updated AND db_client = p_client_uuid AND db_client_seq = (node_data->>'ClientSequence')::int THEN
RAISE NOTICE '04, already recorded, %, %', db_client, db_client_seq;
CONTINUE;
END IF;
/* Determine if the incoming node data is to go into history or replace the current node. */
IF db_updated > node_updated THEN
RAISE NOTICE '02 DB newer, % > % (%))', db_updated, node_updated, node_uuid;
/* Incoming node is going straight to history since it is older than the current node. */
INSERT INTO node_history(
user_id, "uuid", parents, created, updated,
"name", "content", markdown, "content_encrypted",
client, client_sequence
)
VALUES(
p_user_id,
node_uuid,
(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 (client, client_sequence)
DO NOTHING;
ELSE
RAISE NOTICE '03 Client newer, % > % (%, %)', node_updated, db_updated, node_uuid, (node_data->>'ClientSequence');
/* Incoming node is newer and will replace the current node.
*
* The current node is copied to the node_history table and then modified in place
* with the incoming data. */
INSERT INTO node_history(
user_id, "uuid", parents,
created, updated, "name", "content", markdown, "content_encrypted",
client, client_sequence
)
SELECT
user_id,
"uuid",
(
WITH RECURSIVE nodes AS (
SELECT
uuid,
COALESCE(parent_uuid, '') AS parent_uuid,
name,
0 AS depth
FROM node
WHERE
uuid = node_uuid
UNION
SELECT
n.uuid,
COALESCE(n.parent_uuid, '') AS parent_uuid,
n.name,
nr.depth+1 AS depth
FROM node n
INNER JOIN nodes nr ON n.uuid = nr.parent_uuid
)
SELECT ARRAY (
SELECT name
FROM nodes
ORDER BY depth DESC
OFFSET 1 /* discard itself */
)
),
created,
updated,
name,
content,
markdown,
content_encrypted,
client,
client_sequence
FROM public."node"
WHERE
user_id = p_user_id AND
uuid = node_uuid
ON CONFLICT (client, client_sequence)
DO NOTHING;
/* Current node in database is updated with incoming data. */
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 = node_uuid;
END IF;
END LOOP;
END
$$;

View file

@ -1,166 +0,0 @@
CREATE OR REPLACE PROCEDURE add_nodes(p_user_id int4, p_client_uuid varchar, p_nodes jsonb)
LANGUAGE PLPGSQL AS $$
DECLARE
node_data jsonb;
node_updated timestamptz;
db_updated timestamptz;
db_uuid bpchar;
db_client bpchar;
db_client_seq int;
node_uuid bpchar;
parent_uuid_nullable bpchar;
BEGIN
RAISE NOTICE '--------------------------';
FOR node_data IN SELECT * FROM jsonb_array_elements(p_nodes)
LOOP
node_uuid = (node_data->>'UUID')::bpchar;
node_updated = (node_data->>'Updated')::timestamptz;
IF node_data->>'ParentUUID' = '00000000-0000-0000-0000-000000000000' THEN
parent_uuid_nullable = NULL;
ELSE
parent_uuid_nullable = node_data->>'ParentUUID';
END IF;
/* 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 = node_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,
parent_uuid_nullable,
(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;
/* The client could send a specific node again if it didn't receive the OK from this procedure before. */
IF db_updated = node_updated AND db_client = p_client_uuid AND db_client_seq = (node_data->>'ClientSequence')::int THEN
RAISE NOTICE '04, already recorded, %, %', db_client, db_client_seq;
CONTINUE;
END IF;
/* Determine if the incoming node data is to go into history or replace the current node. */
IF db_updated > node_updated THEN
RAISE NOTICE '02 DB newer, % > % (%))', db_updated, node_updated, node_uuid;
/* Incoming node is going straight to history since it is older than the current node. */
INSERT INTO node_history(
user_id, "uuid", parents, created, updated,
"name", "content", markdown, "content_encrypted",
client, client_sequence
)
VALUES(
p_user_id,
node_uuid,
(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 (client, client_sequence)
DO NOTHING;
ELSE
RAISE NOTICE '03 Client newer, % > % (%, %)', node_updated, db_updated, node_uuid, (node_data->>'ClientSequence');
/* Incoming node is newer and will replace the current node.
*
* The current node is copied to the node_history table and then modified in place
* with the incoming data. */
INSERT INTO node_history(
user_id, "uuid", parents,
created, updated, "name", "content", markdown, "content_encrypted",
client, client_sequence
)
SELECT
user_id,
"uuid",
(
WITH RECURSIVE nodes AS (
SELECT
uuid,
COALESCE(parent_uuid, '') AS parent_uuid,
name,
0 AS depth
FROM node
WHERE
uuid = node_uuid
UNION
SELECT
n.uuid,
COALESCE(n.parent_uuid, '') AS parent_uuid,
n.name,
nr.depth+1 AS depth
FROM node n
INNER JOIN nodes nr ON n.uuid = nr.parent_uuid
)
SELECT ARRAY (
SELECT name
FROM nodes
ORDER BY depth DESC
OFFSET 1 /* discard itself */
)
),
created,
updated,
name,
content,
markdown,
content_encrypted,
client,
client_sequence
FROM public."node"
WHERE
user_id = p_user_id AND
uuid = node_uuid
ON CONFLICT (client, client_sequence)
DO NOTHING;
/* Current node in database is updated with incoming data. */
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 = node_uuid;
END IF;
END LOOP;
END
$$;

View file

@ -48,7 +48,7 @@ export class N2NodeUI extends CustomHTMLElement {
_mbus.subscribe('NODE_MODIFIED', () => { _mbus.subscribe('NODE_MODIFIED', () => {
document.querySelector('#crumbs .crumbs')?.classList.add('node-modified') document.querySelector('#crumbs .crumbs')?.classList.add('node-modified')
this.elIconSave.src = `/images/${_VERSION}/icon_save.svg` this.elIconSave.src = `/images/${_VERSION}/icon_save.svg`
this.render() this.renderName()
}) })
_mbus.subscribe('NODE_UNMODIFIED', () => { _mbus.subscribe('NODE_UNMODIFIED', () => {
@ -76,6 +76,9 @@ export class N2NodeUI extends CustomHTMLElement {
this.showMarkdown(true) this.showMarkdown(true)
}// }}} }// }}}
renderName() {// {{{
this.elName.innerText = this.node?.get('Name') ?? ''
}// }}}
render() {// {{{ render() {// {{{
this.elName.innerText = this.node?.get('Name') ?? '' this.elName.innerText = this.node?.get('Name') ?? ''
this.elNodeContent.value = this.node?.get('Content') ?? '' this.elNodeContent.value = this.node?.get('Content') ?? ''