diff --git a/backend/resources/migrations/0003.projects.sql b/backend/resources/migrations/0003.projects.sql index 42808f872..98514dbf1 100644 --- a/backend/resources/migrations/0003.projects.sql +++ b/backend/resources/migrations/0003.projects.sql @@ -68,7 +68,7 @@ CREATE TABLE IF NOT EXISTS project_pages ( metadata bytea NULL DEFAULT NULL ); -CREATE TABLE IF NOT EXISTS project_page_history ( +CREATE TABLE IF NOT EXISTS project_page_snapshots ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), user_id uuid NULL REFERENCES users(id) ON DELETE SET NULL, @@ -81,7 +81,8 @@ CREATE TABLE IF NOT EXISTS project_page_history ( pinned bool NOT NULL DEFAULT false, label text NOT NULL DEFAULT '', - data bytea NOT NULL + data bytea NOT NULL, + operations bytea NULL DEFAULT NULL ); -- Indexes @@ -94,8 +95,8 @@ CREATE INDEX project_files__project_id__idx ON project_files(project_id); CREATE INDEX project_pages__user_id__idx ON project_pages(user_id); CREATE INDEX project_pages__file_id__idx ON project_pages(file_id); -CREATE INDEX project_page_history__page_id__idx ON project_page_history(page_id); -CREATE INDEX project_page_history__user_id__idx ON project_page_history(user_id); +CREATE INDEX project_page_snapshots__page_id__idx ON project_page_snapshots(page_id); +CREATE INDEX project_page_snapshots__user_id__idx ON project_page_snapshots(user_id); -- Triggers @@ -152,6 +153,6 @@ CREATE TRIGGER project_pages__modified_at__tgr BEFORE UPDATE ON project_pages FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); -CREATE TRIGGER project_page_history__modified_at__tgr -BEFORE UPDATE ON project_page_history +CREATE TRIGGER project_page_snapshots__modified_at__tgr +BEFORE UPDATE ON project_page_snapshots FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); diff --git a/backend/src/uxbox/services/mutations/project_pages.clj b/backend/src/uxbox/services/mutations/project_pages.clj index 820815ae0..5080383f7 100644 --- a/backend/src/uxbox/services/mutations/project_pages.clj +++ b/backend/src/uxbox/services/mutations/project_pages.clj @@ -56,11 +56,11 @@ (-> (db/query-one conn [sql id user file-id name ordering data mdata]) (p/then' decode-row)))) -;; --- Mutation: Update Page +;; --- Mutation: Update Page Data (declare select-page-for-update) -(declare update-page) -(declare update-history) +(declare update-page-data) +(declare insert-page-snapshot) (s/def ::update-project-page-data (s/keys :req-un [::id ::user ::data])) @@ -73,13 +73,13 @@ (let [data (blob/encode data) version (inc version) params (assoc params :id id :data data :version version)] - (p/do! (update-page conn params) - (update-history conn params) + (p/do! (update-page-data conn params) + (insert-page-snapshot conn params) (select-keys params [:id :version])))))) (defn- select-page-for-update [conn id] - (let [sql "select p.id, p.version, p.file_id + (let [sql "select p.id, p.version, p.file_id, p.data from project_pages as p where p.id = $1 and deleted_at is null @@ -87,8 +87,8 @@ (-> (db/query-one conn [sql id]) (p/then' su/raise-not-found-if-nil)))) -(defn- update-page - [conn {:keys [id name version data metadata]}] +(defn- update-page-data + [conn {:keys [id name version data]}] (let [sql "update project_pages set version = $1, data = $2 @@ -96,12 +96,12 @@ (-> (db/query-one conn [sql version data id]) (p/then' su/constantly-nil)))) -(defn- update-history - [conn {:keys [user id version data]}] - (let [sql "insert into project_page_history (user_id, page_id, version, data) - values ($1, $2, $3, $4)"] - (-> (db/query-one conn [sql user id version data]) - (p/then' su/constantly-nil)))) +(defn- insert-page-snapshot + [conn {:keys [user id version data operations]}] + (let [sql "insert into project_page_snapshots (user_id, page_id, version, data, operations) + values ($1, $2, $3, $4, $5) + returning id, version, operations"] + (db/query-one conn [sql user id version data operations]))) ;; --- Mutation: Rename Page @@ -126,23 +126,60 @@ (-> (db/query-one db/pool [sql id name]) (p/then su/constantly-nil)))) -;; --- Mutation: Update Page Metadata +;; --- Mutation: Update Page -;; (s/def ::update-page-metadata -;; (s/keys :req-un [::user ::project-id ::name ::metadata ::id])) +;; A generic, Ops based (granular) page update method. -;; (sm/defmutation ::update-page-metadata -;; [{:keys [id user project-id name metadata]}] -;; (let [sql "update pages -;; set name = $3, -;; metadata = $4 -;; where id = $1 -;; and user_id = $2 -;; and deleted_at is null -;; returning *" -;; mdata (blob/encode metadata)] -;; (-> (db/query-one db/pool [sql id user name mdata]) -;; (p/then' decode-row)))) +(s/def ::operations + (s/coll-of ::cp/opeation :kind vector?)) + +(s/def ::update-project-page + (s/keys :opt-un [::id ::user ::version ::operations])) + +(declare update-project-page) +(declare retrieve-lagged-operations) + +(sm/defmutation ::update-project-page + [{:keys [id user] :as params}] + (db/with-atomic [conn db/pool] + (p/let [{:keys [file-id] :as page} (select-page-for-update conn id)] + (files/check-edition-permissions! conn user file-id) + (update-project-page conn page params)))) + +(defn- update-project-page + [conn page params] + (when (> (:version page) + (:version params)) + (ex/raise :type :validation + :code :version-conflict + :hint "The incoming version is greater that stored version." + :context {:incoming-version (:version params) + :stored-version (:version page)})) + (let [ops (:operations params) + data (-> (:data page) + (blob/decode) + (cp/process-ops ops) + (blob/encode)) + page (assoc page + :data data + :version (inc (:version page)) + :operations (blob/encode ops))] + (-> (update-page-data conn page) + (p/then (fn [_] (insert-page-snapshot conn page))) + (p/then (fn [s] (retrieve-lagged-operations conn s params)))))) + +(su/defstr sql:lagged-snapshots + "select s.id, s.version, s.operations, + s.created_at, s.modified_at, s.user_id + from project_page_snapshots as s + where s.page_id = $1 + and s.version > $2") + +(defn- retrieve-lagged-operations + [conn snapshot params] + (let [sql sql:lagged-snapshots] + (-> (db/query conn [sql (:id params) (:version params)]) + (p/then (partial mapv decode-row))))) ;; --- Mutation: Delete Page @@ -158,12 +195,15 @@ (files/check-edition-permissions! conn user (:file-id page)) (delete-page conn id)))) +(su/defstr sql:delete-page + "update project_pages + set deleted_at = clock_timestamp() + where id = $1 + and deleted_at is null") + (defn- delete-page [conn id] - (let [sql "update project_pages - set deleted_at = clock_timestamp() - where id = $1 - and deleted_at is null"] + (let [sql sql:delete-page] (-> (db/query-one conn [sql id]) (p/then su/constantly-nil)))) @@ -178,7 +218,7 @@ ;; (some-> (db/fetch-one conn sqlv) ;; (decode-row)))) -;; (s/def ::label ::us/string) +;; (s/def ::label ::cs/string) ;; (s/def ::update-page-history ;; (s/keys :req-un [::user ::id ::pinned ::label])) diff --git a/backend/src/uxbox/services/queries/project_pages.clj b/backend/src/uxbox/services/queries/project_pages.clj index 7a5375008..2411edb98 100644 --- a/backend/src/uxbox/services/queries/project_pages.clj +++ b/backend/src/uxbox/services/queries/project_pages.clj @@ -139,8 +139,9 @@ ;; --- Helpers (defn decode-row - [{:keys [data metadata] :as row}] + [{:keys [data metadata operations] :as row}] (when row (cond-> row data (assoc :data (blob/decode data)) - metadata (assoc :metadata (blob/decode metadata))))) + metadata (assoc :metadata (blob/decode metadata)) + operations (assoc :operations (blob/decode operations))))) diff --git a/backend/test/uxbox/tests/test_services_project_pages.clj b/backend/test/uxbox/tests/test_services_project_pages.clj index 3133c7824..2a1feacb8 100644 --- a/backend/test/uxbox/tests/test_services_project_pages.clj +++ b/backend/test/uxbox/tests/test_services_project_pages.clj @@ -7,6 +7,7 @@ [uxbox.http :as http] [uxbox.services.mutations :as sm] [uxbox.services.queries :as sq] + [uxbox.util.uuid :as uuid] [uxbox.tests.helpers :as th])) (t/use-fixtures :once th/state-init) @@ -34,7 +35,9 @@ pf @(th/create-project-file db/pool (:id user) (:id proj) 1) data {::sm/type :create-project-page - :data {} + :data {:canvas [] + :shapes [] + :shapes-by-id {}} :metadata {} :file-id (:id pf) :ordering 1 @@ -57,7 +60,9 @@ page @(th/create-project-page db/pool (:id user) (:id file) 1) data {::sm/type :update-project-page-data :id (:id page) - :data {:shapes [1 2 3]} + :data {:shapes [(uuid/next)] + :canvas [] + :shapes-by-id {}} :file-id (:id file) :user (:id user)} out (th/try-on! (sm/handle data))] diff --git a/frontend/src/uxbox/main/data/pages.cljs b/frontend/src/uxbox/main/data/pages.cljs index ab7a7de7e..f323b70d3 100644 --- a/frontend/src/uxbox/main/data/pages.cljs +++ b/frontend/src/uxbox/main/data/pages.cljs @@ -82,6 +82,9 @@ "A marker protocol for mark events that alters the page and is subject to perform a backend synchronization.") +(defprotocol IPageOps + (-ops [_] "Get a list of ops for the event.")) + (defn page-update? [o] (or (satisfies? IPageDataUpdate o)