From 183f0a5400d74276c86f4153779e83ace0297a7b Mon Sep 17 00:00:00 2001 From: Andrey Antukh <niwi@niwi.nz> Date: Mon, 9 Dec 2019 16:27:01 +0100 Subject: [PATCH] :recycle: Refactor services (for add the project-file concept. And fix many tests. --- backend/resources/migrations/0002.users.sql | 23 +- .../resources/migrations/0003.projects.sql | 125 ++++++++++- .../{0005.emails.sql => 0004.emails.sql} | 0 backend/resources/migrations/0004.pages.sql | 65 ------ .../{0006.images.sql => 0005.images.sql} | 23 +- .../{0007.icons.sql => 0006.icons.sql} | 26 +-- backend/src/uxbox/config.clj | 2 +- backend/src/uxbox/db.clj | 20 +- backend/src/uxbox/fixtures.clj | 157 ++++++++----- backend/src/uxbox/http/errors.clj | 20 +- backend/src/uxbox/migrations.clj | 15 +- backend/src/uxbox/services/init.clj | 16 +- backend/src/uxbox/services/mutations.clj | 4 +- .../src/uxbox/services/mutations/pages.clj | 174 -------------- .../services/mutations/project_files.clj | 142 ++++++++++++ .../services/mutations/project_pages.clj | 190 ++++++++++++++++ .../src/uxbox/services/mutations/projects.clj | 67 ++++-- .../{user_storage.clj => user_attrs.clj} | 16 +- .../mutations/{profiles.clj => users.clj} | 25 +-- backend/src/uxbox/services/queries.clj | 1 + backend/src/uxbox/services/queries/pages.clj | 92 -------- .../uxbox/services/queries/project_files.clj | 55 +++++ .../uxbox/services/queries/project_pages.clj | 145 ++++++++++++ .../src/uxbox/services/queries/projects.clj | 44 ++-- .../{user_storage.clj => user_attrs.clj} | 8 +- .../queries/{profiles.clj => users.clj} | 6 +- backend/src/uxbox/util/dispatcher.clj | 17 +- backend/src/uxbox/util/exceptions.clj | 2 +- backend/src/uxbox/util/http.clj | 212 +----------------- backend/src/uxbox/util/sql.clj | 6 +- backend/test/uxbox/tests/helpers.clj | 70 +++--- backend/test/uxbox/tests/test_emails.clj | 28 +-- .../test/uxbox/tests/test_services_auth.clj | 11 +- .../test/uxbox/tests/test_services_pages.clj | 144 ------------ .../tests/test_services_project_files.clj | 76 +++++++ .../tests/test_services_project_pages.clj | 84 +++++++ .../uxbox/tests/test_services_projects.clj | 40 ++-- ...orage.clj => test_services_user_attrs.clj} | 33 ++- ...test_users.clj => test_services_users.clj} | 76 +++---- backend/test/uxbox/tests/test_util_svg.clj | 25 ++- 40 files changed, 1279 insertions(+), 1006 deletions(-) rename backend/resources/migrations/{0005.emails.sql => 0004.emails.sql} (100%) delete mode 100644 backend/resources/migrations/0004.pages.sql rename backend/resources/migrations/{0006.images.sql => 0005.images.sql} (64%) rename backend/resources/migrations/{0007.icons.sql => 0006.icons.sql} (59%) delete mode 100644 backend/src/uxbox/services/mutations/pages.clj create mode 100644 backend/src/uxbox/services/mutations/project_files.clj create mode 100644 backend/src/uxbox/services/mutations/project_pages.clj rename backend/src/uxbox/services/mutations/{user_storage.clj => user_attrs.clj} (75%) rename backend/src/uxbox/services/mutations/{profiles.clj => users.clj} (94%) delete mode 100644 backend/src/uxbox/services/queries/pages.clj create mode 100644 backend/src/uxbox/services/queries/project_files.clj create mode 100644 backend/src/uxbox/services/queries/project_pages.clj rename backend/src/uxbox/services/queries/{user_storage.clj => user_attrs.clj} (85%) rename backend/src/uxbox/services/queries/{profiles.clj => users.clj} (94%) delete mode 100644 backend/test/uxbox/tests/test_services_pages.clj create mode 100644 backend/test/uxbox/tests/test_services_project_files.clj create mode 100644 backend/test/uxbox/tests/test_services_project_pages.clj rename backend/test/uxbox/tests/{test_services_user_storage.clj => test_services_user_attrs.clj} (62%) rename backend/test/uxbox/tests/{test_users.clj => test_services_users.clj} (65%) diff --git a/backend/resources/migrations/0002.users.sql b/backend/resources/migrations/0002.users.sql index 8833f7821..cbf4c7d11 100644 --- a/backend/resources/migrations/0002.users.sql +++ b/backend/resources/migrations/0002.users.sql @@ -10,10 +10,11 @@ CREATE TABLE users ( email text NOT NULL, photo text NOT NULL, password text NOT NULL, - metadata bytea NOT NULL + + metadata bytea NULL DEFAULT NULL ); -CREATE TABLE IF NOT EXISTS user_storage ( +CREATE TABLE IF NOT EXISTS user_attrs ( user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, created_at timestamptz NOT NULL DEFAULT clock_timestamp(), @@ -25,7 +26,7 @@ CREATE TABLE IF NOT EXISTS user_storage ( PRIMARY KEY (key, user_id) ); -CREATE TABLE user_tokens ( +CREATE TABLE IF NOT EXISTS tokens ( user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, token text NOT NULL, @@ -35,7 +36,7 @@ CREATE TABLE user_tokens ( PRIMARY KEY (token, user_id) ); -CREATE TABLE sessions ( +CREATE TABLE IF NOT EXISTS sessions ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), created_at timestamptz NOT NULL DEFAULT clock_timestamp(), @@ -56,17 +57,19 @@ VALUES ('00000000-0000-0000-0000-000000000000'::uuid, '!', '{}'); -CREATE UNIQUE INDEX users_username_idx - ON users USING btree (username) +CREATE UNIQUE INDEX users__username__idx + ON users (username) WHERE deleted_at is null; -CREATE UNIQUE INDEX users_email_idx - ON users USING btree (email) +CREATE UNIQUE INDEX users__email__idx + ON users (email) WHERE deleted_at is null; -CREATE TRIGGER users_modified_at_tgr BEFORE UPDATE ON users +CREATE TRIGGER users__modified_at__tgr +BEFORE UPDATE ON users FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); -CREATE TRIGGER user_storage_modified_at_tgr BEFORE UPDATE ON user_storage +CREATE TRIGGER user_attrs__modified_at__tgr +BEFORE UPDATE ON user_attrs FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); diff --git a/backend/resources/migrations/0003.projects.sql b/backend/resources/migrations/0003.projects.sql index 561d3c130..42808f872 100644 --- a/backend/resources/migrations/0003.projects.sql +++ b/backend/resources/migrations/0003.projects.sql @@ -8,43 +8,150 @@ CREATE TABLE IF NOT EXISTS projects ( modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), deleted_at timestamptz DEFAULT NULL, - name text NOT NULL + name text NOT NULL, + metadata bytea NULL DEFAULT NULL ); -CREATE TABLE IF NOT EXISTS projects_users ( +CREATE TABLE IF NOT EXISTS project_users ( user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE, created_at timestamptz NOT NULL DEFAULT clock_timestamp(), modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), - role text NOT NULL, + can_edit boolean DEFAULT false, PRIMARY KEY (user_id, project_id) ); +CREATE TABLE IF NOT EXISTS project_files ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE, + + name text NOT NULL, + + created_at timestamptz NOT NULL DEFAULT clock_timestamp(), + modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), + deleted_at timestamptz DEFAULT NULL, + + metadata bytea NULL DEFAULT NULL +); + +CREATE TABLE IF NOT EXISTS project_file_users ( + file_id uuid NOT NULL REFERENCES project_files(id) ON DELETE CASCADE, + user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + + created_at timestamptz NOT NULL DEFAULT clock_timestamp(), + modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), + + can_edit boolean DEFAULT false, + + PRIMARY KEY (user_id, file_id) +); + +CREATE TABLE IF NOT EXISTS project_pages ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + + user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + file_id uuid NOT NULL REFERENCES project_files(id) ON DELETE CASCADE, + + created_at timestamptz NOT NULL DEFAULT clock_timestamp(), + modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), + deleted_at timestamptz DEFAULT NULL, + + version bigint NOT NULL, + ordering smallint NOT NULL, + + name text NOT NULL, + data bytea NOT NULL, + metadata bytea NULL DEFAULT NULL +); + +CREATE TABLE IF NOT EXISTS project_page_history ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + + user_id uuid NULL REFERENCES users(id) ON DELETE SET NULL, + page_id uuid NOT NULL REFERENCES project_pages(id) ON DELETE CASCADE, + + created_at timestamptz NOT NULL DEFAULT clock_timestamp(), + modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), + version bigint NOT NULL DEFAULT 0, + + pinned bool NOT NULL DEFAULT false, + label text NOT NULL DEFAULT '', + + data bytea NOT NULL +); + -- Indexes -CREATE INDEX projects_user_idx ON projects(user_id); -CREATE INDEX projects_users_user_id_idx ON projects_users(project_id); -CREATE INDEX projects_users_project_id_idx ON projects_users(user_id); +CREATE INDEX projects__user_id__idx ON projects(user_id); + +CREATE INDEX project_files__user_id__idx ON project_files(user_id); +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); -- Triggers CREATE OR REPLACE FUNCTION handle_project_insert() RETURNS TRIGGER AS $$ BEGIN - INSERT INTO projects_users (user_id, project_id, role) - VALUES (NEW.user_id, NEW.id, 'owner'); + INSERT INTO project_users (user_id, project_id, can_edit) + VALUES (NEW.user_id, NEW.id, true); RETURN NEW; END; $$ LANGUAGE plpgsql; +CREATE OR REPLACE FUNCTION handle_page_update() + RETURNS TRIGGER AS $pagechange$ + DECLARE + current_dt timestamptz := clock_timestamp(); + proj_id uuid; + BEGIN + UPDATE project_files + SET modified_at = current_dt + WHERE id = OLD.file_id + RETURNING project_id + INTO STRICT proj_id; + + --- Update projects modified_at attribute when a + --- page of that project is modified. + UPDATE projects + SET modified_at = current_dt + WHERE id = proj_id; + + RETURN NEW; + END; +$pagechange$ LANGUAGE plpgsql; + CREATE TRIGGER projects_on_insert_tgr AFTER INSERT ON projects FOR EACH ROW EXECUTE PROCEDURE handle_project_insert(); -CREATE TRIGGER projects_modified_at_tgr +CREATE TRIGGER pages__on_update__tgr +BEFORE UPDATE ON project_pages + FOR EACH ROW EXECUTE PROCEDURE handle_page_update(); + + +CREATE TRIGGER projects__modified_at__tgr BEFORE UPDATE ON projects FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); + +CREATE TRIGGER project_files__modified_at__tgr +BEFORE UPDATE ON project_files + FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); + +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 + FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); diff --git a/backend/resources/migrations/0005.emails.sql b/backend/resources/migrations/0004.emails.sql similarity index 100% rename from backend/resources/migrations/0005.emails.sql rename to backend/resources/migrations/0004.emails.sql diff --git a/backend/resources/migrations/0004.pages.sql b/backend/resources/migrations/0004.pages.sql deleted file mode 100644 index a867a700c..000000000 --- a/backend/resources/migrations/0004.pages.sql +++ /dev/null @@ -1,65 +0,0 @@ --- Tables - -CREATE TABLE IF NOT EXISTS pages ( - id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), - - user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, - project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE, - - created_at timestamptz NOT NULL DEFAULT clock_timestamp(), - modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), - deleted_at timestamptz DEFAULT NULL, - - version bigint NOT NULL, - ordering smallint NOT NULL, - - name text NOT NULL, - data bytea NOT NULL, - metadata bytea NOT NULL -); - -CREATE TABLE IF NOT EXISTS pages_history ( - id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), - - user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, - page_id uuid NOT NULL REFERENCES pages(id) ON DELETE CASCADE, - - created_at timestamptz NOT NULL DEFAULT clock_timestamp(), - modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), - version bigint NOT NULL DEFAULT 0, - - pinned bool NOT NULL DEFAULT false, - label text NOT NULL DEFAULT '', - data bytea NOT NULL, - metadata bytea NOT NULL -); - --- Indexes - -CREATE INDEX pages_project_idx ON pages(project_id); -CREATE INDEX pages_user_idx ON pages(user_id); -CREATE INDEX pages_history_page_idx ON pages_history(page_id); -CREATE INDEX pages_history_user_idx ON pages_history(user_id); - --- Triggers - -CREATE OR REPLACE FUNCTION handle_page_update() - RETURNS TRIGGER AS $pagechange$ - BEGIN - --- Update projects modified_at attribute when a - --- page of that project is modified. - UPDATE projects SET modified_at = clock_timestamp() - WHERE id = OLD.project_id; - - RETURN NEW; - END; -$pagechange$ LANGUAGE plpgsql; - -CREATE TRIGGER page_on_update_tgr BEFORE UPDATE ON pages - FOR EACH ROW EXECUTE PROCEDURE handle_page_update(); - -CREATE TRIGGER pages_modified_at_tgr BEFORE UPDATE ON pages - FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); - -CREATE TRIGGER pages_history_modified_at_tgr BEFORE UPDATE ON pages - FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); diff --git a/backend/resources/migrations/0006.images.sql b/backend/resources/migrations/0005.images.sql similarity index 64% rename from backend/resources/migrations/0006.images.sql rename to backend/resources/migrations/0005.images.sql index ca6dea90f..b97c91004 100644 --- a/backend/resources/migrations/0006.images.sql +++ b/backend/resources/migrations/0005.images.sql @@ -1,6 +1,6 @@ -- Tables -CREATE TABLE IF NOT EXISTS images_collections ( +CREATE TABLE IF NOT EXISTS image_collections ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, @@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS images ( width int NOT NULL, height int NOT NULL, mimetype text NOT NULL, - collection_id uuid REFERENCES images_collections(id) + collection_id uuid REFERENCES image_collections(id) ON DELETE SET NULL DEFAULT NULL, name text NOT NULL, @@ -31,20 +31,17 @@ CREATE TABLE IF NOT EXISTS images ( -- Indexes -CREATE INDEX images_collections_user_idx - ON images_collections (user_id); - -CREATE INDEX images_collection_idx - ON images (collection_id); - -CREATE INDEX images_user_idx - ON images (user_id); +CREATE INDEX image_collections__user_id__idx ON image_collections (user_id); +CREATE INDEX images__collection_id__idx ON images (collection_id); +CREATE INDEX images__user_id__idx ON images (user_id); -- Triggers -CREATE TRIGGER images_collections_modified_at_tgr BEFORE UPDATE ON images_collections +CREATE TRIGGER image_collections__modified_at__tgr +BEFORE UPDATE ON image_collections FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); -CREATE TRIGGER images_modified_at_tgr BEFORE UPDATE ON images - FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); +CREATE TRIGGER images__modified_at__tgr +BEFORE UPDATE ON images + FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); diff --git a/backend/resources/migrations/0007.icons.sql b/backend/resources/migrations/0006.icons.sql similarity index 59% rename from backend/resources/migrations/0007.icons.sql rename to backend/resources/migrations/0006.icons.sql index 12bbbf5e0..476ce70b8 100644 --- a/backend/resources/migrations/0007.icons.sql +++ b/backend/resources/migrations/0006.icons.sql @@ -1,6 +1,6 @@ -- Tables -CREATE TABLE IF NOT EXISTS icons_collections ( +CREATE TABLE IF NOT EXISTS icon_collections ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, @@ -23,27 +23,23 @@ CREATE TABLE IF NOT EXISTS icons ( content text NOT NULL, metadata bytea NOT NULL, - collection_id uuid REFERENCES icons_collections(id) + collection_id uuid REFERENCES icon_collections(id) ON DELETE SET NULL DEFAULT NULL ); -- Indexes -CREATE INDEX icon_colections_user_idx - ON icons_collections (user_id); - -CREATE INDEX icons_user_idx - ON icons (user_id); - -CREATE INDEX icons_collection_idx - ON icons (collection_id); +CREATE INDEX icon_colections__user_id__idx ON icon_collections (user_id); +CREATE INDEX icons__user_id__idx ON icons(user_id); +CREATE INDEX icons__collection_id__idx ON icons(collection_id); -- Triggers -CREATE TRIGGER icons_collections_modified_at_tgr BEFORE UPDATE ON icons_collections - FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); - -CREATE TRIGGER icons_modified_at_tgr BEFORE UPDATE ON icons - FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); +CREATE TRIGGER icon_collections__modified_at__tgr +BEFORE UPDATE ON icon_collections + FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); +CREATE TRIGGER icons__modified_at__tgr +BEFORE UPDATE ON icons + FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); diff --git a/backend/src/uxbox/config.clj b/backend/src/uxbox/config.clj index 20f8eb01d..1133e319f 100644 --- a/backend/src/uxbox/config.clj +++ b/backend/src/uxbox/config.clj @@ -50,7 +50,7 @@ :email-reply-to (lookup-env env :uxbox-email-reply-to "no-reply@uxbox.io") :email-from (lookup-env env :uxbox-email-from "no-reply@uxbox.io") - :smtp-host (lookup-env env :uxbox-smtp-host "smtp") + :smtp-host (lookup-env env :uxbox-smtp-host "localhost") :smtp-port (lookup-env env :uxbox-smtp-port 25) :smtp-user (lookup-env env :uxbox-smtp-user nil) :smtp-password (lookup-env env :uxbox-smtp-password nil) diff --git a/backend/src/uxbox/db.clj b/backend/src/uxbox/db.clj index a421437e1..84d92e026 100644 --- a/backend/src/uxbox/db.clj +++ b/backend/src/uxbox/db.clj @@ -13,6 +13,7 @@ [uxbox.config :as cfg] [uxbox.core :refer [system]] [uxbox.util.data :as data] + [uxbox.util.exceptions :as ex] [uxbox.util.pgsql :as pg] [vertx.core :as vx]) (:import io.vertx.core.buffer.Buffer)) @@ -33,17 +34,22 @@ :start (create-pool cfg/config system)) (defmacro with-atomic - [& args] - `(pg/with-atomic ~@args)) + [bindings & args] + `(pg/with-atomic ~bindings (p/do! ~@args))) (def row-xfm (comp (map pg/row->map) (map data/normalize-attrs))) (defmacro query - [& args] - `(pg/query ~@args {:xfm row-xfm})) - + [conn sql] + `(-> (pg/query ~conn ~sql {:xfm row-xfm}) + (p/catch' (fn [err#] + (ex/raise :type :database-error + :cause err#))))) (defmacro query-one - [& args] - `(pg/query-one ~@args {:xfm row-xfm})) + [conn sql] + `(-> (pg/query-one ~conn ~sql {:xfm row-xfm}) + (p/catch' (fn [err#] + (ex/raise :type :database-error + :cause err#))))) diff --git a/backend/src/uxbox/fixtures.clj b/backend/src/uxbox/fixtures.clj index 857dc2712..1d7871059 100644 --- a/backend/src/uxbox/fixtures.clj +++ b/backend/src/uxbox/fixtures.clj @@ -7,6 +7,7 @@ (ns uxbox.fixtures "A initial fixtures." (:require + [clojure.tools.logging :as log] [buddy.hashers :as hashers] [mount.core :as mount] [promesa.core :as p] @@ -20,26 +21,27 @@ (defn- mk-uuid [prefix & args] - (uuid/namespaced uuid/oid (apply str prefix args))) + (uuid/namespaced uuid/oid (apply str prefix (interpose "-" args)))) ;; --- Users creation (def create-user-sql - "insert into users (id, fullname, username, email, password, metadata, photo) - values ($1, $2, $3, $4, $5, $6, $7) + "insert into users (id, fullname, username, email, password, photo) + values ($1, $2, $3, $4, $5, $6) returning *;") +(def password (hashers/encrypt "123123")) + (defn create-user - [conn i] - (println "create user" i) - (db/query-one conn [create-user-sql - (mk-uuid "user" i) - (str "User " i) - (str "user" i) - (str "user" i ".test@uxbox.io") - (hashers/encrypt "123123") - (blob/encode {}) - ""])) + [conn user-index] + (log/info "create user" user-index) + (let [sql create-user-sql + id (mk-uuid "user" user-index) + fullname (str "User " user-index) + username (str "user" user-index) + email (str "user" user-index ".test@uxbox.io") + photo ""] + (db/query-one conn [sql id fullname username email password photo]))) ;; --- Projects creation @@ -49,29 +51,46 @@ returning *;") (defn create-project - [conn [pjid uid]] - (println "create project" pjid "(for user=" uid ")") - (db/query-one conn [create-project-sql - (mk-uuid "project" pjid uid) - (mk-uuid "user" uid) - (str "sample project " pjid)])) + [conn [project-index user-index]] + (log/info "create project" user-index project-index) + (let [sql create-project-sql + id (mk-uuid "project" project-index user-index) + user-id (mk-uuid "user" user-index) + name (str "sample project " project-index)] + (db/query-one conn [sql id user-id name]))) -;; --- Pages creation +;; --- Create Page Files + +(def create-file-sql + "insert into project_files (id, user_id, project_id, name) + values ($1, $2, $3, $4) returning id") + +(defn create-file + [conn [file-index project-index user-index]] + (log/info "create page file" user-index project-index file-index) + (let [sql create-file-sql + id (mk-uuid "page-file" file-index project-index user-index) + user-id (mk-uuid "user" user-index) + project-id (mk-uuid "project" project-index user-index) + name (str "Sample file " file-index)] + (db/query-one conn [sql id user-id project-id name]))) + +;; --- Create Pages (def create-page-sql - "insert into pages (id, user_id, project_id, name, + "insert into project_pages (id, user_id, file_id, name, version, ordering, data, metadata) values ($1, $2, $3, $4, $5, $6, $7, $8) returning id;") (def create-page-history-sql - "insert into pages_history (page_id, user_id, version, data, metadata) - values ($1, $2, $3, $4, $5) + "insert into project_page_history (page_id, user_id, version, data) + values ($1, $2, $3, $4) returning id;") (defn create-page - [conn [pjid paid uid]] - (println "create page" paid "(for project=" pjid ", user=" uid ")") + [conn [page-index file-index project-index user-index]] + (log/info "create page" user-index project-index file-index page-index) (let [canvas {:id (mk-uuid "canvas" 1) :name "Canvas-1" :type :canvas @@ -82,29 +101,61 @@ data {:shapes [] :canvas [(:id canvas)] :shapes-by-id {(:id canvas) canvas}} + + sql1 create-page-sql + sql2 create-page-history-sql + + id (mk-uuid "page" page-index file-index project-index user-index) + user-id (mk-uuid "user" user-index) + file-id (mk-uuid "page-file" file-index project-index user-index) + name (str "page " page-index) + version 0 + ordering page-index data (blob/encode data) mdata (blob/encode {})] (p/do! - (db/query-one conn [create-page-sql - (mk-uuid "page" pjid paid uid) - (mk-uuid "user" uid) - (mk-uuid "project" pjid uid) - (str "page " paid) - 0 - paid - data - mdata]) - (db/query-one conn [create-page-history-sql - (mk-uuid "page" pjid paid uid) - (mk-uuid "user" uid) - 0 - data - mdata])))) + (db/query-one conn [sql1 id user-id file-id name version ordering data mdata]) + #_(db/query-one conn [sql2 id user-id version data])))) +(def preset-small + {:users 50 + :projects 5 + :files 5 + :pages 3}) -(def num-users 5) -(def num-projects 5) -(def num-pages 5) +(def preset-medium + {:users 500 + :projects 20 + :files 5 + :pages 3}) + +(def preset-big + {:users 5000 + :projects 50 + :files 5 + :pages 4}) + +(defn run + [opts] + (db/with-atomic [conn db/pool] + (p/do! + (p/run! #(create-user conn %) (range (:users opts))) + (p/run! #(create-project conn %) + (for [user-index (range (:users opts)) + project-index (range (:projects opts))] + [project-index user-index])) + (p/run! #(create-file conn %) + (for [user-index (range (:users opts)) + project-index (range (:projects opts)) + file-index (range (:files opts))] + [file-index project-index user-index])) + (p/run! #(create-page conn %) + (for [user-index (range (:users opts)) + project-index (range (:projects opts)) + file-index (range (:files opts)) + page-index (range (:pages opts))] + [page-index file-index project-index user-index])) + (p/promise nil)))) (defn -main [& args] @@ -115,18 +166,12 @@ #'uxbox.db/pool #'uxbox.migrations/migrations}) (mount/start)) - @(db/with-atomic [conn db/pool] - (p/do! - (p/run! #(create-user conn %) (range num-users)) - (p/run! #(create-project conn %) - (for [uid (range num-users) - pjid (range num-projects)] - [pjid uid])) - (p/run! #(create-page conn %) - (for [pjid(range num-projects) - paid (range num-pages) - uid (range num-users)] - [pjid paid uid])) - (p/promise 1))) + (let [preset (case (first args) + (nil "small") preset-small + "medium" preset-medium + "big" preset-big + preset-small)] + (log/info "Using preset:" (pr-str preset)) + (deref (run preset))) (finally (mount/stop)))) diff --git a/backend/src/uxbox/http/errors.clj b/backend/src/uxbox/http/errors.clj index 614371667..0863e1ae1 100644 --- a/backend/src/uxbox/http/errors.clj +++ b/backend/src/uxbox/http/errors.clj @@ -8,6 +8,7 @@ "A errors handling for the http server." (:require [clojure.tools.logging :as log] + [cuerdas.core :as str] [io.aviso.exception :as e])) (defmulti handle-exception @@ -16,9 +17,18 @@ (defmethod handle-exception :validation [err req] - (let [response (ex-data err)] - {:status 400 - :body response})) + (let [header (get-in req [:headers "accept"]) + response (ex-data err)] + (cond + (and (str/starts-with? header "text/html") + (= :spec-validation (:code response))) + {:status 400 + :headers {"content-type" "text/html"} + :body (str "<pre style='font-size:16px'>" (:explain response) "</pre>\n")} + + :else + {:status 400 + :body response}))) (defmethod handle-exception :not-found [err req] @@ -26,6 +36,10 @@ {:status 404 :body response})) +(defmethod handle-exception :service-error + [err req] + (handle-exception (.getCause err) req)) + (defmethod handle-exception :parse [err req] {:status 400 diff --git a/backend/src/uxbox/migrations.clj b/backend/src/uxbox/migrations.clj index 098d42cf8..22de8de26 100644 --- a/backend/src/uxbox/migrations.clj +++ b/backend/src/uxbox/migrations.clj @@ -26,18 +26,15 @@ {:desc "Initial projects tables" :name "0003-projects" :fn (mg/resource "migrations/0003.projects.sql")} - {:desc "Initial pages tables" - :name "0004-pages" - :fn (mg/resource "migrations/0004.pages.sql")} {:desc "Initial emails related tables" - :name "0005-emails" - :fn (mg/resource "migrations/0005.emails.sql")} + :name "0004-emails" + :fn (mg/resource "migrations/0004.emails.sql")} {:desc "Initial images tables" - :name "0006-images" - :fn (mg/resource "migrations/0006.images.sql")} + :name "0005-images" + :fn (mg/resource "migrations/0005.images.sql")} {:desc "Initial icons tables" - :name "0007-icons" - :fn (mg/resource "migrations/0007.icons.sql")} + :name "0006-icons" + :fn (mg/resource "migrations/0006.icons.sql")} ]}) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/backend/src/uxbox/services/init.clj b/backend/src/uxbox/services/init.clj index e3a846d2d..bda9a1af8 100644 --- a/backend/src/uxbox/services/init.clj +++ b/backend/src/uxbox/services/init.clj @@ -13,20 +13,22 @@ [] (require 'uxbox.services.queries.icons) (require 'uxbox.services.queries.images) - (require 'uxbox.services.queries.pages) - (require 'uxbox.services.queries.profiles) (require 'uxbox.services.queries.projects) - (require 'uxbox.services.queries.user-storage)) + (require 'uxbox.services.queries.project-files) + (require 'uxbox.services.queries.project-pages) + (require 'uxbox.services.queries.users) + (require 'uxbox.services.queries.user-attrs)) (defn- load-mutation-services [] - (require 'uxbox.services.mutations.auth) (require 'uxbox.services.mutations.icons) (require 'uxbox.services.mutations.images) (require 'uxbox.services.mutations.projects) - (require 'uxbox.services.mutations.pages) - (require 'uxbox.services.mutations.profiles) - (require 'uxbox.services.mutations.user-storage)) + (require 'uxbox.services.mutations.project-files) + (require 'uxbox.services.mutations.project-pages) + (require 'uxbox.services.mutations.auth) + (require 'uxbox.services.mutations.users) + (require 'uxbox.services.mutations.user-attrs)) (defstate query-services :start (load-query-services)) diff --git a/backend/src/uxbox/services/mutations.clj b/backend/src/uxbox/services/mutations.clj index f62a08d89..6bfa98c50 100644 --- a/backend/src/uxbox/services/mutations.clj +++ b/backend/src/uxbox/services/mutations.clj @@ -6,11 +6,13 @@ (ns uxbox.services.mutations (:require - [uxbox.util.dispatcher :as uds])) + [uxbox.util.dispatcher :as uds] + [uxbox.util.exceptions :as ex])) (uds/defservice handle {:dispatch-by ::type :interceptors [uds/spec-interceptor + uds/wrap-errors #_logging-interceptor #_context-interceptor]}) diff --git a/backend/src/uxbox/services/mutations/pages.clj b/backend/src/uxbox/services/mutations/pages.clj deleted file mode 100644 index a70fe7125..000000000 --- a/backend/src/uxbox/services/mutations/pages.clj +++ /dev/null @@ -1,174 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz> - -(ns uxbox.services.mutations.pages - (:require - [clojure.spec.alpha :as s] - [promesa.core :as p] - [uxbox.db :as db] - [uxbox.util.spec :as us] - [uxbox.services.mutations :as sm] - [uxbox.services.util :as su] - [uxbox.services.queries.pages :refer [decode-row]] - [uxbox.util.sql :as sql] - [uxbox.util.blob :as blob] - [uxbox.util.uuid :as uuid])) - -;; --- Helpers & Specs - -;; TODO: validate `:data` and `:metadata` - -(s/def ::id ::us/uuid) -(s/def ::name ::us/string) -(s/def ::data any?) -(s/def ::user ::us/uuid) -(s/def ::project-id ::us/uuid) -(s/def ::metadata any?) -(s/def ::ordering ::us/number) - -;; --- Mutation: Create Page - -(declare create-page) - -(s/def ::create-page - (s/keys :req-un [::data ::user ::project-id ::name ::metadata] - :opt-un [::id])) - -(sm/defmutation ::create-page - [params] - (create-page db/pool params)) - -(defn create-page - [conn {:keys [id user project-id name ordering data metadata] :as params}] - (let [sql "insert into pages (id, user_id, project_id, name, - ordering, data, metadata, version) - values ($1, $2, $3, $4, $5, $6, $7, 0) - returning *" - id (or id (uuid/next)) - data (blob/encode data) - mdata (blob/encode metadata)] - (-> (db/query-one db/pool [sql id user project-id name ordering data mdata]) - (p/then' decode-row)))) - -;; --- Mutation: Update Page - -(s/def ::update-page - (s/keys :req-un [::data ::user ::project-id ::name ::data ::metadata ::id])) - -(letfn [(select-for-update [conn id] - (let [sql "select p.id, p.version - from pages as p - where p.id = $1 - and deleted_at is null - for update;"] - (-> (db/query-one conn [sql id]) - (p/then' su/raise-not-found-if-nil)))) - - (update-page [conn {:keys [id name version data metadata user]}] - (let [sql "update pages - set name = $1, - version = $2, - data = $3, - metadata = $4 - where id = $5 - and user_id = $6"] - (-> (db/query-one conn [sql name version data metadata id user]) - (p/then' su/constantly-nil)))) - - (update-history [conn {:keys [user id version data metadata]}] - (let [sql "insert into pages_history (user_id, page_id, version, data, metadata) - values ($1, $2, $3, $4, $5)"] - (-> (db/query-one conn [sql user id version data metadata]) - (p/then' su/constantly-nil))))] - - (sm/defmutation ::update-page - [{:keys [id data metadata] :as params}] - (db/with-atomic [conn db/pool] - (-> (select-for-update conn id) - (p/then (fn [{:keys [id version]}] - (let [data (blob/encode data) - mdata (blob/encode metadata) - version (inc version) - params (assoc params - :id id - :version version - :data data - :metadata mdata)] - (p/do! (update-page conn params) - (update-history conn params) - (select-keys params [:id :version]))))))))) - -;; --- Mutation: Rename Page - -(s/def ::rename-page - (s/keys :req-un [::id ::name ::user])) - -(sm/defmutation ::rename-page - [{:keys [id name user]}] - (let [sql "update pages - set name = $3 - where id = $1 - and user_id = $2 - and deleted_at is null"] - (-> (db/query-one db/pool [sql id user name]) - (p/then su/constantly-nil)))) - -;; --- Mutation: Update Page Metadata - -(s/def ::update-page-metadata - (s/keys :req-un [::user ::project-id ::name ::metadata ::id])) - -(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)))) - -;; --- Mutation: Delete Page - -(s/def ::delete-page - (s/keys :req-un [::user ::id])) - -(sm/defmutation ::delete-page - [{:keys [id user]}] - (let [sql "update pages - set deleted_at = clock_timestamp() - where id = $1 - and user_id = $2 - and deleted_at is null - returning id"] - (-> (db/query-one db/pool [sql id user]) - (p/then su/raise-not-found-if-nil) - (p/then su/constantly-nil)))) - -;; ;; --- Update Page History - -;; (defn update-page-history -;; [conn {:keys [user id label pinned]}] -;; (let [sqlv (sql/update-page-history {:user user -;; :id id -;; :label label -;; :pinned pinned})] -;; (some-> (db/fetch-one conn sqlv) -;; (decode-row)))) - -;; (s/def ::label ::us/string) -;; (s/def ::update-page-history -;; (s/keys :req-un [::user ::id ::pinned ::label])) - -;; (sm/defmutation :update-page-history -;; {:doc "Update page history" -;; :spec ::update-page-history} -;; [params] -;; (with-open [conn (db/connection)] -;; (update-page-history conn params))) diff --git a/backend/src/uxbox/services/mutations/project_files.clj b/backend/src/uxbox/services/mutations/project_files.clj new file mode 100644 index 000000000..521aaf97b --- /dev/null +++ b/backend/src/uxbox/services/mutations/project_files.clj @@ -0,0 +1,142 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz> + +(ns uxbox.services.mutations.project-files + (:require + [clojure.spec.alpha :as s] + [promesa.core :as p] + [uxbox.db :as db] + [uxbox.util.spec :as us] + [uxbox.services.mutations :as sm] + [uxbox.services.mutations.projects :as proj] + [uxbox.services.util :as su] + [uxbox.util.exceptions :as ex] + [uxbox.util.blob :as blob] + [uxbox.util.uuid :as uuid])) + +;; --- Helpers & Specs + +(s/def ::id ::us/uuid) +(s/def ::name ::us/string) +(s/def ::user ::us/uuid) +(s/def ::project-id ::us/uuid) + +;; --- Permissions Checks + +;; A query that returns all (not-equal) user assignations for a +;; requested file (project level and file level). + +;; Is important having the condition of user_id in the join and not in +;; where clause because we need all results independently if value is +;; true, false or null; with that, the empty result means there are no +;; file found. + +(def ^:private sql:file-permissions + "select pf.id, + pfu.can_edit as can_edit + from project_files as pf + left join project_file_users as pfu + on (pfu.file_id = pf.id and pfu.user_id = $1) + where pf.id = $2 + union all + select pf.id, + pu.can_edit as can_edit + from project_files as pf + left join project_users as pu + on (pf.project_id = pu.project_id and pu.user_id = $1) + where pf.id = $2") + +(defn check-edition-permissions! + [conn user file-id] + (-> (db/query conn [sql:file-permissions user file-id]) + (p/then' seq) + (p/then' su/raise-not-found-if-nil) + (p/then' (fn [rows] + (when-not (some :can-edit rows) + (ex/raise :type :validation + :code :not-authorized)))))) + +;; --- Mutation: Create Project + +(declare create-file) +(declare create-page) + +(s/def ::create-project-file + (s/keys :req-un [::user ::name ::project-id] + :opt-un [::id])) + +(sm/defmutation ::create-project-file + [{:keys [user project-id] :as params}] + (db/with-atomic [conn db/pool] + (proj/check-edition-permissions! conn user project-id) + (p/let [file (create-file conn params)] + (create-page conn (assoc params :file-id (:id file))) + file))) + +(defn create-file + [conn {:keys [id user name project-id] :as params}] + (let [id (or id (uuid/next)) + sql "insert into project_files (id, user_id, project_id, name) + values ($1, $2, $3, $4) returning *"] + (db/query-one conn [sql id user project-id name]))) + +(defn- create-page + "Creates an initial page for the file." + [conn {:keys [user file-id] :as params}] + (let [id (uuid/next) + name "Page 1" + data (blob/encode {}) + sql "insert into project_pages (id, user_id, file_id, name, version, + ordering, data) + values ($1, $2, $3, $4, 0, 1, $5) returning id"] + (db/query-one conn [sql id user file-id name data]))) + +;; --- Mutation: Update Project + +(declare update-file) + +(s/def ::update-project-file + (s/keys :req-un [::user ::name ::id])) + +(sm/defmutation ::update-project-file + [{:keys [id user] :as params}] + (db/with-atomic [conn db/pool] + (check-edition-permissions! conn user id) + (update-file conn params))) + +(defn- update-file + [conn {:keys [id name user] :as params}] + (let [sql "update project_files + set name = $2 + where id = $1 + and deleted_at is null"] + (-> (db/query-one conn [sql id name]) + (p/then' su/constantly-nil)))) + +;; --- Mutation: Delete Project + +(declare delete-file) + +(s/def ::delete-project-file + (s/keys :req-un [::id ::user])) + +(sm/defmutation ::delete-project-file + [{:keys [id user] :as params}] + (db/with-atomic [conn db/pool] + (check-edition-permissions! conn user id) + (delete-file conn params))) + +(def ^:private sql:delete-file + "update project_files + set deleted_at = clock_timestamp() + where id = $1 + and deleted_at is null") + +(defn delete-file + [conn {:keys [id] :as params}] + (let [sql sql:delete-file] + (-> (db/query-one conn [sql id]) + (p/then' su/constantly-nil)))) diff --git a/backend/src/uxbox/services/mutations/project_pages.clj b/backend/src/uxbox/services/mutations/project_pages.clj new file mode 100644 index 000000000..579b53855 --- /dev/null +++ b/backend/src/uxbox/services/mutations/project_pages.clj @@ -0,0 +1,190 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz> + +(ns uxbox.services.mutations.project-pages + (:require + [clojure.spec.alpha :as s] + [promesa.core :as p] + [uxbox.db :as db] + [uxbox.services.mutations :as sm] + [uxbox.services.mutations.project-files :as files] + [uxbox.services.queries.project-pages :refer [decode-row]] + [uxbox.services.util :as su] + [uxbox.util.blob :as blob] + [uxbox.util.spec :as us] + [uxbox.util.sql :as sql] + [uxbox.util.uuid :as uuid])) + +;; --- Helpers & Specs + +;; TODO: validate `:data` and `:metadata` + +(s/def ::id ::us/uuid) +(s/def ::name ::us/string) +(s/def ::data any?) +(s/def ::user ::us/uuid) +(s/def ::project-id ::us/uuid) +(s/def ::metadata any?) +(s/def ::ordering ::us/number) + +;; --- Mutation: Create Page + +(declare create-page) + +(s/def ::create-project-page + (s/keys :req-un [::user ::file-id ::name ::ordering ::metadata ::data] + :opt-un [::id])) + +(sm/defmutation ::create-project-page + [{:keys [user file-id] :as params}] + (db/with-atomic [conn db/pool] + (files/check-edition-permissions! conn user file-id) + (create-page conn params))) + +(defn create-page + [conn {:keys [id user file-id name ordering data metadata] :as params}] + (let [sql "insert into project_pages (id, user_id, file_id, name, + ordering, data, metadata, version) + values ($1, $2, $3, $4, $5, $6, $7, 0) + returning *" + id (or id (uuid/next)) + data (blob/encode data) + mdata (blob/encode metadata)] + (-> (db/query-one conn [sql id user file-id name ordering data mdata]) + (p/then' decode-row)))) + +;; --- Mutation: Update Page + +(declare select-page-for-update) +(declare update-page) +(declare update-history) + +(s/def ::update-project-page-data + (s/keys :req-un [::id ::user ::data])) + +(sm/defmutation ::update-project-page-data + [{:keys [id user data] :as params}] + (db/with-atomic [conn db/pool] + (p/let [{:keys [version file-id]} (select-page-for-update conn id)] + (files/check-edition-permissions! conn user file-id) + (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) + (select-keys params [:id :version])))))) + +(defn- select-page-for-update + [conn id] + (let [sql "select p.id, p.version, p.file_id + from project_pages as p + where p.id = $1 + and deleted_at is null + for update;"] + (-> (db/query-one conn [sql id]) + (p/then' su/raise-not-found-if-nil)))) + +(defn- update-page + [conn {:keys [id name version data metadata]}] + (let [sql "update project_pages + set version = $1, + data = $2 + where id = $3"] + (-> (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)))) + +;; --- Mutation: Rename Page + +(declare rename-page) + +(s/def ::rename-project-page + (s/keys :req-un [::id ::name ::user])) + +(sm/defmutation ::rename-project-page + [{:keys [id name user]}] + (db/with-atomic [conn db/pool] + (p/let [page (select-page-for-update conn id)] + (files/check-edition-permissions! conn user (:file-id page)) + (rename-page conn (assoc page :name name))))) + +(defn- rename-page + [conn {:keys [id name] :as params}] + (let [sql "update project_pages + set name = $2 + where id = $1 + and deleted_at is null"] + (-> (db/query-one db/pool [sql id name]) + (p/then su/constantly-nil)))) + +;; --- Mutation: Update Page Metadata + +;; (s/def ::update-page-metadata +;; (s/keys :req-un [::user ::project-id ::name ::metadata ::id])) + +;; (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)))) + +;; --- Mutation: Delete Page + +(declare delete-page) + +(s/def ::delete-project-page + (s/keys :req-un [::user ::id])) + +(sm/defmutation ::delete-project-page + [{:keys [id user]}] + (db/with-atomic [conn db/pool] + (p/let [page (select-page-for-update conn id)] + (files/check-edition-permissions! conn user (:file-id page)) + (delete-page conn id)))) + +(defn- delete-page + [conn id] + (let [sql "update project_pages + set deleted_at = clock_timestamp() + where id = $1 + and deleted_at is null"] + (-> (db/query-one conn [sql id]) + (p/then su/constantly-nil)))) + +;; --- Update Page History + +;; (defn update-page-history +;; [conn {:keys [user id label pinned]}] +;; (let [sqlv (sql/update-page-history {:user user +;; :id id +;; :label label +;; :pinned pinned})] +;; (some-> (db/fetch-one conn sqlv) +;; (decode-row)))) + +;; (s/def ::label ::us/string) +;; (s/def ::update-page-history +;; (s/keys :req-un [::user ::id ::pinned ::label])) + +;; (sm/defmutation :update-page-history +;; {:doc "Update page history" +;; :spec ::update-page-history} +;; [params] +;; (with-open [conn (db/connection)] +;; (update-page-history conn params))) diff --git a/backend/src/uxbox/services/mutations/projects.clj b/backend/src/uxbox/services/mutations/projects.clj index 1a408c6c2..ea6755266 100644 --- a/backend/src/uxbox/services/mutations/projects.clj +++ b/backend/src/uxbox/services/mutations/projects.clj @@ -13,6 +13,7 @@ [uxbox.services.mutations :as sm] [uxbox.services.util :as su] [uxbox.util.blob :as blob] + [uxbox.util.exceptions :as ex] [uxbox.util.uuid :as uuid])) ;; --- Helpers & Specs @@ -22,6 +23,27 @@ (s/def ::token ::us/string) (s/def ::user ::us/uuid) +;; --- Permissions Checks + +(def ^:private sql:project-permissions + "select p.id, + pu.can_edit as can_edit + from projects as p + inner join project_users as pu + on (pu.project_id = p.id) + where pu.user_id = $1 + and p.id = $2 + for update of p;") + +(defn check-edition-permissions! + [conn user project-id] + (-> (db/query-one conn [sql:project-permissions user project-id]) + (p/then' su/raise-not-found-if-nil) + (p/then' (fn [{:keys [id can-edit] :as proj}] + (when-not can-edit + (ex/raise :type :validation + :code :not-authorized)))))) + ;; --- Mutation: Create Project (declare create-project) @@ -31,11 +53,9 @@ :opt-un [::id])) (sm/defmutation ::create-project - [{:keys [id user name] :as params}] - (let [id (or id (uuid/next)) - sql "insert into projects (id, user_id, name) - values ($1, $2, $3) returning *"] - (db/query-one db/pool [sql id user name]))) + [params] + (db/with-atomic [conn db/pool] + (create-project conn params))) (defn create-project [conn {:keys [id user name] :as params}] @@ -46,32 +66,49 @@ ;; --- Mutation: Update Project +(declare update-project) + (s/def ::update-project (s/keys :req-un [::user ::name ::id])) (sm/defmutation ::update-project - [{:keys [id name user] :as params}] + [{:keys [id user] :as params}] + (db/with-atomic [conn db/pool] + (check-edition-permissions! conn user id) + (update-project conn params))) + +(defn update-project + [conn {:keys [id name user] :as params}] (let [sql "update projects set name = $3 where id = $1 and user_id = $2 and deleted_at is null returning *"] - (db/query-one db/pool [sql id user name]))) + (db/query-one conn [sql id user name]))) ;; --- Mutation: Delete Project +(declare delete-project) + (s/def ::delete-project (s/keys :req-un [::id ::user])) (sm/defmutation ::delete-project [{:keys [id user] :as params}] - (let [sql "update projects - set deleted_at = clock_timestamp() - where id = $1 - and user_id = $2 - and deleted_at is null - returning id"] - (-> (db/query-one db/pool [sql id user]) - (p/then' su/raise-not-found-if-nil) + (db/with-atomic [conn db/pool] + (check-edition-permissions! conn user id) + (delete-project conn params))) + +(def ^:private sql:delete-project + "update projects + set deleted_at = clock_timestamp() + where id = $1 + and deleted_at is null + returning id") + +(defn delete-project + [conn {:keys [id user] :as params}] + (let [sql sql:delete-project] + (-> (db/query-one conn [sql id]) (p/then' su/constantly-nil)))) diff --git a/backend/src/uxbox/services/mutations/user_storage.clj b/backend/src/uxbox/services/mutations/user_attrs.clj similarity index 75% rename from backend/src/uxbox/services/mutations/user_storage.clj rename to backend/src/uxbox/services/mutations/user_attrs.clj index 9328fbb5c..9d459ebb6 100644 --- a/backend/src/uxbox/services/mutations/user_storage.clj +++ b/backend/src/uxbox/services/mutations/user_attrs.clj @@ -4,14 +4,14 @@ ;; ;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz> -(ns uxbox.services.mutations.user-storage +(ns uxbox.services.mutations.user-attrs (:require [clojure.spec.alpha :as s] [promesa.core :as p] [uxbox.db :as db] [uxbox.services.mutations :as sm] [uxbox.services.util :as su] - [uxbox.services.queries.user-storage :refer [decode-row]] + [uxbox.services.queries.user-attrs :refer [decode-row]] [uxbox.util.blob :as blob] [uxbox.util.spec :as us])) @@ -21,12 +21,12 @@ (s/def ::key ::us/string) (s/def ::val any?) -(s/def ::upsert-user-storage-entry +(s/def ::upsert-user-attr (s/keys :req-un [::key ::val ::user])) -(sm/defmutation ::upsert-user-storage-entry +(sm/defmutation ::upsert-user-attr [{:keys [key val user] :as params}] - (let [sql "insert into user_storage (key, val, user_id) + (let [sql "insert into user_attrs (key, val, user_id) values ($1, $2, $3) on conflict (user_id, key) do update set val = $2" @@ -36,12 +36,12 @@ ;; --- Delete KVStore -(s/def ::delete-user-storage-entry +(s/def ::delete-user-attr (s/keys :req-un [::key ::user])) -(sm/defmutation ::delete-user-storage-entry +(sm/defmutation ::delete-user-attr [{:keys [user key] :as params}] - (let [sql "delete from user_storage + (let [sql "delete from user_attrs where user_id = $2 and key = $1"] (-> (db/query-one db/pool [sql key user]) diff --git a/backend/src/uxbox/services/mutations/profiles.clj b/backend/src/uxbox/services/mutations/users.clj similarity index 94% rename from backend/src/uxbox/services/mutations/profiles.clj rename to backend/src/uxbox/services/mutations/users.clj index af221e7db..308775ff9 100644 --- a/backend/src/uxbox/services/mutations/profiles.clj +++ b/backend/src/uxbox/services/mutations/users.clj @@ -4,7 +4,7 @@ ;; ;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz> -(ns uxbox.services.mutations.profiles +(ns uxbox.services.mutations.users (:require [buddy.hashers :as hashers] [clojure.spec.alpha :as s] @@ -19,10 +19,10 @@ [uxbox.media :as media] [uxbox.services.mutations :as sm] [uxbox.services.util :as su] - [uxbox.services.queries.profiles :refer [get-profile - decode-profile-row - strip-private-attrs - resolve-thumbnail]] + [uxbox.services.queries.users :refer [get-profile + decode-profile-row + strip-private-attrs + resolve-thumbnail]] [uxbox.util.blob :as blob] [uxbox.util.exceptions :as ex] [uxbox.util.spec :as us] @@ -56,7 +56,7 @@ and id != $1 ) as val"] (p/let [res1 (db/query-one conn [sql1 id username]) - res2 (db/query-one conn [sql2 id email])] + res2 (db/query-one conn [sql2 id email])] (when (:val res1) (ex/raise :type :validation :code ::username-already-exists)) @@ -83,9 +83,7 @@ (s/def ::update-profile (s/keys :req-un [::id ::username ::email ::fullname ::metadata])) -(sm/defmutation :update-profile - {:doc "Update self profile." - :spec ::update-profile} +(sm/defmutation ::update-profile [params] (db/with-atomic [conn db/pool] (-> (p/resolved params) @@ -134,9 +132,7 @@ (def valid-image-types? #{"image/jpeg", "image/png", "image/webp"}) -(sm/defmutation :update-profile-photo - {:doc "Update profile photo." - :spec ::update-profile-photo} +(sm/defmutation ::update-profile-photo [{:keys [user file] :as params}] (letfn [(store-photo [{:keys [name path] :as upload}] (let [filename (fs/name name) @@ -149,16 +145,17 @@ set photo = $1 where id = $2 and deleted_at is null - returning *"] + returning id, photo"] (-> (db/query-one db/pool [sql (str path) user]) (p/then' su/raise-not-found-if-nil) - (p/then' strip-private-attrs) + ;; (p/then' strip-private-attrs) (p/then resolve-thumbnail))))] (when-not (valid-image-types? (:mtype file)) (ex/raise :type :validation :code :image-type-not-allowed :hint "Seems like you are uploading an invalid image.")) + (-> (store-photo file) (p/then update-user-photo)))) diff --git a/backend/src/uxbox/services/queries.clj b/backend/src/uxbox/services/queries.clj index bfa48b6d2..97639d2c0 100644 --- a/backend/src/uxbox/services/queries.clj +++ b/backend/src/uxbox/services/queries.clj @@ -11,6 +11,7 @@ (uds/defservice handle {:dispatch-by ::type :interceptors [uds/spec-interceptor + uds/wrap-errors #_logging-interceptor #_context-interceptor]}) diff --git a/backend/src/uxbox/services/queries/pages.clj b/backend/src/uxbox/services/queries/pages.clj deleted file mode 100644 index d432d4bce..000000000 --- a/backend/src/uxbox/services/queries/pages.clj +++ /dev/null @@ -1,92 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz> - -(ns uxbox.services.queries.pages - (:require - [clojure.spec.alpha :as s] - [promesa.core :as p] - [uxbox.db :as db] - [uxbox.services.queries :as sq] - [uxbox.util.blob :as blob] - [uxbox.util.spec :as us] - [uxbox.util.sql :as sql])) - -;; --- Helpers & Specs - -(declare decode-row) - -(s/def ::id ::us/uuid) -(s/def ::user ::us/uuid) -(s/def ::project-id ::us/uuid) - -;; --- Query: Pages by Project - -(s/def ::pages-by-project - (s/keys :req-un [::user ::project-id])) - -(sq/defquery ::pages-by-project - [{:keys [user project-id] :as params}] - (let [sql "select pg.*, - pg.data, - pg.metadata - from pages as pg - where pg.user_id = $2 - and pg.project_id = $1 - and pg.deleted_at is null - order by pg.created_at asc;"] - (-> (db/query db/pool [sql project-id user]) - (p/then #(mapv decode-row %))))) - -;; --- Query: Page by Id - -(s/def ::page - (s/keys :req-un [::user ::id])) - -(sq/defquery ::page - [{:keys [user id] :as params}] - (let [sql "select pg.*, - pg.data, - pg.metadata - from pages as pg - where pg.user_id = $2 - and pg.id = $1 - and pg.deleted_at is null"] - (-> (db/query-one db/pool [sql id user]) - (p/then' decode-row)))) - -;; --- Query: Page History - -(s/def ::page-id ::us/uuid) -(s/def ::max ::us/integer) -(s/def ::pinned ::us/boolean) -(s/def ::since ::us/integer) - -(s/def ::page-history - (s/keys :req-un [::page-id ::user] - :opt-un [::max ::pinned ::since])) - -(sq/defquery ::page-history - [{:keys [page-id user since max pinned] :or {since Long/MAX_VALUE max 10}}] - (let [sql (-> (sql/from ["pages_history" "ph"]) - (sql/select "ph.*") - (sql/where ["ph.user_id = ?" user] - ["ph.page_id = ?" page-id] - ["ph.version < ?" since] - (when pinned - ["ph.pinned = ?" true])) - (sql/order "ph.version desc") - (sql/limit max))] - (-> (db/query db/pool (sql/fmt sql)) - (p/then (partial mapv decode-row))))) - -;; --- Helpers - -(defn decode-row - [{:keys [data metadata] :as row}] - (when row - (cond-> row - data (assoc :data (blob/decode data)) - metadata (assoc :metadata (blob/decode metadata))))) diff --git a/backend/src/uxbox/services/queries/project_files.clj b/backend/src/uxbox/services/queries/project_files.clj new file mode 100644 index 000000000..1e3effa59 --- /dev/null +++ b/backend/src/uxbox/services/queries/project_files.clj @@ -0,0 +1,55 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz> + +(ns uxbox.services.queries.project-files + (:require + [clojure.spec.alpha :as s] + [promesa.core :as p] + [uxbox.db :as db] + [uxbox.services.queries :as sq] + [uxbox.util.blob :as blob] + [uxbox.util.spec :as us])) + +(declare decode-row) + +;; --- Helpers & Specs + +(s/def ::id ::us/uuid) +(s/def ::name ::us/string) +(s/def ::project-id ::us/uuid) +(s/def ::user ::us/uuid) + +;; --- Query: Project Files + +(def ^:private sql:project-files + "select pf.*, + array_agg(pp.id) as pages + from project_files as pf + inner join projects as p on (pf.project_id = p.id) + inner join project_users as pu on (p.id = pu.project_id) + left join project_pages as pp on (pf.id = pp.file_id) + where pu.user_id = $1 + and pu.project_id = $2 + and pu.can_edit = true + group by pf.id + order by pf.created_at asc;") + +(s/def ::project-files + (s/keys :req-un [::user ::project-id])) + +(sq/defquery ::project-files + [{:keys [user project-id] :as params}] + (-> (db/query db/pool [sql:project-files user project-id]) + (p/then' (partial mapv decode-row)))) + +;; --- Helpers + +(defn decode-row + [{:keys [metadata pages] :as row}] + (when row + (cond-> row + pages (assoc :pages (vec (remove nil? pages))) + metadata (assoc :metadata (blob/decode metadata))))) diff --git a/backend/src/uxbox/services/queries/project_pages.clj b/backend/src/uxbox/services/queries/project_pages.clj new file mode 100644 index 000000000..3ba860c5f --- /dev/null +++ b/backend/src/uxbox/services/queries/project_pages.clj @@ -0,0 +1,145 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz> + +(ns uxbox.services.queries.project-pages + (:require + [clojure.spec.alpha :as s] + [promesa.core :as p] + [uxbox.db :as db] + [uxbox.services.queries :as sq] + [uxbox.services.util :as su] + [uxbox.util.blob :as blob] + [uxbox.util.spec :as us] + [uxbox.util.sql :as sql])) + +;; --- Helpers & Specs + +(declare decode-row) + +(s/def ::id ::us/uuid) +(s/def ::user ::us/uuid) +(s/def ::project-id ::us/uuid) +(s/def ::file-id ::us/uuid) + +(def ^:private sql:generic-project-pages + "select pp.* + from project_pages as pp + inner join project_files as pf on (pf.id = pp.file_id) + inner join projects as p on (p.id = pf.project_id) + left join project_users as pu on (pu.project_id = p.id) + left join project_file_users as pfu on (pfu.file_id = pf.id) + where ((pfu.user_id = $1 and pfu.can_edit = true) or + (pu.user_id = $1 and pu.can_edit = true)) + order by pp.created_at") + +;; --- Query: Project Pages (By File ID) + +(def ^:private sql:project-pages + (str "with pages as (" sql:generic-project-pages ")" + " select * from pages where file_id = $2")) + +;; (defn project-pages-sql +;; [user] +;; (-> (sql/from ["project_pages" "pp"]) +;; (sql/join ["project_files" "pf"] "pf.id = pp.file_id") +;; (sql/join ["projects" "p"] "p.id = pf.project_id") +;; (sql/ljoin ["project_users", "pu"] "pu.project_id = p.id") +;; (sql/ljoin ["project_file_users", "pfu"] "pfu.file_id = pf.id") +;; (sql/select "pp.*") +;; (sql/where ["((pfu.user_id = ? and pfu.can_edit = true) or +;; (pu.user_id = ? and pu.can_edit = true))" user user]) +;; (sql/order "pp.created_at"))) + +;; (let [sql (-> (project-pages-sql user) +;; (sql/where ["pp.file_id = ?" file-id]) +;; (sql/fmt))] +;; (-> (db/query db/pool sql) +;; (p/then #(mapv decode-row %))))) + +(s/def ::project-pages + (s/keys :req-un [::user ::file-id])) + +(sq/defquery ::project-pages + [{:keys [user file-id] :as params}] + (let [sql sql:project-pages] + (-> (db/query db/pool [sql user file-id]) + (p/then #(mapv decode-row %))))) + +;; --- Query: Project Page (By ID) + +(def ^:private sql:project-page + (str "with pages as (" sql:generic-project-pages ")" + " select * from pages where id = $2")) + +(defn retrieve-page + [conn {:keys [user id] :as params}] + (let [sql sql:project-page] + (-> (db/query-one conn [sql user id]) + (p/then' su/raise-not-found-if-nil) + (p/then' decode-row)))) + +(s/def ::project-page + (s/keys :req-un [::user ::id])) + +(sq/defquery ::project-page + [{:keys [user id] :as params}] + (retrieve-page db/pool params)) + +;; --- Query: Project Page History (by Page ID) + +;; (def ^:private sql:generic-page-history +;; "select pph.* +;; from project_page_history as pph +;; where pph.page_id = $2 +;; and pph.version < $3 +;; order by pph.version < desc") + +;; (def ^:private sql:page-history +;; (str "with history as (" sql:generic-page-history ")" +;; " select * from history limit $4")) + +;; (def ^:private sql:pinned-page-history +;; (str "with history as (" sql:generic-page-history ")" +;; " select * from history where pinned = true limit $4")) + +(s/def ::page-id ::us/uuid) +(s/def ::max ::us/integer) +(s/def ::pinned ::us/boolean) +(s/def ::since ::us/integer) + +(s/def ::page-history + (s/keys :req-un [::page-id ::user] + :opt-un [::max ::pinned ::since])) + +(defn retrieve-page-history + [{:keys [page-id user since max pinned] :or {since Long/MAX_VALUE max 10}}] + (let [sql (-> (sql/from ["pages_history" "ph"]) + (sql/select "ph.*") + (sql/where ["ph.user_id = ?" user] + ["ph.page_id = ?" page-id] + ["ph.version < ?" since] + (when pinned + ["ph.pinned = ?" true])) + (sql/order "ph.version desc") + (sql/limit max))] + (-> (db/query db/pool (sql/fmt sql)) + (p/then (partial mapv decode-row))))) + +(sq/defquery ::page-history + [{:keys [page-id user] :as params}] + (db/with-atomic [conn db/pool] + (p/do! (retrieve-page conn {:id page-id :user user}) + (retrieve-page-history conn params)))) + + +;; --- Helpers + +(defn decode-row + [{:keys [data metadata] :as row}] + (when row + (cond-> row + data (assoc :data (blob/decode data)) + metadata (assoc :metadata (blob/decode metadata))))) diff --git a/backend/src/uxbox/services/queries/projects.clj b/backend/src/uxbox/services/queries/projects.clj index c3cfa47f6..392b227ed 100644 --- a/backend/src/uxbox/services/queries/projects.clj +++ b/backend/src/uxbox/services/queries/projects.clj @@ -13,6 +13,8 @@ [uxbox.util.blob :as blob] [uxbox.util.spec :as us])) +(declare decode-row) + ;; --- Helpers & Specs (s/def ::id ::us/uuid) @@ -22,19 +24,26 @@ ;; --- Query: Projects +;; (def ^:private projects-sql +;; "select distinct on (p.id, p.created_at) +;; p.*, +;; array_agg(pg.id) over ( +;; partition by p.id +;; order by pg.created_at +;; range between unbounded preceding and unbounded following +;; ) as pages +;; from projects as p +;; left join pages as pg +;; on (pg.project_id = p.id) +;; where p.user_id = $1 +;; order by p.created_at asc") + (def ^:private projects-sql - "select distinct on (p.id, p.created_at) - p.*, - array_agg(pg.id) over ( - partition by p.id - order by pg.created_at - range between unbounded preceding and unbounded following - ) as pages - from projects as p - left join pages as pg - on (pg.project_id = p.id) - where p.user_id = $1 - order by p.created_at asc") + "select p.* + from project_users as pu + inner join projects as p on (p.id = pu.project_id) + where pu.can_edit = true + and pu.user_id = $1;") (s/def ::projects (s/keys :req-un [::user])) @@ -42,5 +51,12 @@ (sq/defquery ::projects [{:keys [user] :as params}] (-> (db/query db/pool [projects-sql user]) - (p/then (fn [rows] - (mapv #(update % :pages vec) rows))))) + (p/then' (partial mapv decode-row)))) + +;; --- Helpers + +(defn decode-row + [{:keys [metadata] :as row}] + (when row + (cond-> row + metadata (assoc :metadata (blob/decode metadata))))) diff --git a/backend/src/uxbox/services/queries/user_storage.clj b/backend/src/uxbox/services/queries/user_attrs.clj similarity index 85% rename from backend/src/uxbox/services/queries/user_storage.clj rename to backend/src/uxbox/services/queries/user_attrs.clj index 49a316959..4498e2a46 100644 --- a/backend/src/uxbox/services/queries/user_storage.clj +++ b/backend/src/uxbox/services/queries/user_attrs.clj @@ -4,7 +4,7 @@ ;; ;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz> -(ns uxbox.services.queries.user-storage +(ns uxbox.services.queries.user-attrs (:require [clojure.spec.alpha :as s] [promesa.core :as p] @@ -20,13 +20,13 @@ (cond-> row val (assoc :val (blob/decode val))))) -(s/def ::user-storage-entry +(s/def ::user-attr (s/keys :req-un [::key ::user])) -(sq/defquery ::user-storage-entry +(sq/defquery ::user-attr [{:keys [key user]}] (let [sql "select kv.* - from user_storage as kv + from user_attrs as kv where kv.user_id = $2 and kv.key = $1"] (-> (db/query-one db/pool [sql key user]) diff --git a/backend/src/uxbox/services/queries/profiles.clj b/backend/src/uxbox/services/queries/users.clj similarity index 94% rename from backend/src/uxbox/services/queries/profiles.clj rename to backend/src/uxbox/services/queries/users.clj index 52c72a7b5..55982c859 100644 --- a/backend/src/uxbox/services/queries/profiles.clj +++ b/backend/src/uxbox/services/queries/users.clj @@ -4,7 +4,7 @@ ;; ;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz> -(ns uxbox.services.queries.profiles +(ns uxbox.services.queries.users (:require [clojure.spec.alpha :as s] [promesa.core :as p] @@ -51,9 +51,7 @@ (s/def ::profile (s/keys :req-un [::user])) -(sq/defquery :profile - {:doc "Retrieve the user profile." - :spec ::profile} +(sq/defquery ::profile [{:keys [user] :as params}] (-> (get-profile db/pool user) (p/then' strip-private-attrs))) diff --git a/backend/src/uxbox/util/dispatcher.clj b/backend/src/uxbox/util/dispatcher.clj index 75b6fc3fe..db9588b54 100644 --- a/backend/src/uxbox/util/dispatcher.clj +++ b/backend/src/uxbox/util/dispatcher.clj @@ -28,7 +28,7 @@ IDispatcher (add [this key f metadata] (.put ^Map reg key (MapEntry/create f metadata)) - nil) + this) clojure.lang.IDeref (deref [_] @@ -56,7 +56,7 @@ (defn dispatcher? [v] - (instance? Dispatcher v)) + (instance? IDispatcher v)) (defmacro defservice [sname {:keys [dispatch-by interceptors]}] @@ -118,5 +118,16 @@ :code :spec-validation :explain (with-out-str (expound/printer data)) - :data data)))) + :data (::s/problems data))))) data)))}) + +(def wrap-errors + {:error + (fn [data] + (let [error (:error data) + mdata (meta (:request data))] + (assoc data :error (ex/error :type :service-error + :name (:spec mdata) + :cause error))))}) + + diff --git a/backend/src/uxbox/util/exceptions.clj b/backend/src/uxbox/util/exceptions.clj index 70db97db5..b39a9e8ce 100644 --- a/backend/src/uxbox/util/exceptions.clj +++ b/backend/src/uxbox/util/exceptions.clj @@ -12,7 +12,7 @@ (s/def ::code keyword?) (s/def ::mesage string?) (s/def ::hint string?) -(s/def ::cause #(instance? Exception %)) +(s/def ::cause #(instance? Throwable %)) (s/def ::error-params (s/keys :req-un [::type] :opt-un [::code diff --git a/backend/src/uxbox/util/http.clj b/backend/src/uxbox/util/http.clj index 3fd4b4561..4cbc1fb78 100644 --- a/backend/src/uxbox/util/http.clj +++ b/backend/src/uxbox/util/http.clj @@ -4,213 +4,5 @@ ;; ;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz> -(ns uxbox.util.http) - -(defn response - "Create a response instance." - ([body] (response body 200 {})) - ([body status] (response body status {})) - ([body status headers] {:body body :status status :headers headers})) - -(defn response? - [resp] - (and (map? resp) - (integer? (:status resp)) - (map? (:headers resp)))) - -(defn continue - ([body] (response body 100)) - ([body headers] (response body 100 headers))) - -(defn ok - "HTTP 200 OK - Should be used to indicate nonspecific success. Must not be used to - communicate errors in the response body. - - In most cases, 200 is the code the client hopes to see. It indicates that - the REST API successfully carried out whatever action the client requested, - and that no more specific code in the 2xx series is appropriate. Unlike - the 204 status code, a 200 response should include a response body." - ([body] (response body 200)) - ([body headers] (response body 200 headers))) - -(defn created - "HTTP 201 Created - Must be used to indicate successful resource creation. - - A REST API responds with the 201 status code whenever a collection creates, - or a store adds, a new resource at the client's request. There may also be - times when a new resource is created as a result of some controller action, - in which case 201 would also be an appropriate response." - ([location] (response "" 201 {"location" location})) - ([location body] (response body 201 {"location" location})) - ([location body headers] (response body 201 (merge headers {"location" location})))) - -(defn accepted - "HTTP 202 Accepted - Must be used to indicate successful start of an asynchronous action. - - A 202 response indicates that the client's request will be handled - asynchronously. This response status code tells the client that the request - appears valid, but it still may have problems once it's finally processed. - A 202 response is typically used for actions that take a long while to - process." - ([body] (response body 202)) - ([body headers] (response body 202 headers))) - -(defn no-content - "HTTP 204 No Content - Should be used when the response body is intentionally empty. - - The 204 status code is usually sent out in response to a PUT, POST, or - DELETE request, when the REST API declines to send back any status message - or representation in the response message's body. An API may also send 204 - in conjunction with a GET request to indicate that the requested resource - exists, but has no state representation to include in the body." - ([] (response "" 204)) - ([headers] (response "" 204 headers))) - -(defn moved-permanently - "301 Moved Permanently - Should be used to relocate resources. - - The 301 status code indicates that the REST API's resource model has been - significantly redesigned and a new permanent URI has been assigned to the - client's requested resource. The REST API should specify the new URI in - the response's Location header." - ([location] (response "" 301 {"location" location})) - ([location body] (response body 301 {"location" location})) - ([location body headers] (response body 301 (merge headers {"location" location})))) - -(defn found - "HTTP 302 Found - Should not be used. - - The intended semantics of the 302 response code have been misunderstood - by programmers and incorrectly implemented in programs since version 1.0 - of the HTTP protocol. - The confusion centers on whether it is appropriate for a client to always - automatically issue a follow-up GET request to the URI in response's - Location header, regardless of the original request's method. For the - record, the intent of 302 is that this automatic redirect behavior only - applies if the client's original request used either the GET or HEAD - method. - - To clear things up, HTTP 1.1 introduced status codes 303 (\"See Other\") - and 307 (\"Temporary Redirect\"), either of which should be used - instead of 302." - ([location] (response "" 302 {"location" location})) - ([location body] (response body 302 {"location" location})) - ([location body headers] (response body 302 (merge headers {"location" location})))) - -(defn see-other - "HTTP 303 See Other - Should be used to refer the client to a different URI. - - A 303 response indicates that a controller resource has finished its work, - but instead of sending a potentially unwanted response body, it sends the - client the URI of a response resource. This can be the URI of a temporary - status message, or the URI to some already existing, more permanent, - resource. - Generally speaking, the 303 status code allows a REST API to send a - reference to a resource without forcing the client to download its state. - Instead, the client may send a GET request to the value of the Location - header." - ([location] (response "" 303 {"location" location})) - ([location body] (response body 303 {"location" location})) - ([location body headers] (response body 303 (merge headers {"location" location})))) - -(defn temporary-redirect - "HTTP 307 Temporary Redirect - Should be used to tell clients to resubmit the request to another URI. - - HTTP/1.1 introduced the 307 status code to reiterate the originally - intended semantics of the 302 (\"Found\") status code. A 307 response - indicates that the REST API is not going to process the client's request. - Instead, the client should resubmit the request to the URI specified by - the response message's Location header. - - A REST API can use this status code to assign a temporary URI to the - client's requested resource. For example, a 307 response can be used to - shift a client request over to another host." - ([location] (response "" 307 {"location" location})) - ([location body] (response body 307 {"location" location})) - ([location body headers] (response body 307 (merge headers {"location" location})))) - -(defn bad-request - "HTTP 400 Bad Request - May be used to indicate nonspecific failure. - - 400 is the generic client-side error status, used when no other 4xx error - code is appropriate." - ([body] (response body 400)) - ([body headers] (response body 400 headers))) - -(defn unauthorized - "HTTP 401 Unauthorized - Must be used when there is a problem with the client credentials. - - A 401 error response indicates that the client tried to operate on a - protected resource without providing the proper authorization. It may have - provided the wrong credentials or none at all." - ([body] (response body 401)) - ([body headers] (response body 401 headers))) - -(defn forbidden - "HTTP 403 Forbidden - Should be used to forbid access regardless of authorization state. - - A 403 error response indicates that the client's request is formed - correctly, but the REST API refuses to honor it. A 403 response is not a - case of insufficient client credentials; that would be 401 (\"Unauthorized\"). - REST APIs use 403 to enforce application-level permissions. For example, a - client may be authorized to interact with some, but not all of a REST API's - resources. If the client attempts a resource interaction that is outside of - its permitted scope, the REST API should respond with 403." - ([body] (response body 403)) - ([body headers] (response body 403 headers))) - -(defn not-found - "HTTP 404 Not Found - Must be used when a client's URI cannot be mapped to a resource. - - The 404 error status code indicates that the REST API can't map the - client's URI to a resource." - ([body] (response body 404)) - ([body headers] (response body 404 headers))) - -(defn method-not-allowed - ([body] (response body 405)) - ([body headers] (response body 405 headers))) - -(defn not-acceptable - ([body] (response body 406)) - ([body headers] (response body 406 headers))) - -(defn conflict - ([body] (response body 409)) - ([body headers] (response body 409 headers))) - -(defn gone - ([body] (response body 410)) - ([body headers] (response body 410 headers))) - -(defn precondition-failed - ([body] (response body 412)) - ([body headers] (response body 412 headers))) - -(defn unsupported-mediatype - ([body] (response body 415)) - ([body headers] (response body 415 headers))) - -(defn too-many-requests - ([body] (response body 429)) - ([body headers] (response body 429 headers))) - -(defn internal-server-error - ([body] (response body 500)) - ([body headers] (response body 500 headers))) - -(defn not-implemented - ([body] (response body 501)) - ([body headers] (response body 501 headers))) +(ns uxbox.util.http + "Http related helpers.") diff --git a/backend/src/uxbox/util/sql.clj b/backend/src/uxbox/util/sql.clj index 44bef679d..48b4cab58 100644 --- a/backend/src/uxbox/util/sql.clj +++ b/backend/src/uxbox/util/sql.clj @@ -157,7 +157,9 @@ (into rp p) (first n) (rest n))) - [(str prefix (str/join join-with rs) suffix) rp])))) + (if (empty? rs) + ["" []] + [(str prefix (str/join join-with rs) suffix) rp]))))) (defn- process-param-tokens [sql] @@ -168,7 +170,7 @@ (def ^:private select-formatters [#(format-exprs (::select %) {:prefix "SELECT "}) #(format-exprs (::from %) {:prefix "FROM "}) - #(format-exprs (::join %)) + #(format-exprs (::join %) {:join-with " "}) #(format-exprs (::where %) {:prefix "WHERE (" :join-with ") AND (" :suffix ")"}) diff --git a/backend/test/uxbox/tests/helpers.clj b/backend/test/uxbox/tests/helpers.clj index 8e75991c4..ea2bda316 100644 --- a/backend/test/uxbox/tests/helpers.clj +++ b/backend/test/uxbox/tests/helpers.clj @@ -6,9 +6,10 @@ [cuerdas.core :as str] [mount.core :as mount] [datoteka.storages :as st] - [uxbox.services.mutations.profiles :as profiles] + [uxbox.services.mutations.users :as users] [uxbox.services.mutations.projects :as projects] - [uxbox.services.mutations.pages :as pages] + [uxbox.services.mutations.project-files :as files] + [uxbox.services.mutations.project-pages :as pages] [uxbox.fixtures :as fixtures] [uxbox.migrations] [uxbox.media] @@ -24,6 +25,8 @@ #'uxbox.config/secret #'uxbox.core/system #'uxbox.db/pool + #'uxbox.services.init/query-services + #'uxbox.services.init/mutation-services #'uxbox.migrations/migrations #'uxbox.media/assets-storage #'uxbox.media/media-storage @@ -64,39 +67,44 @@ (defn create-user [conn i] - (profiles/create-profile conn {:id (mk-uuid "user" i) - :fullname (str "User " i) - :username (str "user" i) - :email (str "user" i ".test@uxbox.io") - :password "123123" - :metadata {}})) + (users/create-profile conn {:id (mk-uuid "user" i) + :fullname (str "User " i) + :username (str "user" i) + :email (str "user" i ".test@uxbox.io") + :password "123123" + :metadata {}})) (defn create-project [conn user-id i] (projects/create-project conn {:id (mk-uuid "project" i) :user user-id + :version 1 :name (str "sample project " i)})) -(defn create-page - [conn uid pid i] + +(defn create-project-file + [conn user-id project-id i] + (files/create-file conn {:id (mk-uuid "project-file" i) + :user user-id + :project-id project-id + :name (str "sample project file" i)})) + + +(defn create-project-page + [conn user-id file-id i] (pages/create-page conn {:id (mk-uuid "page" i) - :user uid - :project-id pid + :user user-id + :file-id file-id :name (str "page" i) - :data {:shapes []} + :ordering i + :data {} :metadata {}})) (defn handle-error [err] - (cond - (instance? clojure.lang.ExceptionInfo err) - (ex-data err) - - (instance? java.util.concurrent.ExecutionException err) + (if (instance? java.util.concurrent.ExecutionException err) (handle-error (.getCause err)) - - :else - [err nil])) + err)) (defmacro try-on [expr] @@ -126,21 +134,28 @@ {:error (handle-error e#) :result nil}))) +(defn print-error! + [error] + (let [data (ex-data error)] + (cond + (= :spec-validation (:code data)) + (println (:explain data)) + + :else + (.printStackTrace error)))) + (defn print-result! [{:keys [error result]}] (if error (do (println "====> START ERROR") - (if (= :spec-validation (:code error)) - (s/explain-out (:data error)) - (prn error)) + (print-error! error) (println "====> END ERROR")) (do (println "====> START RESPONSE") (prn result) (println "====> END RESPONSE")))) - (defn exception? [v] (instance? Throwable v)) @@ -154,6 +169,11 @@ (let [data (ex-data e)] (= type (:type data)))) +(defn ex-of-code? + [e code] + (let [data (ex-data e)] + (= code (:code data)))) + (defn ex-with-code? [e code] (let [data (ex-data e)] diff --git a/backend/test/uxbox/tests/test_emails.clj b/backend/test/uxbox/tests/test_emails.clj index 9e853e974..248ddb71c 100644 --- a/backend/test/uxbox/tests/test_emails.clj +++ b/backend/test/uxbox/tests/test_emails.clj @@ -25,20 +25,20 @@ (t/is (contains? result :reply-to)) (t/is (vector? (:body result))))) -(t/deftest email-sending-and-sendmail-job - (let [res @(emails/send! emails/register {:to "example@uxbox.io" :name "foo"})] - (t/is (nil? res))) - (with-mock mock - {:target 'uxbox.jobs.sendmail/impl-sendmail - :return (p/resolved nil)} +;; (t/deftest email-sending-and-sendmail-job +;; (let [res @(emails/send! emails/register {:to "example@uxbox.io" :name "foo"})] +;; (t/is (nil? res))) +;; (with-mock mock +;; {:target 'uxbox.jobs.sendmail/impl-sendmail +;; :return (p/resolved nil)} - (let [res @(uxbox.jobs.sendmail/send-emails {})] - (t/is (= 1 res)) - (t/is (:called? @mock)) - (t/is (= 1 (:call-count @mock)))) +;; (let [res @(uxbox.jobs.sendmail/send-emails {})] +;; (t/is (= 1 res)) +;; (t/is (:called? @mock)) +;; (t/is (= 1 (:call-count @mock)))) - (let [res @(uxbox.jobs.sendmail/send-emails {})] - (t/is (= 0 res)) - (t/is (:called? @mock)) - (t/is (= 1 (:call-count @mock)))))) +;; (let [res @(uxbox.jobs.sendmail/send-emails {})] +;; (t/is (= 0 res)) +;; (t/is (:called? @mock)) +;; (t/is (= 1 (:call-count @mock)))))) diff --git a/backend/test/uxbox/tests/test_services_auth.clj b/backend/test/uxbox/tests/test_services_auth.clj index 36ef17214..35ac36b80 100644 --- a/backend/test/uxbox/tests/test_services_auth.clj +++ b/backend/test/uxbox/tests/test_services_auth.clj @@ -24,9 +24,14 @@ :scope "foobar"} out (th/try-on! (sm/handle event))] ;; (th/print-result! out) - (t/is (map? (:error out))) - (t/is (= (get-in out [:error :type]) :validation)) - (t/is (= (get-in out [:error :code]) :uxbox.services.mutations.auth/wrong-credentials)))) + (let [error (:error out)] + (t/is (th/ex-info? error)) + (t/is (th/ex-of-type? error :service-error))) + + (let [error (ex-cause (:error out))] + (t/is (th/ex-info? error)) + (t/is (th/ex-of-type? error :validation)) + (t/is (th/ex-of-code? error :uxbox.services.mutations.auth/wrong-credentials))))) (t/deftest success-auth (let [user @(th/create-user db/pool 1) diff --git a/backend/test/uxbox/tests/test_services_pages.clj b/backend/test/uxbox/tests/test_services_pages.clj deleted file mode 100644 index b1a5ba920..000000000 --- a/backend/test/uxbox/tests/test_services_pages.clj +++ /dev/null @@ -1,144 +0,0 @@ -(ns uxbox.tests.test-services-pages - (:require - [clojure.spec.alpha :as s] - [clojure.test :as t] - [promesa.core :as p] - [uxbox.db :as db] - [uxbox.http :as http] - [uxbox.services.mutations :as sm] - [uxbox.services.queries :as sq] - [uxbox.tests.helpers :as th])) - -(t/use-fixtures :once th/state-init) -(t/use-fixtures :each th/database-reset) - -(t/deftest mutation-create-page - (let [user @(th/create-user db/pool 1) - proj @(th/create-project db/pool (:id user) 1) - data {::sm/type :create-page - :data {:shapes []} - :metadata {} - :project-id (:id proj) - :name "test page" - :user (:id user)} - res (th/try-on! (sm/handle data))] - (t/is (nil? (:error res))) - (t/is (uuid? (get-in res [:result :id]))) - (let [rsp (:result res)] - (t/is (= (:user data) (:user-id rsp))) - (t/is (= (:name data) (:name rsp))) - (t/is (= (:data data) (:data rsp))) - (t/is (= (:metadata data) (:metadata rsp)))))) - -(t/deftest mutation-update-page - (let [user @(th/create-user db/pool 1) - proj @(th/create-project db/pool (:id user) 1) - page @(th/create-page db/pool (:id user) (:id proj) 1) - data {::sm/type :update-page - :id (:id page) - :data {:shapes [1 2 3]} - :metadata {:foo 2} - :project-id (:id proj) - :name "test page" - :user (:id user)} - res (th/try-on! (sm/handle data))] - - ;; (th/print-result! res) - - (t/is (nil? (:error res))) - (t/is (= (:id data) (get-in res [:result :id]))) - #_(t/is (= (:user data) (get-in res [:result :user-id]))) - #_(t/is (= (:name data) (get-in res [:result :name]))) - #_(t/is (= (:data data) (get-in res [:result :data]))) - #_(t/is (= (:metadata data) (get-in res [:result :metadata]))))) - -(t/deftest mutation-update-page-metadata - (let [user @(th/create-user db/pool 1) - proj @(th/create-project db/pool (:id user) 1) - page @(th/create-page db/pool (:id user) (:id proj) 1) - data {::sm/type :update-page-metadata - :id (:id page) - :metadata {:foo 2} - :project-id (:id proj) - :name "test page" - :user (:id user)} - res (th/try-on! (sm/handle data))] - - ;; (th/print-result! res) - (t/is (nil? (:error res))) - (t/is (= (:id data) (get-in res [:result :id]))) - (t/is (= (:user data) (get-in res [:result :user-id]))) - (t/is (= (:name data) (get-in res [:result :name]))) - (t/is (= (:metadata data) (get-in res [:result :metadata]))))) - -(t/deftest mutation-delete-page - (let [user @(th/create-user db/pool 1) - proj @(th/create-project db/pool (:id user) 1) - page @(th/create-page db/pool (:id user) (:id proj) 1) - data {::sm/type :delete-page - :id (:id page) - :user (:id user)} - res (th/try-on! (sm/handle data))] - - ;; (th/print-result! res) - (t/is (nil? (:error res))) - (t/is (nil? (:result res))))) - -(t/deftest query-pages-by-project - (let [user @(th/create-user db/pool 1) - proj @(th/create-project db/pool (:id user) 1) - page @(th/create-page db/pool (:id user) (:id proj) 1) - data {::sq/type :pages-by-project - :project-id (:id proj) - :user (:id user)} - res (th/try-on! (sq/handle data))] - - ;; (th/print-result! res) - (t/is (nil? (:error res))) - (t/is (vector? (:result res))) - (t/is (= 1 (count (:result res)))) - (t/is (= "page1" (get-in res [:result 0 :name]))) - (t/is (:id proj) (get-in res [:result 0 :project-id])))) - -;; (t/deftest http-page-history-update -;; (with-open [conn (db/connection)] -;; (let [user (th/create-user conn 1) -;; proj (uspr/create-project conn {:user (:id user) :name "proj1"}) -;; data {:id (uuid/random) -;; :user (:id user) -;; :project (:id proj) -;; :version 0 -;; :data "1" -;; :metadata "2" -;; :name "page1" -;; :width 200 -;; :height 200 -;; :layout "mobil"} -;; page (uspg/create-page conn data)] - -;; (dotimes [i 10] -;; (let [page (uspg/get-page-by-id conn (:id data))] -;; (uspg/update-page conn (assoc page :data (str i))))) - -;; ;; Check inserted history -;; (let [sql (str "SELECT * FROM pages_history " -;; " WHERE page=? ORDER BY created_at DESC") -;; result (sc/fetch conn [sql (:id data)]) -;; item (first result)] - -;; (th/with-server {:handler @http/app} -;; (let [uri (str th/+base-url+ -;; "/api/pages/" (:id page) -;; "/history/" (:id item)) -;; params {:body {:label "test" :pinned true}} -;; [status data] (th/http-put user uri params)] -;; ;; (println "RESPONSE:" status data) -;; (t/is (= 200 status)) -;; (t/is (= (:id data) (:id item)))))) - -;; (let [sql (str "SELECT * FROM pages_history " -;; " WHERE page=? AND pinned = true " -;; " ORDER BY created_at DESC") -;; result (sc/fetch-one conn [sql (:id data)])] -;; (t/is (= "test" (:label result))) -;; (t/is (= true (:pinned result))))))) diff --git a/backend/test/uxbox/tests/test_services_project_files.clj b/backend/test/uxbox/tests/test_services_project_files.clj new file mode 100644 index 000000000..c3ab27ddb --- /dev/null +++ b/backend/test/uxbox/tests/test_services_project_files.clj @@ -0,0 +1,76 @@ +(ns uxbox.tests.test-services-project-files + (:require + [clojure.test :as t] + [promesa.core :as p] + [uxbox.db :as db] + [uxbox.http :as http] + [uxbox.services.mutations :as sm] + [uxbox.services.queries :as sq] + [uxbox.tests.helpers :as th])) + +(t/use-fixtures :once th/state-init) +(t/use-fixtures :each th/database-reset) + +(t/deftest query-project-files + (let [user @(th/create-user db/pool 2) + proj @(th/create-project db/pool (:id user) 1) + pf @(th/create-project-file db/pool (:id user) (:id proj) 1) + pp @(th/create-project-page db/pool (:id user) (:id pf) 1) + data {::sq/type :project-files + :user (:id user) + :project-id (:id proj)} + out (th/try-on! (sq/handle data))] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (= 1 (count (:result out)))) + (t/is (= (:id pf) (get-in out [:result 0 :id]))) + (t/is (= (:id proj) (get-in out [:result 0 :project-id]))) + (t/is (= (:name pf) (get-in out [:result 0 :name]))) + (t/is (= [(:id pp)] (get-in out [:result 0 :pages]))))) + +(t/deftest mutation-create-project-file + (let [user @(th/create-user db/pool 1) + proj @(th/create-project db/pool (:id user) 1) + data {::sm/type :create-project-file + :user (:id user) + :name "test file" + :project-id (:id proj)} + out (th/try-on! (sm/handle data)) + ] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (= (:name data) (get-in out [:result :name]))) + #_(t/is (= (:project-id data) (get-in out [:result :project-id]))))) + +(t/deftest mutation-update-project-file + (let [user @(th/create-user db/pool 1) + proj @(th/create-project db/pool (:id user) 1) + pf @(th/create-project-file db/pool (:id user) (:id proj) 1) + data {::sm/type :update-project-file + :id (:id pf) + :name "new file name" + :user (:id user)} + out (th/try-on! (sm/handle data))] + ;; (th/print-result! out) + ;; TODO: check the result + (t/is (nil? (:error out))) + (t/is (nil? (:result out))))) + +(t/deftest mutation-delete-project-file + (let [user @(th/create-user db/pool 1) + proj @(th/create-project db/pool (:id user) 1) + pf @(th/create-project-file db/pool (:id user) (:id proj) 1) + data {::sm/type :delete-project-file + :id (:id pf) + :user (:id user)} + out (th/try-on! (sm/handle data))] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (nil? (:result out))) + + (let [sql "select * from project_files + where project_id=$1 and deleted_at is null" + res @(db/query db/pool [sql (:id proj)])] + (t/is (empty? res))))) + +;; ;; TODO: add permisions related tests diff --git a/backend/test/uxbox/tests/test_services_project_pages.clj b/backend/test/uxbox/tests/test_services_project_pages.clj new file mode 100644 index 000000000..3133c7824 --- /dev/null +++ b/backend/test/uxbox/tests/test_services_project_pages.clj @@ -0,0 +1,84 @@ +(ns uxbox.tests.test-services-project-pages + (:require + [clojure.spec.alpha :as s] + [clojure.test :as t] + [promesa.core :as p] + [uxbox.db :as db] + [uxbox.http :as http] + [uxbox.services.mutations :as sm] + [uxbox.services.queries :as sq] + [uxbox.tests.helpers :as th])) + +(t/use-fixtures :once th/state-init) +(t/use-fixtures :each th/database-reset) + +(t/deftest query-project-pages + (let [user @(th/create-user db/pool 1) + proj @(th/create-project db/pool (:id user) 1) + file @(th/create-project-file db/pool (:id user) (:id proj) 1) + page @(th/create-project-page db/pool (:id user) (:id file) 1) + data {::sq/type :project-pages + :file-id (:id file) + :user (:id user)} + out (th/try-on! (sq/handle data))] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (vector? (:result out))) + (t/is (= 1 (count (:result out)))) + (t/is (= "page1" (get-in out [:result 0 :name]))) + (t/is (:id file) (get-in out [:result 0 :file-id])))) + +(t/deftest mutation-create-project-page + (let [user @(th/create-user db/pool 1) + proj @(th/create-project db/pool (:id user) 1) + pf @(th/create-project-file db/pool (:id user) (:id proj) 1) + + data {::sm/type :create-project-page + :data {} + :metadata {} + :file-id (:id pf) + :ordering 1 + :name "test page" + :user (:id user)} + out (th/try-on! (sm/handle data))] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (uuid? (get-in out [:result :id]))) + (t/is (= (:user data) (get-in out [:result :user-id]))) + (t/is (= (:name data) (get-in out [:result :name]))) + (t/is (= (:data data) (get-in out [:result :data]))) + (t/is (= (:metadata data) (get-in out [:result :metadata]))) + (t/is (= 0 (get-in out [:result :version]))))) + +(t/deftest mutation-update-project-page-data + (let [user @(th/create-user db/pool 1) + proj @(th/create-project db/pool (:id user) 1) + file @(th/create-project-file db/pool (:id user) (:id proj) 1) + 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]} + :file-id (:id file) + :user (:id user)} + out (th/try-on! (sm/handle data))] + + ;; (th/print-result! out) + ;; TODO: check history creation + ;; TODO: check correct page data update operation + + (t/is (nil? (:error out))) + (t/is (= (:id data) (get-in out [:result :id]))) + (t/is (= 1 (get-in out [:result :version]))))) + +(t/deftest mutation-delete-project-page + (let [user @(th/create-user db/pool 1) + proj @(th/create-project db/pool (:id user) 1) + file @(th/create-project-file db/pool (:id user) (:id proj) 1) + page @(th/create-project-page db/pool (:id user) (:id file) 1) + data {::sm/type :delete-project-page + :id (:id page) + :user (:id user)} + out (th/try-on! (sm/handle data))] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (nil? (:result out))))) diff --git a/backend/test/uxbox/tests/test_services_projects.clj b/backend/test/uxbox/tests/test_services_projects.clj index eb5d4cc61..9d354fa76 100644 --- a/backend/test/uxbox/tests/test_services_projects.clj +++ b/backend/test/uxbox/tests/test_services_projects.clj @@ -11,17 +11,13 @@ (t/use-fixtures :once th/state-init) (t/use-fixtures :each th/database-reset) -;; TODO: migrate from try-on to try-on! - -(t/deftest query-project-list +(t/deftest query-projects (let [user @(th/create-user db/pool 1) proj @(th/create-project db/pool (:id user) 1) data {::sq/type :projects :user (:id user)} out (th/try-on! (sq/handle data))] - ;; (th/print-result! out) - (t/is (nil? (:error out))) (t/is (= 1 (count (:result out)))) (t/is (= (:id proj) (get-in out [:result 0 :id]))) @@ -32,11 +28,10 @@ data {::sm/type :create-project :user (:id user) :name "test project"} - [err rsp] (th/try-on (sm/handle data))] - ;; (prn "RESPONSE:" err rsp) - (t/is (nil? err)) - (t/is (= (:user data) (:user-id rsp))) - (t/is (= (:name data) (:name rsp))))) + out (th/try-on! (sm/handle data))] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (= (:name data) (get-in out [:result :name]))))) (t/deftest mutation-update-project (let [user @(th/create-user db/pool 1) @@ -45,12 +40,12 @@ :id (:id proj) :name "test project mod" :user (:id user)} - [err rsp] (th/try-on (sm/handle data))] - ;; (prn "RESPONSE:" err rsp) - (t/is (nil? err)) - (t/is (= (:id data) (:id rsp))) - (t/is (= (:user data) (:user-id rsp))) - (t/is (= (:name data) (:name rsp))))) + out (th/try-on! (sm/handle data))] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (= (:id data) (get-in out [:result :id]))) + (t/is (= (:user data) (get-in out [:result :user-id]))) + (t/is (= (:name data) (get-in out [:result :name]))))) (t/deftest mutation-delete-project (let [user @(th/create-user db/pool 1) @@ -58,12 +53,13 @@ data {::sm/type :delete-project :id (:id proj) :user (:id user)} - [err rsp] (th/try-on (sm/handle data))] - ;; (prn "RESPONSE:" err rsp) - (t/is (nil? err)) - (t/is (nil? rsp)) + out (th/try-on! (sm/handle data))] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (nil? (:result out))) - (let [sql "SELECT * FROM projects - WHERE user_id=$1 AND deleted_at is null" + (let [sql "select * from projects where user_id=$1 and deleted_at is null" res @(db/query db/pool [sql (:id user)])] (t/is (empty? res))))) + +;; TODO: add permisions related tests diff --git a/backend/test/uxbox/tests/test_services_user_storage.clj b/backend/test/uxbox/tests/test_services_user_attrs.clj similarity index 62% rename from backend/test/uxbox/tests/test_services_user_storage.clj rename to backend/test/uxbox/tests/test_services_user_attrs.clj index 8585f5e96..db9826d9e 100644 --- a/backend/test/uxbox/tests/test_services_user_storage.clj +++ b/backend/test/uxbox/tests/test_services_user_attrs.clj @@ -1,4 +1,4 @@ -(ns uxbox.tests.test-services-user-storage +(ns uxbox.tests.test-services-user-attrs (:require [clojure.spec.alpha :as s] [clojure.test :as t] @@ -12,16 +12,22 @@ (t/use-fixtures :once th/state-init) (t/use-fixtures :each th/database-reset) -(t/deftest test-user-storage +(t/deftest test-user-attrs (let [{:keys [id] :as user} @(th/create-user db/pool 1)] - (let [out (th/try-on! (sq/handle {::sq/type :user-storage-entry + (let [out (th/try-on! (sq/handle {::sq/type :user-attr :key "foobar" :user id}))] (t/is (nil? (:result out))) - (t/is (map? (:error out))) - (t/is (= :not-found (get-in out [:error :type])))) - (let [out (th/try-on! (sm/handle {::sm/type :upsert-user-storage-entry + (let [error (:error out)] + (t/is (th/ex-info? error)) + (t/is (th/ex-of-type? error :service-error))) + + (let [error (ex-cause (:error out))] + (t/is (th/ex-info? error)) + (t/is (th/ex-of-type? error :not-found)))) + + (let [out (th/try-on! (sm/handle {::sm/type :upsert-user-attr :user id :key "foobar" :val {:some #{:value}}}))] @@ -29,7 +35,7 @@ (t/is (nil? (:error out))) (t/is (nil? (:result out)))) - (let [out (th/try-on! (sq/handle {::sq/type :user-storage-entry + (let [out (th/try-on! (sq/handle {::sq/type :user-attr :key "foobar" :user id}))] ;; (th/print-result! out) @@ -37,18 +43,23 @@ (t/is (= {:some #{:value}} (get-in out [:result :val]))) (t/is (= "foobar" (get-in out [:result :key])))) - (let [out (th/try-on! (sm/handle {::sm/type :delete-user-storage-entry + (let [out (th/try-on! (sm/handle {::sm/type :delete-user-attr :user id :key "foobar"}))] ;; (th/print-result! out) (t/is (nil? (:error out))) (t/is (nil? (:result out)))) - (let [out (th/try-on! (sq/handle {::sq/type :user-storage-entry + (let [out (th/try-on! (sq/handle {::sq/type :user-attr :key "foobar" :user id}))] ;; (th/print-result! out) (t/is (nil? (:result out))) - (t/is (map? (:error out))) - (t/is (= :not-found (get-in out [:error :type])))))) + (let [error (:error out)] + (t/is (th/ex-info? error)) + (t/is (th/ex-of-type? error :service-error))) + + (let [error (ex-cause (:error out))] + (t/is (th/ex-info? error)) + (t/is (th/ex-of-type? error :not-found)))))) diff --git a/backend/test/uxbox/tests/test_users.clj b/backend/test/uxbox/tests/test_services_users.clj similarity index 65% rename from backend/test/uxbox/tests/test_users.clj rename to backend/test/uxbox/tests/test_services_users.clj index f761b32ed..ab64fb38d 100644 --- a/backend/test/uxbox/tests/test_users.clj +++ b/backend/test/uxbox/tests/test_services_users.clj @@ -1,11 +1,10 @@ -(ns uxbox.tests.test-users +(ns uxbox.tests.test-services-users (:require [clojure.test :as t] [clojure.java.io :as io] [promesa.core :as p] [cuerdas.core :as str] [datoteka.core :as fs] - [vertx.core :as vc] [uxbox.db :as db] [uxbox.services.mutations :as sm] [uxbox.services.queries :as sq] @@ -16,47 +15,48 @@ (t/deftest test-query-profile (let [user @(th/create-user db/pool 1) - event {::sq/type :profile - :user (:id user)} - [err rsp] (th/try-on (sq/handle event))] - ;; (println "RESPONSE:" resp))) - (t/is (nil? err)) - (t/is (= (:fullname rsp) "User 1")) - (t/is (= (:username rsp) "user1")) - (t/is (= (:metadata rsp) {})) - (t/is (= (:email rsp) "user1.test@uxbox.io")) - (t/is (not (contains? rsp :password))))) + data {::sq/type :profile + :user (:id user)} + + out (th/try-on! (sq/handle data))] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (= "User 1" (get-in out [:result :fullname]))) + (t/is (= "user1" (get-in out [:result :username]))) + (t/is (= "user1.test@uxbox.io" (get-in out [:result :email]))) + (t/is (not (contains? (:result out) :password))))) (t/deftest test-mutation-update-profile - (let [user @(th/create-user db/pool 1) - event (assoc user - ::sm/type :update-profile - :fullname "Full Name" - :username "user222" - :metadata {:foo "bar"} - :email "user222@uxbox.io") - [err data] (th/try-on (sm/handle event))] - ;; (println "RESPONSE:" err data) - (t/is (nil? err)) - (t/is (= (:fullname data) "Full Name")) - (t/is (= (:username data) "user222")) - (t/is (= (:metadata data) {:foo "bar"})) - (t/is (= (:email data) "user222@uxbox.io")) - (t/is (not (contains? data :password))))) + (let [user @(th/create-user db/pool 1) + data (assoc user + ::sm/type :update-profile + :fullname "Full Name" + :username "user222" + :metadata {:foo "bar"} + :email "user222@uxbox.io") + out (th/try-on! (sm/handle data))] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (= (:fullname data) (get-in out [:result :fullname]))) + (t/is (= (:username data) (get-in out [:result :username]))) + (t/is (= (:email data) (get-in out [:result :email]))) + (t/is (= (:metadata data) (get-in out [:result :metadata]))) + (t/is (not (contains? (:result out) :password))))) (t/deftest test-mutation-update-profile-photo (let [user @(th/create-user db/pool 1) - event {::sm/type :update-profile-photo - :user (:id user) - :file {:name "sample.jpg" - :path (fs/path "test/uxbox/tests/_files/sample.jpg") - :size 123123 - :mtype "image/jpeg"}} - [err rsp] (th/try-on (sm/handle event))] - ;; (prn "RESPONSE:" [err rsp]) - (t/is (nil? err)) - (t/is (= (:id user) (:id rsp))) - (t/is (str/starts-with? (:photo rsp) "http")))) + data {::sm/type :update-profile-photo + :user (:id user) + :file {:name "sample.jpg" + :path (fs/path "test/uxbox/tests/_files/sample.jpg") + :size 123123 + :mtype "image/jpeg"}} + + out (th/try-on! (sm/handle data))] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (= (:id user) (get-in out [:result :id]))) + (t/is (str/starts-with? (get-in out [:result :photo]) "http")))) ;; (t/deftest test-mutation-register-profile ;; (let[data {:fullname "Full Name" diff --git a/backend/test/uxbox/tests/test_util_svg.clj b/backend/test/uxbox/tests/test_util_svg.clj index a58c2c6f6..75270bb6b 100644 --- a/backend/test/uxbox/tests/test_util_svg.clj +++ b/backend/test/uxbox/tests/test_util_svg.clj @@ -34,19 +34,20 @@ (t/deftest parse-invalid-svg-1 (let [image (io/resource "uxbox/tests/_files/sample.jpg") - result (th/try! (svg/parse image))] - (t/is (map? (:error result))) - (t/is (= (get-in result [:error :code]) - ::svg/invalid-input)))) + out (th/try! (svg/parse image))] + + (let [error (:error out)] + (t/is (th/ex-info? error)) + (t/is (th/ex-of-code? error ::svg/invalid-input))))) (t/deftest parse-invalid-svg-2 - (let [result (th/try! (svg/parse-string ""))] - (t/is (map? (:error result))) - (t/is (= (get-in result [:error :code]) - ::svg/invalid-input)))) + (let [out (th/try! (svg/parse-string ""))] + (let [error (:error out)] + (t/is (th/ex-info? error)) + (t/is (th/ex-of-code? error ::svg/invalid-input))))) (t/deftest parse-invalid-svg-3 - (let [result (th/try! (svg/parse-string "<svg></svg>"))] - (t/is (map? (:error result))) - (t/is (= (get-in result [:error :code]) - ::svg/invalid-result)))) + (let [out (th/try! (svg/parse-string "<svg></svg>"))] + (let [error (:error out)] + (t/is (th/ex-info? error)) + (t/is (th/ex-of-code? error ::svg/invalid-result)))))