diff --git a/backend/resources/migrations/0001.main.sql b/backend/resources/migrations/0001.main.sql index 2054c8cdbd..3f79521ccd 100644 --- a/backend/resources/migrations/0001.main.sql +++ b/backend/resources/migrations/0001.main.sql @@ -1,8 +1,6 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE EXTENSION IF NOT EXISTS "pgcrypto"; --- Modified At - CREATE FUNCTION update_modified_at() RETURNS TRIGGER AS $updt$ BEGIN @@ -10,3 +8,19 @@ CREATE FUNCTION update_modified_at() RETURN NEW; END; $updt$ LANGUAGE plpgsql; + +CREATE TABLE pending_to_delete ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + created_at timestamptz NOT NULL DEFAULT clock_timestamp(), + type text NOT NULL, + data jsonb NOT NULL +); + +CREATE FUNCTION handle_delete() + RETURNS TRIGGER AS $pagechange$ + BEGIN + INSERT INTO pending_to_delete (type, data) + VALUES (TG_TABLE_NAME, row_to_json(OLD)); + RETURN OLD; + END; +$pagechange$ LANGUAGE plpgsql; diff --git a/backend/resources/migrations/0002.users.sql b/backend/resources/migrations/0002.users.sql index 1e7ded169f..23dc551d36 100644 --- a/backend/resources/migrations/0002.users.sql +++ b/backend/resources/migrations/0002.users.sql @@ -1,4 +1,4 @@ -CREATE TABLE users ( +CREATE TABLE profile ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), created_at timestamptz NOT NULL DEFAULT clock_timestamp(), @@ -14,18 +14,26 @@ CREATE TABLE users ( is_demo boolean NOT NULL DEFAULT false ); -CREATE UNIQUE INDEX users__email__idx - ON users (email) +CREATE UNIQUE INDEX profile__email__idx + ON profile (email) WHERE deleted_at IS null; -CREATE INDEX users__is_demo - ON users (is_demo) +CREATE INDEX profile__is_demo + ON profile (is_demo) WHERE deleted_at IS null AND is_demo IS true; ---- Table used for register all used emails by the user -CREATE TABLE user_emails ( - user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, +INSERT INTO profile (id, fullname, email, photo, password) +VALUES ('00000000-0000-0000-0000-000000000000'::uuid, + 'System Profile', + 'system@uxbox.io', + '', + '!'); + + + +CREATE TABLE profile_email ( + profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE, created_at timestamptz NOT NULL DEFAULT clock_timestamp(), verified_at timestamptz NULL DEFAULT NULL, @@ -36,13 +44,58 @@ CREATE TABLE user_emails ( is_verified boolean NOT NULL DEFAULT false ); -CREATE INDEX user_emails__user_id__idx - ON user_emails (user_id); +CREATE INDEX profile_email__profile_id__idx + ON profile_email (profile_id); ---- Table for user key value attributes +CREATE UNIQUE INDEX profile_email__email__idx + ON profile_email (email); -CREATE TABLE user_attrs ( - user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + + +CREATE TABLE team ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + + created_at timestamptz NOT NULL DEFAULT clock_timestamp(), + modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), + deleted_at timestamptz NULL, + + name text NOT NULL, + photo text NOT NULL, + + is_default boolean NOT NULL DEFAULT false +); + +CREATE TRIGGER team__modified_at__tgr +BEFORE UPDATE ON team + FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); + + + +CREATE TABLE team_profile_rel ( + team_id uuid NOT NULL REFERENCES team(id) ON DELETE CASCADE, + profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE RESTRICT, + + created_at timestamptz NOT NULL DEFAULT clock_timestamp(), + modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), + + is_admin boolean DEFAULT false, + is_owner boolean DEFAULT false, + can_edit boolean DEFAULT false, + + PRIMARY KEY (team_id, profile_id) +); + +COMMENT ON TABLE team_profile_rel + IS 'Relation between teams and profiles (NM)'; + +CREATE TRIGGER team_profile_rel__modified_at__tgr +BEFORE UPDATE ON team_profile_rel + FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); + + + +CREATE TABLE profile_attr ( + profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE, created_at timestamptz NOT NULL DEFAULT clock_timestamp(), modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), @@ -50,52 +103,39 @@ CREATE TABLE user_attrs ( key text NOT NULL, val bytea NOT NULL, - PRIMARY KEY (key, user_id) + PRIMARY KEY (key, profile_id) ); ---- Table for store verification tokens +CREATE INDEX profile_attr__profile_id__idx + ON profile_attr(profile_id); -CREATE TABLE tokens ( - user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, +CREATE TRIGGER profile_attr__modified_at__tgr +BEFORE UPDATE ON profile_attr + FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); + + + +CREATE TABLE password_recovery_token ( + profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE, token text NOT NULL, created_at timestamptz NOT NULL DEFAULT clock_timestamp(), used_at timestamptz NULL, - PRIMARY KEY (token, user_id) + PRIMARY KEY (profile_id, token) ); ---- Table for store user sessions. -CREATE TABLE sessions ( + +CREATE TABLE session ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), created_at timestamptz NOT NULL DEFAULT clock_timestamp(), modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), - user_id uuid REFERENCES users(id) ON DELETE CASCADE, + profile_id uuid REFERENCES profile(id) ON DELETE CASCADE, user_agent text NULL ); -CREATE INDEX sessions__user_id__idx - ON sessions (user_id); - --- Insert a placeholder system user. - -INSERT INTO users (id, fullname, email, photo, password) -VALUES ('00000000-0000-0000-0000-000000000000'::uuid, - 'System User', - 'system@uxbox.io', - '', - '!'); - ---- Triggers - -CREATE TRIGGER users__modified_at__tgr -BEFORE UPDATE ON users - FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); - -CREATE TRIGGER user_attrs__modified_at__tgr -BEFORE UPDATE ON user_attrs - FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); - +CREATE INDEX session__profile_id__idx + ON session(profile_id); diff --git a/backend/resources/migrations/0003.projects.sql b/backend/resources/migrations/0003.projects.sql index 5be4d3bb62..435f3a2aba 100644 --- a/backend/resources/migrations/0003.projects.sql +++ b/backend/resources/migrations/0003.projects.sql @@ -1,8 +1,53 @@ --- Tables - -CREATE TABLE projects ( +CREATE TABLE project ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), - user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + team_id uuid NOT NULL REFERENCES team(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, + + is_default boolean NOT NULL DEFAULT false, + + name text NOT NULL +); + +CREATE INDEX project__team_id__idx + ON project(team_id); + +CREATE TRIGGER project__modified_at__tgr +BEFORE UPDATE ON project + FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); + + + +CREATE TABLE project_profile_rel ( + profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE, + project_id uuid NOT NULL REFERENCES project(id) ON DELETE CASCADE, + + created_at timestamptz NOT NULL DEFAULT clock_timestamp(), + modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), + + is_owner boolean DEFAULT false, + is_admin boolean DEFAULT false, + can_edit boolean DEFAULT false, + + PRIMARY KEY (profile_id, project_id) +); + +COMMENT ON TABLE project_profile_rel + IS 'Relation between projects and profiles (NM)'; + +CREATE INDEX project_profile_rel__profile_id__idx + ON project_profile_rel(profile_id); + +CREATE INDEX project_profile_rel__project_id__idx + ON project_profile_rel(project_id); + + + +CREATE TABLE file ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + project_id uuid NOT NULL REFERENCES project(id) ON DELETE CASCADE, created_at timestamptz NOT NULL DEFAULT clock_timestamp(), modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), @@ -11,49 +56,47 @@ CREATE TABLE projects ( name text NOT NULL ); -CREATE INDEX projects__user_id__idx - ON projects(user_id); +CREATE TRIGGER file__modified_at__tgr +BEFORE UPDATE ON file + FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); -CREATE TABLE project_users ( - user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, - project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE, + +CREATE TABLE file_profile_rel ( + file_id uuid NOT NULL REFERENCES file(id) ON DELETE CASCADE, + profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE, created_at timestamptz NOT NULL DEFAULT clock_timestamp(), modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), + is_owner boolean DEFAULT false, + is_admin boolean DEFAULT false, can_edit boolean DEFAULT false, - PRIMARY KEY (user_id, project_id) + PRIMARY KEY (file_id, profile_id) ); -CREATE INDEX project_users__user_id__idx - ON project_users(user_id); +COMMENT ON TABLE file_profile_rel + IS 'Relation between files and profiles (NM)'; -CREATE INDEX project_users__project_id__idx - ON project_users(project_id); +CREATE INDEX file_profile_rel__profile_id__idx + ON file_profile_rel(profile_id); -CREATE TABLE project_files ( +CREATE INDEX file_profile_rel__file_id__idx + ON file_profile_rel(file_id); + +CREATE TRIGGER file_profile_rel__modified_at__tgr +BEFORE UPDATE ON file + FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); + + + +CREATE TABLE file_image ( 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, + file_id uuid NOT NULL REFERENCES file(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 -); - -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 TABLE project_file_images ( - id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), - file_id uuid NOT NULL REFERENCES project_files(id) ON DELETE CASCADE, - user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + deleted_at timestamptz DEFAULT NULL, name text NOT NULL, @@ -69,103 +112,56 @@ CREATE TABLE project_file_images ( thumb_mtype text NOT NULL ); -CREATE INDEX project_file_images__file_id__idx - ON project_file_images(file_id); +CREATE INDEX file_image__file_id__idx + ON file_image(file_id); -CREATE INDEX project_file_images__user_id__idx - ON project_file_images(user_id); +CREATE TRIGGER file_image__modified_at__tgr +BEFORE UPDATE ON file_image + FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); -CREATE TABLE 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, +CREATE TRIGGER file_image__on_delete__tgr + AFTER DELETE ON file_image + FOR EACH ROW EXECUTE PROCEDURE handle_delete(); - 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 INDEX project_file_users__user_id__idx - ON project_file_users(user_id); - -CREATE INDEX project_file_users__file_id__idx - ON project_file_users(file_id); - -CREATE TABLE project_pages ( +CREATE TABLE page ( 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, + file_id uuid NOT NULL REFERENCES file(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, + version bigint NOT NULL DEFAULT 0, + revn bigint NOT NULL DEFAULT 0, + ordering smallint NOT NULL, name text NOT NULL, data bytea NOT NULL ); -CREATE INDEX project_pages__user_id__idx - ON project_pages(user_id); +CREATE INDEX page__file_id__idx + ON page(file_id); -CREATE INDEX project_pages__file_id__idx - ON project_pages(file_id); - -CREATE TABLE project_page_snapshots ( - 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, - changes bytea NULL DEFAULT NULL -); - -CREATE INDEX project_page_snapshots__user_id__idx - ON project_page_snapshots(user_id); - -CREATE INDEX project_page_snapshots__page_id_id__idx - ON project_page_snapshots(page_id); - --- Triggers - -CREATE OR REPLACE FUNCTION handle_project_insert() - RETURNS TRIGGER AS $$ - BEGIN - 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() +CREATE FUNCTION handle_page_update() RETURNS TRIGGER AS $pagechange$ DECLARE current_dt timestamptz := clock_timestamp(); proj_id uuid; BEGIN - UPDATE project_files + NEW.modified_at := current_dt; + + UPDATE file SET modified_at = current_dt WHERE id = OLD.file_id - RETURNING project_id + RETURNING project_id INTO STRICT proj_id; --- Update projects modified_at attribute when a --- page of that project is modified. - UPDATE projects + UPDATE project SET modified_at = current_dt WHERE id = proj_id; @@ -173,27 +169,60 @@ CREATE OR REPLACE FUNCTION handle_page_update() END; $pagechange$ LANGUAGE plpgsql; -CREATE TRIGGER projects_on_insert_tgr - AFTER INSERT ON projects - FOR EACH ROW EXECUTE PROCEDURE handle_project_insert(); - -CREATE TRIGGER pages__on_update__tgr -BEFORE UPDATE ON project_pages +CREATE TRIGGER page__on_update__tgr +BEFORE UPDATE ON page FOR EACH ROW EXECUTE PROCEDURE handle_page_update(); -CREATE TRIGGER projects__modified_at__tgr -BEFORE UPDATE ON projects + +CREATE TABLE page_version ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + + page_id uuid NOT NULL REFERENCES page(id) ON DELETE CASCADE, + profile_id uuid NULL REFERENCES profile(id) ON DELETE SET NULL, + + 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 DEFAULT 0, + + label text NOT NULL DEFAULT '', + data bytea NOT NULL, + + changes bytea NULL DEFAULT NULL +); + +CREATE INDEX page_version__profile_id__idx + ON page_version(profile_id); + +CREATE INDEX page_version__page_id__idx + ON page_version(page_id); + +CREATE TRIGGER page_version__modified_at__tgr +BEFORE UPDATE ON page_version 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_snapshots__modified_at__tgr -BEFORE UPDATE ON project_page_snapshots +CREATE TABLE page_change ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + page_id uuid NOT NULL REFERENCES page(id) ON DELETE CASCADE, + + created_at timestamptz NOT NULL DEFAULT clock_timestamp(), + modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), + + revn bigint NOT NULL DEFAULT 0, + + label text NOT NULL DEFAULT '', + data bytea NOT NULL, + + changes bytea NULL DEFAULT NULL +); + +CREATE INDEX page_change__page_id__idx + ON page_change(page_id); + +CREATE TRIGGER page_change__modified_at__tgr +BEFORE UPDATE ON page_change FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); diff --git a/backend/resources/migrations/0004.tasks.sql b/backend/resources/migrations/0004.tasks.sql index e2bb1c245d..058c836b0c 100644 --- a/backend/resources/migrations/0004.tasks.sql +++ b/backend/resources/migrations/0004.tasks.sql @@ -1,6 +1,4 @@ ---- Tables - -CREATE TABLE tasks ( +CREATE TABLE task ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), created_at timestamptz NOT NULL DEFAULT clock_timestamp(), @@ -14,16 +12,21 @@ CREATE TABLE tasks ( props bytea NOT NULL, error text NULL DEFAULT NULL, - result bytea NULL DEFAULT NULL, retry_num smallint NOT NULL DEFAULT 0, status text NOT NULL DEFAULT 'new' ); -CREATE INDEX tasks__scheduled_at__queue__idx - ON tasks (scheduled_at, queue); +CREATE INDEX task__scheduled_at__queue__idx + ON task (scheduled_at, queue); -CREATE TABLE scheduled_tasks ( +CREATE TRIGGER task__modified_at__tgr +BEFORE UPDATE ON task + FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); + + + +CREATE TABLE scheduled_task ( id text PRIMARY KEY, created_at timestamptz NOT NULL DEFAULT clock_timestamp(), @@ -33,9 +36,6 @@ CREATE TABLE scheduled_tasks ( cron_expr text NOT NULL ); ---- Triggers - -CREATE TRIGGER scheduled_tasks__modified_at__tgr -BEFORE UPDATE ON scheduled_tasks +CREATE TRIGGER scheduled_task__modified_at__tgr +BEFORE UPDATE ON scheduled_task FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); - diff --git a/backend/resources/migrations/0005.images.sql b/backend/resources/migrations/0005.images.sql index 2467417426..cf62f053fb 100644 --- a/backend/resources/migrations/0005.images.sql +++ b/backend/resources/migrations/0005.images.sql @@ -1,6 +1,6 @@ -CREATE TABLE image_collections ( +CREATE TABLE image_collection ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), - user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE, created_at timestamptz NOT NULL DEFAULT clock_timestamp(), modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), @@ -9,13 +9,19 @@ CREATE TABLE image_collections ( name text NOT NULL ); -CREATE INDEX image_collections__user_id__idx - ON image_collections(user_id); +CREATE INDEX image_collection__profile_id__idx + ON image_collection(profile_id); -CREATE TABLE images ( +CREATE TRIGGER image_collection__modified_at__tgr +BEFORE UPDATE ON image_collection + FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); + + + +CREATE TABLE image ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), - user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, - collection_id uuid NOT NULL REFERENCES image_collections(id) ON DELETE CASCADE, + profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE, + collection_id uuid NOT NULL REFERENCES image_collection(id) ON DELETE CASCADE, created_at timestamptz NOT NULL DEFAULT clock_timestamp(), modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), @@ -35,17 +41,17 @@ CREATE TABLE images ( thumb_mtype text NOT NULL ); -CREATE INDEX images__user_id__idx - ON images(user_id); +CREATE INDEX image__profile_id__idx + ON image(profile_id); -CREATE INDEX images__collection_id__idx - ON images(collection_id); +CREATE INDEX image__collection_id__idx + ON image(collection_id); -CREATE TRIGGER image_collections__modified_at__tgr -BEFORE UPDATE ON image_collections +CREATE TRIGGER image__modified_at__tgr +BEFORE UPDATE ON image 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 image__on_delete__tgr + AFTER DELETE ON image + FOR EACH ROW EXECUTE PROCEDURE handle_delete(); diff --git a/backend/resources/migrations/0006.icons.sql b/backend/resources/migrations/0006.icons.sql index 6113538d37..277f8332ff 100644 --- a/backend/resources/migrations/0006.icons.sql +++ b/backend/resources/migrations/0006.icons.sql @@ -1,8 +1,6 @@ --- Tables - -CREATE TABLE icon_collections ( +CREATE TABLE icon_collection ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), - user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE, created_at timestamptz NOT NULL DEFAULT clock_timestamp(), modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), @@ -11,35 +9,36 @@ CREATE TABLE icon_collections ( name text NOT NULL ); -CREATE TABLE icons ( +CREATE INDEX icon_colection__profile_id__idx + ON icon_collection (profile_id); + +CREATE TRIGGER icon_collection__modified_at__tgr +BEFORE UPDATE ON icon_collection + FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); + + + +CREATE TABLE icon ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), - user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + profile_id uuid NOT NULL REFERENCES profile(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, + collection_id uuid REFERENCES icon_collection(id) + ON DELETE CASCADE, + name text NOT NULL, content text NOT NULL, - metadata bytea NOT NULL, - - collection_id uuid REFERENCES icon_collections(id) - ON DELETE SET NULL - DEFAULT NULL + metadata bytea NOT NULL ); --- Indexes +CREATE INDEX icon__profile_id__idx + ON icon(profile_id); +CREATE INDEX icon__collection_id__idx + ON icon(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 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 +CREATE TRIGGER icon__modified_at__tgr +BEFORE UPDATE ON icon FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); diff --git a/backend/resources/migrations/0007.colors.sql b/backend/resources/migrations/0007.colors.sql new file mode 100644 index 0000000000..d09c8dfd22 --- /dev/null +++ b/backend/resources/migrations/0007.colors.sql @@ -0,0 +1,43 @@ +CREATE TABLE color_collection ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + profile_id uuid NOT NULL REFERENCES profile(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, + + name text NOT NULL +); + +CREATE INDEX color_colection__profile_id__idx + ON color_collection (profile_id); + +CREATE TRIGGER color_collection__modified_at__tgr +BEFORE UPDATE ON color_collection + FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); + + + +CREATE TABLE color ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + profile_id uuid NOT NULL REFERENCES profile(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, + + collection_id uuid REFERENCES color_collection(id) + ON DELETE CASCADE, + + name text NOT NULL, + content text NOT NULL +); + +CREATE INDEX color__profile_id__idx + ON color(profile_id); +CREATE INDEX color__collection_id__idx + ON color(collection_id); + +CREATE TRIGGER color__modified_at__tgr +BEFORE UPDATE ON color + FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); diff --git a/backend/src/uxbox/config.clj b/backend/src/uxbox/config.clj index c985a5cec0..7c5199dbb2 100644 --- a/backend/src/uxbox/config.clj +++ b/backend/src/uxbox/config.clj @@ -16,7 +16,8 @@ [cuerdas.core :as str] [environ.core :refer [env]] [mount.core :refer [defstate]] - [uxbox.common.exceptions :as ex])) + [uxbox.common.exceptions :as ex] + [uxbox.util.time :as tm])) (def defaults {:http-server-port 6060 @@ -106,3 +107,6 @@ (defstate config :start (read-config env)) + +(def default-deletion-delay + (tm/duration {:hours 48})) diff --git a/backend/src/uxbox/fixtures.clj b/backend/src/uxbox/fixtures.clj index 72ac7bcae1..5c832d7dd8 100644 --- a/backend/src/uxbox/fixtures.clj +++ b/backend/src/uxbox/fixtures.clj @@ -12,173 +12,255 @@ [mount.core :as mount] [promesa.core :as p] [uxbox.config :as cfg] + [uxbox.common.data :as d] [uxbox.core] [uxbox.db :as db] [uxbox.media :as media] [uxbox.migrations] + [uxbox.services.mutations.profile :as mt.profile] [uxbox.util.blob :as blob] - [uxbox.util.uuid :as uuid])) + [uxbox.util.uuid :as uuid] + [vertx.util :as vu])) (defn- mk-uuid [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, photo) - values ($1, $2, $3, $4, $5, $6) - returning *;") +;; --- Profiles creation (def password (pwhash/derive "123123")) -(defn create-user - [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]))) - -;; --- Project User Relation Creation - -(def create-project-user-sql - "insert into project_users (project_id, user_id, can_edit) - values ($1, $2, true) - returning *") - -(defn create-additional-project-user - [conn [project-index user-index]] - (log/info "create project user" user-index project-index) - (let [sql create-project-user-sql - project-id (mk-uuid "project" project-index user-index) - user-id (mk-uuid "user" (dec user-index))] - (db/query-one conn [sql project-id user-id]))) - -;; --- Projects creation - -(def create-project-sql - "insert into projects (id, user_id, name) +(def sql:create-team + "insert into team (id, name, photo) values ($1, $2, $3) returning *;") -(defn create-project - [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 "project " project-index "," user-index)] - (p/do! (db/query-one conn [sql id user-id name]) - (when (and (= project-index 0) - (> user-index 0)) - (create-additional-project-user conn [project-index user-index]))))) +(def sql:create-team-profile + "insert into team_profile_rel (team_id, profile_id, is_owner, is_admin, can_edit) + values ($1, $2, $3, $4, $5) + returning *;") -;; --- Create Page Files +(def sql:create-project + "insert into project (id, team_id, name) + values ($1, $2, $3) + returning *;") -(def create-file-sql - "insert into project_files (id, user_id, project_id, name) - values ($1, $2, $3, $4) returning id") +(def sql:create-project-profile + "insert into project_profile_rel (project_id, profile_id, is_owner, is_admin, can_edit) + values ($1, $2, $3, $4, $5) + returning *") -(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 "file " file-index "," project-index "," user-index)] - (db/query-one conn [sql id user-id project-id name]))) +(def sql:create-file-profile + "insert into file_profile_rel (file_id, profile_id, is_owner, is_admin, can_edit) + values ($1, $2, $3, $4, $5) + returning *") -;; --- Create Pages +(def sql:create-file + "insert into file (id, project_id, name) + values ($1, $2, $3 ) returning *") -(def create-page-sql - "insert into project_pages (id, user_id, file_id, name, - version, ordering, data) - values ($1, $2, $3, $4, $5, $6, $7) +(def sql:create-page + "insert into page (id, file_id, name, + version, ordering, data) + values ($1, $2, $3, $4, $5, $6) returning id;") -(def create-page-history-sql - "insert into project_page_history (page_id, user_id, version, data) - values ($1, $2, $3, $4) - returning id;") - -(defn create-page - [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 - :x 200 - :y 200 - :width 1024 - :height 768 - :stroke-color "#000000" - :stroke-opacity 1 - :fill-color "#ffffff" - :fill-opacity 1} - data {:version 1 - :shapes [] - :canvas [(:id canvas)] - :options {} - :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)] - (p/do! - (db/query-one conn [sql1 id user-id file-id name version ordering data]) - #_(db/query-one conn [sql2 id user-id version data])))) - (def preset-small - {:users 50 - :projects 5 - :files 5 - :pages 3}) + {:num-teams 50 + :num-profiles 50 + :num-profiles-per-team 5 + :num-projects-per-team 5 + :num-files-per-project 5 + :num-pages-per-file 3 + :num-draft-files-per-profile 10 + :num-draft-pages-per-file 3}) -(def preset-medium - {:users 500 - :projects 20 - :files 5 - :pages 3}) +(defn rng-ids + [rng n max] + (let [stream (->> (.longs rng 0 max) + (.iterator) + (iterator-seq))] + (reduce (fn [acc item] + (if (= (count acc) n) + (reduced acc) + (conj acc item))) + #{} + stream))) -(def preset-big - {:users 5000 - :projects 50 - :files 5 - :pages 4}) +(defn rng-vec + [rng vdata n] + (let [ids (rng-ids rng n (count vdata))] + (mapv #(nth vdata %) ids))) + +(defn rng-nth + [rng vdata] + (let [stream (->> (.longs rng 0 (count vdata)) + (.iterator) + (iterator-seq))] + (nth vdata (first stream)))) + +(defn collect + [f items] + (reduce (fn [acc n] + (p/then acc (fn [acc] + (p/then (f n) + (fn [res] + (conj acc res)))))) + (p/promise []) + items)) (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)))) + (let [rng (java.util.Random. 1) + + create-profile + (fn [conn index] + (let [id (mk-uuid "profile" index)] + (log/info "create profile" id) + (mt.profile/register-profile conn + {:id id + :fullname (str "Profile " index) + :password "123123" + :demo? true + :email (str "profile" index ".test@uxbox.io")}))) + + create-profiles + (fn [conn] + (log/info "create profiles") + (collect (partial create-profile conn) + (range (:num-profiles opts)))) + + create-team + (fn [conn index] + (let [sql sql:create-team + id (mk-uuid "team" index) + name (str "Team" index)] + (log/info "create team" id) + + (-> (db/query-one conn [sql id name ""]) + (p/then (constantly id))))) + + create-teams + (fn [conn] + (log/info "create teams") + (collect (partial create-team conn) + (range (:num-teams opts)))) + + create-page + (fn [conn owner-id project-id file-id index] + (p/let [id (mk-uuid "page" project-id file-id index) + data {:version 1 + :shapes [] + :canvas [] + :options {} + :shapes-by-id {}} + + name (str "page " index) + version 0 + ordering index + data (blob/encode data)] + (log/info "create page" id) + (db/query-one conn [sql:create-page + id file-id name version ordering data]))) + + create-pages + (fn [conn owner-id project-id file-id] + (log/info "create pages") + (p/run! (partial create-page conn owner-id project-id file-id) + (range (:num-pages-per-file opts)))) + + create-file + (fn [conn owner-id project-id index] + (p/let [id (mk-uuid "file" project-id index) + name (str "file" index)] + (log/info "create file" id) + (db/query-one conn [sql:create-file id project-id name]) + (db/query-one conn [sql:create-file-profile + id owner-id true true true]) + id)) + + create-files + (fn [conn owner-id project-id] + (log/info "create files") + (p/let [file-ids (collect (partial create-file conn owner-id project-id) + (range (:num-files-per-project opts)))] + (p/run! (partial create-pages conn owner-id project-id) file-ids))) + + create-project + (fn [conn team-id owner-id index] + (p/let [id (mk-uuid "project" team-id index) + name (str "project " index)] + (log/info "create project" id) + (db/query-one conn [sql:create-project id team-id name]) + (db/query-one conn [sql:create-project-profile + id owner-id true true true]) + id)) + + create-projects + (fn [conn team-id profile-ids] + (log/info "create projects") + (p/let [owner-id (rng-nth rng profile-ids) + project-ids (collect (partial create-project conn team-id owner-id) + (range (:num-projects-per-team opts)))] + (p/run! (partial create-files conn owner-id) project-ids))) + + assign-profile-to-team + (fn [conn team-id owner? profile-id] + (let [sql sql:create-team-profile] + (db/query-one conn [sql team-id profile-id owner? true true]))) + + setup-team + (fn [conn team-id profile-ids] + (log/info "setup team" team-id profile-ids) + (p/do! + (assign-profile-to-team conn team-id true (first profile-ids)) + (p/run! (partial assign-profile-to-team conn team-id false) + (rest profile-ids)) + (create-projects conn team-id profile-ids))) + + assign-teams-and-profiles + (fn [conn teams profiles] + (log/info "assign teams and profiles") + (vu/loop [team-id (first teams) + teams (rest teams)] + (when-not (nil? team-id) + (p/let [n-profiles-team (:num-profiles-per-team opts) + selected-profiles (rng-vec rng profiles n-profiles-team)] + (setup-team conn team-id selected-profiles) + (p/recur (first teams) + (rest teams)))))) + + + create-draft-pages + (fn [conn owner-id file-id] + (log/info "create draft pages") + (p/run! (partial create-page conn owner-id nil file-id) + (range (:num-draft-pages-per-file opts)))) + + create-draft-file + (fn [conn owner index] + (p/let [owner-id (:id owner) + id (mk-uuid "file" "draft" owner-id index) + name (str "file" index) + project-id (:id (:default-project owner))] + (log/info "create draft file" id) + (db/query-one conn [sql:create-file id project-id name]) + (db/query-one conn [sql:create-file-profile + id owner-id true true true]) + id)) + + create-draft-files + (fn [conn profile] + (p/let [file-ids (collect (partial create-draft-file conn profile) + (range (:num-draft-files-per-profile opts)))] + (p/run! (partial create-draft-pages conn (:id profile)) file-ids))) + ] + + (db/with-atomic [conn db/pool] + (p/let [profiles (create-profiles conn) + teams (create-teams conn)] + (assign-teams-and-profiles conn teams (map :id profiles)) + (p/run! (partial create-draft-files conn) profiles))))) (defn -main [& args] @@ -190,8 +272,8 @@ (mount/start)) (let [preset (case (first args) (nil "small") preset-small - "medium" preset-medium - "big" preset-big + ;; "medium" preset-medium + ;; "big" preset-big preset-small)] (log/info "Using preset:" (pr-str preset)) (deref (run preset))) diff --git a/backend/src/uxbox/http/handlers.clj b/backend/src/uxbox/http/handlers.clj index ec99762f50..e5444cdf70 100644 --- a/backend/src/uxbox/http/handlers.clj +++ b/backend/src/uxbox/http/handlers.clj @@ -34,8 +34,8 @@ (let [type (keyword (get-in req [:path-params :type])) data (merge (:params req) {::sq/type type - :user (:user req)})] - (if (or (:user req) + :profile-id (:profile-id req)})] + (if (or (:profile-id req) (isa? query-types-hierarchy type ::unauthenticated)) (-> (sq/handle (with-meta data {:req req})) (p/then' (fn [result] @@ -52,8 +52,8 @@ (:body-params req) (:uploads req) {::sm/type type - :user (:user req)})] - (if (or (:user req) + :profile-id (:profile-id req)})] + (if (or (:profile-id req) (isa? mutation-types-hierarchy type ::unauthenticated)) (-> (sm/handle (with-meta data {:req req})) (p/then' (fn [result] @@ -66,12 +66,11 @@ [req] (let [data (:body-params req) user-agent (get-in req [:headers "user-agent"])] - (-> (sm/handle (assoc data ::sm/type :login)) - (p/then #(session/create (:id %) user-agent)) - (p/then' (fn [token] - {:status 204 - :cookies {"auth-token" {:value token :path "/"}} - :body ""}))))) + (p/let [profile (sm/handle (assoc data ::sm/type :login)) + token (session/create (:id profile) user-agent)] + {:status 200 + :cookies {"auth-token" {:value token :path "/"}} + :body profile}))) (defn logout-handler [req] @@ -83,22 +82,10 @@ :cookies {"auth-token" nil} :body ""}))))) -;; (defn register-handler -;; [req] -;; (let [data (merge (:body-params req) -;; {::sm/type :register-profile}) -;; user-agent (get-in req [:headers "user-agent"])] -;; (-> (sm/handle (with-meta data {:req req})) -;; (p/then (fn [{:keys [id] :as user}] -;; (session/create id user-agent))) -;; (p/then' (fn [token] -;; {:status 204 -;; :body ""}))))) - (defn echo-handler [req] - {:status 200 - :body {:params (:params req) - :cookies (:cookies req) - :headers (:headers req)}}) + (p/promise {:status 200 + :body {:params (:params req) + :cookies (:cookies req) + :headers (:headers req)}})) diff --git a/backend/src/uxbox/http/session.clj b/backend/src/uxbox/http/session.clj index 75f8ed79bd..7927baa571 100644 --- a/backend/src/uxbox/http/session.clj +++ b/backend/src/uxbox/http/session.clj @@ -17,26 +17,26 @@ "Retrieves a user id associated with the provided auth token." [token] (when token - (let [sql "select user_id from sessions where id = $1"] + (let [sql "select profile_id from session where id = $1"] (-> (db/query-one db/pool [sql token]) - (p/then' (fn [row] (when row (:user-id row)))))))) + (p/then' (fn [row] (when row (:profile-id row)))))))) (defn create [user-id user-agent] (let [id (uuid/random) - sql "insert into sessions (id, user_id, user_agent) values ($1, $2, $3)"] + sql "insert into session (id, profile_id, user_agent) values ($1, $2, $3)"] (-> (db/query-one db/pool [sql id user-id user-agent]) (p/then (constantly (str id)))))) (defn delete [token] - (let [sql "delete from sessions where id = $1"] + (let [sql "delete from session where id = $1"] (-> (db/query-one db/pool [sql token]) (p/then' (constantly nil))))) ;; --- Interceptor -(defn parse-token +(defn- parse-token [request] (try (when-let [token (get-in request [:cookies "auth-token"])] diff --git a/backend/src/uxbox/http/ws.clj b/backend/src/uxbox/http/ws.clj index 4ad32c1c06..7ee867294a 100644 --- a/backend/src/uxbox/http/ws.clj +++ b/backend/src/uxbox/http/ws.clj @@ -37,8 +37,7 @@ ;; --- State Management -(defonce state - (atom {})) +(def state (atom {})) (defn send! [{:keys [output] :as ws} message] @@ -50,15 +49,15 @@ (fn [ws message] (:type message))) (defmethod handle-message :connect - [{:keys [file-id user-id] :as ws} message] - (let [local (swap! state assoc-in [file-id user-id] ws) + [{:keys [file-id profile-id] :as ws} message] + (let [local (swap! state assoc-in [file-id profile-id] ws) sessions (get local file-id) message {:type :who :users (set (keys sessions))}] (p/run! #(send! % message) (vals sessions)))) (defmethod handle-message :disconnect - [{:keys [user-id] :as ws} {:keys [file-id] :as message}] - (let [local (swap! state update file-id dissoc user-id) + [{:keys [profile-id] :as ws} {:keys [file-id] :as message}] + (let [local (swap! state update file-id dissoc profile-id) sessions (get local file-id) message {:type :who :users (set (keys sessions))}] (p/run! #(send! % message) (vals sessions)))) @@ -69,14 +68,14 @@ (send! ws {:type :who :users (set users)}))) (defmethod handle-message :pointer-update - [{:keys [user-id file-id] :as ws} message] + [{:keys [profile-id file-id] :as ws} message] (let [sessions (->> (vals (get @state file-id)) - (remove #(= user-id (:user-id %)))) - message (assoc message :user-id user-id)] + (remove #(= profile-id (:profile-id %)))) + message (assoc message :profile-id profile-id)] (p/run! #(send! % message) sessions))) (defn- on-eventbus-message - [{:keys [file-id user-id] :as ws} {:keys [body] :as message}] + [{:keys [file-id profile-id] :as ws} {:keys [body] :as message}] (send! ws body)) (defn- start-eventbus-consumer! @@ -90,9 +89,9 @@ [ws req] (let [ctx (vu/current-context) file-id (get-in req [:path-params :file-id]) - user-id (:user req) + profile-id (:profile-id req) ws (assoc ws - :user-id user-id + :profile-id profile-id :file-id file-id) send-ping #(send! ws {:type :ping}) sem1 (start-eventbus-consumer! ctx ws file-id) diff --git a/backend/src/uxbox/migrations.clj b/backend/src/uxbox/migrations.clj index eaed5d7bdb..ae952c9b56 100644 --- a/backend/src/uxbox/migrations.clj +++ b/backend/src/uxbox/migrations.clj @@ -35,6 +35,9 @@ {:desc "Initial icons tables" :name "0006-icons" :fn (mg/resource "migrations/0006.icons.sql")} + {:desc "Initial colors tables" + :name "0007-colors" + :fn (mg/resource "migrations/0007.colors.sql")} ]}) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/backend/src/uxbox/services/init.clj b/backend/src/uxbox/services/init.clj index 466f446283..7b7ddf941e 100644 --- a/backend/src/uxbox/services/init.clj +++ b/backend/src/uxbox/services/init.clj @@ -16,22 +16,26 @@ [] (require 'uxbox.services.queries.icons) (require 'uxbox.services.queries.images) + (require 'uxbox.services.queries.colors) (require 'uxbox.services.queries.projects) - (require 'uxbox.services.queries.project-files) - (require 'uxbox.services.queries.project-pages) + (require 'uxbox.services.queries.files) + (require 'uxbox.services.queries.pages) (require 'uxbox.services.queries.profile) - (require 'uxbox.services.queries.user-attrs)) + ;; (require 'uxbox.services.queries.user-attrs) + ) (defn- load-mutation-services [] (require 'uxbox.services.mutations.demo) (require 'uxbox.services.mutations.icons) (require 'uxbox.services.mutations.images) + (require 'uxbox.services.mutations.colors) (require 'uxbox.services.mutations.projects) - (require 'uxbox.services.mutations.project-files) - (require 'uxbox.services.mutations.project-pages) + (require 'uxbox.services.mutations.files) + (require 'uxbox.services.mutations.pages) (require 'uxbox.services.mutations.profile) - (require 'uxbox.services.mutations.user-attrs)) + ;; (require 'uxbox.services.mutations.user-attrs) + ) (defstate query-services :start (load-query-services)) diff --git a/backend/src/uxbox/services/mutations/colors.clj b/backend/src/uxbox/services/mutations/colors.clj new file mode 100644 index 0000000000..51a320db27 --- /dev/null +++ b/backend/src/uxbox/services/mutations/colors.clj @@ -0,0 +1,219 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 Andrey Antukh + +(ns uxbox.services.mutations.colors + (:require + [clojure.spec.alpha :as s] + [datoteka.core :as fs] + [datoteka.storages :as ds] + [promesa.core :as p] + [promesa.exec :as px] + [uxbox.common.exceptions :as ex] + [uxbox.common.spec :as us] + [uxbox.config :as cfg] + [uxbox.db :as db] + [uxbox.media :as media] + [uxbox.images :as images] + [uxbox.tasks :as tasks] + [uxbox.services.queries.colors :refer [decode-row]] + [uxbox.services.mutations :as sm] + [uxbox.services.util :as su] + [uxbox.util.blob :as blob] + [uxbox.util.uuid :as uuid] + [vertx.util :as vu])) + +;; --- Helpers & Specs + +(s/def ::id ::us/uuid) +(s/def ::name ::us/string) +(s/def ::profile-id ::us/uuid) +(s/def ::collection-id ::us/uuid) +(s/def ::content ::us/string) + + +;; --- Mutation: Create Collection + +(declare create-color-collection) + +(s/def ::create-color-collection + (s/keys :req-un [::profile-id ::name] + :opt-un [::id])) + +(sm/defmutation ::create-color-collection + [{:keys [id profile-id name] :as params}] + (db/with-atomic [conn db/pool] + (create-color-collection conn params))) + +(def ^:private sql:create-color-collection + "insert into color_collection (id, profile_id, name) + values ($1, $2, $3) + returning *;") + +(defn- create-color-collection + [conn {:keys [id profile-id name] :as params}] + (let [id (or id (uuid/next))] + (db/query-one conn [sql:create-color-collection id profile-id name]))) + + + +;; --- Collection Permissions Check + +(def ^:private sql:select-collection + "select id, profile_id + from color_collection + where id=$1 and deleted_at is null + for update") + +(defn- check-collection-edition-permissions! + [conn profile-id coll-id] + (p/let [coll (-> (db/query-one conn [sql:select-collection coll-id]) + (p/then' su/raise-not-found-if-nil))] + (when (not= (:profile-id coll) profile-id) + (ex/raise :type :validation + :code :not-authorized)))) + + +;; --- Mutation: Update Collection + +(def ^:private sql:rename-collection + "update color_collection + set name = $2 + where id = $1 + returning *") + +(s/def ::rename-color-collection + (s/keys :req-un [::profile-id ::name ::id])) + +(sm/defmutation ::rename-color-collection + [{:keys [id profile-id name] :as params}] + (db/with-atomic [conn db/pool] + (check-collection-edition-permissions! conn profile-id id) + (db/query-one conn [sql:rename-collection id name]))) + + +;; --- Copy Color + +;; (declare create-color) + +;; (defn- retrieve-color +;; [conn {:keys [profile-id id]}] +;; (let [sql "select * from color +;; where id = $1 +;; and deleted_at is null +;; and (profile_id = $2 or +;; profile_id = '00000000-0000-0000-0000-000000000000'::uuid)"] +;; (-> (db/query-one conn [sql id profile-id]) +;; (p/then' su/raise-not-found-if-nil)))) + +;; (s/def ::copy-color +;; (s/keys :req-un [:us/id ::collection-id ::profile-id])) + +;; (sm/defmutation ::copy-color +;; [{:keys [profile-id id collection-id] :as params}] +;; (db/with-atomic [conn db/pool] +;; (-> (retrieve-color conn {:profile-id profile-id :id id}) +;; (p/then (fn [color] +;; (let [color (-> (dissoc color :id) +;; (assoc :collection-id collection-id))] +;; (create-color conn color))))))) + +;; --- Delete Collection + +(def ^:private sql:mark-collection-deleted + "update color_collection + set deleted_at = clock_timestamp() + where id = $1 + returning id") + +(s/def ::delete-color-collection + (s/keys :req-un [::profile-id ::id])) + +(sm/defmutation ::delete-color-collection + [{:keys [profile-id id] :as params}] + (db/with-atomic [conn db/pool] + (check-collection-edition-permissions! conn profile-id id) + (-> (db/query-one conn [sql:mark-collection-deleted id]) + (p/then' su/constantly-nil)))) + + + +;; --- Mutation: Create Color (Upload) + +(declare create-color) + +(s/def ::create-color + (s/keys :req-un [::profile-id ::name ::content ::collection-id] + :opt-un [::id])) + +(sm/defmutation ::create-color + [{:keys [profile-id collection-id] :as params}] + (db/with-atomic [conn db/pool] + (check-collection-edition-permissions! conn profile-id collection-id) + (create-color conn params))) + +(def ^:private sql:create-color + "insert into color (id, profile_id, name, collection_id, content) + values ($1, $2, $3, $4, $5) returning *") + +(defn create-color + [conn {:keys [id profile-id name collection-id content]}] + (let [id (or id (uuid/next))] + (-> (db/query-one conn [sql:create-color id profile-id name collection-id content]) + (p/then' decode-row)))) + + + +;; --- Mutation: Update Color + +(def ^:private sql:update-color + "update color + set name = $3, + collection_id = $4 + where id = $1 + and profile_id = $2 + returning *") + +(s/def ::update-color + (s/keys :req-un [::id ::profile-id ::name ::collection-id])) + +(sm/defmutation ::update-color + [{:keys [id name profile-id collection-id] :as params}] + (db/with-atomic [conn db/pool] + (check-collection-edition-permissions! conn profile-id collection-id) + (-> (db/query-one db/pool [sql:update-color id profile-id name collection-id]) + (p/then' su/raise-not-found-if-nil)))) + + + +;; --- Mutation: Delete Color + +(def ^:private sql:mark-color-deleted + "update color + set deleted_at = clock_timestamp() + where id = $1 + and profile_id = $2 + returning id") + +(s/def ::delete-color + (s/keys :req-un [::profile-id ::id])) + +(sm/defmutation ::delete-color + [{:keys [id profile-id] :as params}] + (db/with-atomic [conn db/pool] + (-> (db/query-one conn [sql:mark-color-deleted id profile-id]) + (p/then' su/raise-not-found-if-nil)) + + ;; Schedule object deletion + (tasks/schedule! conn {:name "delete-object" + :delay cfg/default-deletion-delay + :props {:id id :type :color}}) + + nil)) + + diff --git a/backend/src/uxbox/services/mutations/demo.clj b/backend/src/uxbox/services/mutations/demo.clj index d91a2984d0..8f88c71115 100644 --- a/backend/src/uxbox/services/mutations/demo.clj +++ b/backend/src/uxbox/services/mutations/demo.clj @@ -11,36 +11,16 @@ "A demo specific mutations." (:require [clojure.spec.alpha :as s] - [datoteka.core :as fs] - [datoteka.storages :as ds] - [promesa.core :as p] - [promesa.exec :as px] [sodi.prng] - [sodi.pwhash] [sodi.util] [uxbox.common.exceptions :as ex] - [uxbox.common.spec :as us] [uxbox.config :as cfg] [uxbox.db :as db] - [uxbox.emails :as emails] - [uxbox.images :as images] - [uxbox.media :as media] [uxbox.services.mutations :as sm] - [uxbox.services.util :as su] [uxbox.services.mutations.profile :as profile] [uxbox.tasks :as tasks] - [uxbox.util.blob :as blob] [uxbox.util.uuid :as uuid] - [uxbox.util.time :as tm] - [vertx.core :as vc])) - -(def sql:insert-user - "insert into users (id, fullname, email, password, photo, is_demo) - values ($1, $2, $3, $4, '', true) returning *") - -(def sql:insert-email - "insert into user_emails (user_id, email, is_main) - values ($1, $2, true)") + [uxbox.util.time :as tm])) (sm/defmutation ::create-demo-profile [_] @@ -49,15 +29,17 @@ email (str "demo-" sem ".demo@nodomain.com") fullname (str "Demo User " sem) password (-> (sodi.prng/random-bytes 12) - (sodi.util/bytes->b64s)) - password' (sodi.pwhash/derive password)] + (sodi.util/bytes->b64s))] (db/with-atomic [conn db/pool] - (db/query-one conn [sql:insert-user id fullname email password']) - (db/query-one conn [sql:insert-email id email]) + (#'profile/register-profile conn {:id id + :email email + :fullname fullname + :demo? true + :password password}) ;; Schedule deletion of the demo profile - (tasks/schedule! conn {:name "remove-demo-profile" - :delay (tm/duration {:hours 48}) - :props {:id id}}) + (tasks/schedule! conn {:name "delete-profile" + :delay cfg/default-deletion-delay + :props {:profile-id id}}) {:email email :password password}))) diff --git a/backend/src/uxbox/services/mutations/files.clj b/backend/src/uxbox/services/mutations/files.clj new file mode 100644 index 0000000000..69ac5eac64 --- /dev/null +++ b/backend/src/uxbox/services/mutations/files.clj @@ -0,0 +1,242 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2019-2020 Andrey Antukh + +(ns uxbox.services.mutations.files + (:require + [clojure.spec.alpha :as s] + [promesa.core :as p] + [datoteka.core :as fs] + [uxbox.config :as cfg] + [uxbox.db :as db] + [uxbox.media :as media] + [uxbox.images :as images] + [uxbox.common.exceptions :as ex] + [uxbox.common.spec :as us] + [uxbox.common.pages :as cp] + [uxbox.tasks :as tasks] + [uxbox.services.queries.files :as files] + [uxbox.services.mutations :as sm] + [uxbox.services.mutations.projects :as proj] + [uxbox.services.mutations.images :as imgs] + [uxbox.services.util :as su] + [uxbox.util.blob :as blob] + [uxbox.util.uuid :as uuid] + [uxbox.util.storage :as ust] + [vertx.util :as vu])) + +;; --- Helpers & Specs + +(s/def ::id ::us/uuid) +(s/def ::name ::us/string) +(s/def ::profile-id ::us/uuid) +(s/def ::project-id ::us/uuid) + +;; --- Mutation: Create Project File + +(declare create-file) +(declare create-page) + +(s/def ::create-file + (s/keys :req-un [::profile-id ::name ::project-id] + :opt-un [::id])) + +(sm/defmutation ::create-file + [{:keys [profile-id project-id] :as params}] + (db/with-atomic [conn db/pool] + (p/let [file (create-file conn params) + page (create-page conn (assoc params :file-id (:id file)))] + (assoc file :pages [(:id page)])))) + +(def ^:private sql:create-file + "insert into file (id, project_id, name) + values ($1, $2, $3) returning *") + +(def ^:private sql:create-file-profile + "insert into file_profile_rel (profile_id, file_id, is_owner, is_admin, can_edit) + values ($1, $2, true, true, true) returning *") + +(def ^:private sql:create-page + "insert into page (id, file_id, name, ordering, data) + values ($1, $2, $3, $4, $5) returning id") + +(defn- create-file-profile + [conn {:keys [profile-id file-id] :as params}] + (db/query-one conn [sql:create-file-profile profile-id file-id])) + +(defn- create-file + [conn {:keys [id profile-id name project-id] :as params}] + (p/let [id (or id (uuid/next)) + file (db/query-one conn [sql:create-file id project-id name])] + (->> (assoc params :file-id id) + (create-file-profile conn)) + file)) + +(defn- create-page + [conn {:keys [file-id] :as params}] + (let [id (uuid/next) + name "Page 1" + data (blob/encode cp/default-page-data)] + (db/query-one conn [sql:create-page id file-id name 1 data]))) + + + +;; --- Mutation: Rename File + +(declare rename-file) + +(s/def ::rename-file + (s/keys :req-un [::profile-id ::name ::id])) + +(sm/defmutation ::rename-file + [{:keys [id profile-id] :as params}] + (db/with-atomic [conn db/pool] + (files/check-edition-permissions! conn profile-id id) + (rename-file conn params))) + +(def ^:private sql:rename-file + "update file + set name = $2 + where id = $1 + and deleted_at is null") + +(defn- rename-file + [conn {:keys [id name] :as params}] + (-> (db/query-one conn [sql:rename-file id name]) + (p/then' su/constantly-nil))) + + +;; --- Mutation: Delete Project File + +(declare mark-file-deleted) + +(s/def ::delete-file + (s/keys :req-un [::id ::profile-id])) + +(sm/defmutation ::delete-file + [{:keys [id profile-id] :as params}] + (db/with-atomic [conn db/pool] + (files/check-edition-permissions! conn profile-id id) + + ;; Schedule object deletion + (tasks/schedule! conn {:name "delete-object" + :delay cfg/default-deletion-delay + :props {:id id :type :file}}) + + (mark-file-deleted conn params))) + +(def ^:private sql:mark-file-deleted + "update file + set deleted_at = clock_timestamp() + where id = $1 + and deleted_at is null") + +(defn mark-file-deleted + [conn {:keys [id] :as params}] + (-> (db/query-one conn [sql:mark-file-deleted id]) + (p/then' su/constantly-nil))) + + +;; --- Mutation: Upload File Image + +(declare create-file-image) + +(s/def ::file-id ::us/uuid) +(s/def ::content ::imgs/upload) + +(s/def ::upload-file-image + (s/keys :req-un [::profile-id ::file-id ::name ::content] + :opt-un [::id])) + +(sm/defmutation ::upload-file-image + [{:keys [profile-id file-id] :as params}] + (db/with-atomic [conn db/pool] + (files/check-edition-permissions! conn profile-id file-id) + (create-file-image conn params))) + +(def ^:private sql:insert-file-image + "insert into file_image + (file_id, name, path, width, height, mtype, + thumb_path, thumb_width, thumb_height, + thumb_quality, thumb_mtype) + values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) + returning *") + +(defn- create-file-image + [conn {:keys [content file-id name] :as params}] + (when-not (imgs/valid-image-types? (:mtype content)) + (ex/raise :type :validation + :code :image-type-not-allowed + :hint "Seems like you are uploading an invalid image.")) + + (p/let [image-opts (vu/blocking (images/info (:path content))) + image-path (imgs/persist-image-on-fs content) + thumb-opts imgs/thumbnail-options + thumb-path (imgs/persist-image-thumbnail-on-fs thumb-opts image-path) + + sqlv [sql:insert-file-image + file-id + name + (str image-path) + (:width image-opts) + (:height image-opts) + (:mtype content) + (str thumb-path) + (:width thumb-opts) + (:height thumb-opts) + (:quality thumb-opts) + (images/format->mtype (:format thumb-opts))]] + (-> (db/query-one db/pool sqlv) + (p/then' #(images/resolve-urls % :path :uri)) + (p/then' #(images/resolve-urls % :thumb-path :thumb-uri))))) + + +;; --- Mutation: Import from collection + +(declare copy-image) +(declare import-image-to-file) + +(s/def ::import-image-to-file + (s/keys :req-un [::image-id ::file-id ::profile-id])) + +(def ^:private sql:select-image-by-id + "select img.* from image as img where id=$1") + +(sm/defmutation ::import-image-to-file + [{:keys [image-id file-id profile-id] :as params}] + (db/with-atomic [conn db/pool] + (files/check-edition-permissions! conn profile-id file-id) + (import-image-to-file conn params))) + +(defn- import-image-to-file + [conn {:keys [image-id file-id] :as params}] + (p/let [image (-> (db/query-one conn [sql:select-image-by-id image-id]) + (p/then' su/raise-not-found-if-nil)) + image-path (copy-image (:path image)) + thumb-path (copy-image (:thumb-path image)) + sqlv [sql:insert-file-image + file-id + (:name image) + (str image-path) + (:width image) + (:height image) + (:mtype image) + (str thumb-path) + (:thumb-width image) + (:thumb-height image) + (:thumb-quality image) + (:thumb-mtype image)]] + (-> (db/query-one db/pool sqlv) + (p/then' #(images/resolve-urls % :path :uri)) + (p/then' #(images/resolve-urls % :thumb-path :thumb-uri))))) + +(defn- copy-image + [path] + (vu/blocking + (let [image-path (ust/lookup media/media-storage path)] + (ust/save! media/media-storage (fs/name image-path) image-path)))) diff --git a/backend/src/uxbox/services/mutations/icons.clj b/backend/src/uxbox/services/mutations/icons.clj index 304d1b3f5d..c0f30249af 100644 --- a/backend/src/uxbox/services/mutations/icons.clj +++ b/backend/src/uxbox/services/mutations/icons.clj @@ -2,26 +2,40 @@ ;; 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/. ;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; ;; Copyright (c) 2019 Andrey Antukh (ns uxbox.services.mutations.icons (:require [clojure.spec.alpha :as s] + [datoteka.core :as fs] + [datoteka.storages :as ds] [promesa.core :as p] - [uxbox.db :as db] + [promesa.exec :as px] + [uxbox.common.exceptions :as ex] [uxbox.common.spec :as us] + [uxbox.config :as cfg] + [uxbox.db :as db] + [uxbox.media :as media] + [uxbox.images :as images] + [uxbox.tasks :as tasks] + [uxbox.services.queries.icons :refer [decode-row]] [uxbox.services.mutations :as sm] [uxbox.services.util :as su] - [uxbox.services.queries.icons :refer [decode-icon-row]] [uxbox.util.blob :as blob] - [uxbox.util.uuid :as uuid])) + [uxbox.util.data :as data] + [uxbox.util.uuid :as uuid] + [uxbox.util.storage :as ust] + [vertx.util :as vu])) ;; --- Helpers & Specs (s/def ::id ::us/uuid) (s/def ::name ::us/string) -(s/def ::user ::us/uuid) -(s/def ::collection-id (s/nilable ::us/uuid)) +(s/def ::profile-id ::us/uuid) +(s/def ::collection-id ::us/uuid) (s/def ::width ::us/integer) (s/def ::height ::us/integer) @@ -36,128 +50,188 @@ (s/def ::metadata (s/keys :opt-un [::width ::height ::view-box ::mimetype])) + + ;; --- Mutation: Create Collection -(s/def ::create-icons-collection - (s/keys :req-un [::user ::name] +(declare create-icon-collection) + +(s/def ::create-icon-collection + (s/keys :req-un [::profile-id ::name] :opt-un [::id])) -(sm/defmutation ::create-icons-collection - [{:keys [id user name] :as params}] - (let [id (or id (uuid/next)) - sql "insert into icon_collections (id, user_id, name) - values ($1, $2, $3) returning *"] - (db/query-one db/pool [sql id user name]))) +(sm/defmutation ::create-icon-collection + [{:keys [id profile-id name] :as params}] + (db/with-atomic [conn db/pool] + (create-icon-collection conn params))) + +(def ^:private sql:create-icon-collection + "insert into icon_collection (id, profile_id, name) + values ($1, $2, $3) + returning *;") + +(defn- create-icon-collection + [conn {:keys [id profile-id name] :as params}] + (let [id (or id (uuid/next))] + (db/query-one conn [sql:create-icon-collection id profile-id name]))) + + + +;; --- Collection Permissions Check + +(def ^:private sql:select-collection + "select id, profile_id + from icon_collection + where id=$1 and deleted_at is null + for update") + +(defn- check-collection-edition-permissions! + [conn profile-id coll-id] + (p/let [coll (-> (db/query-one conn [sql:select-collection coll-id]) + (p/then' su/raise-not-found-if-nil))] + (when (not= (:profile-id coll) profile-id) + (ex/raise :type :validation + :code :not-authorized)))) + + ;; --- Mutation: Update Collection -(s/def ::update-icons-collection - (s/keys :req-un [::user ::name ::id])) +(def ^:private sql:rename-collection + "update icon_collection + set name = $2 + where id = $1 + returning *") -(sm/defmutation ::update-icons-collection - [{:keys [id user name] :as params}] - (let [sql "update icon_collections - set name = $3 - where id = $1 - and user_id = $2 - returning *"] - (-> (db/query-one db/pool [sql id user name]) - (p/then' su/raise-not-found-if-nil)))) +(s/def ::rename-icon-collection + (s/keys :req-un [::profile-id ::name ::id])) -;; --- Copy Icon - -(declare create-icon) - -(defn- retrieve-icon - [conn {:keys [user id]}] - (let [sql "select * from icons - where id = $1 - and deleted_at is null - and (user_id = $2 or - user_id = '00000000-0000-0000-0000-000000000000'::uuid)"] - (-> (db/query-one conn [sql id user]) - (p/then' su/raise-not-found-if-nil)))) - -(s/def ::copy-icon - (s/keys :req-un [:us/id ::collection-id ::user])) - -(sm/defmutation ::copy-icon - [{:keys [user id collection-id] :as params}] +(sm/defmutation ::rename-icon-collection + [{:keys [id profile-id name] :as params}] (db/with-atomic [conn db/pool] - (-> (retrieve-icon conn {:user user :id id}) - (p/then (fn [icon] - (let [icon (-> (dissoc icon :id) - (assoc :collection-id collection-id))] - (create-icon conn icon))))))) + (check-collection-edition-permissions! conn profile-id id) + (db/query-one conn [sql:rename-collection id name]))) + + + +;; ;; --- Copy Icon + +;; (declare create-icon) + +;; (defn- retrieve-icon +;; [conn {:keys [profile-id id]}] +;; (let [sql "select * from icon +;; where id = $1 +;; and deleted_at is null +;; and (profile_id = $2 or +;; profile_id = '00000000-0000-0000-0000-000000000000'::uuid)"] +;; (-> (db/query-one conn [sql id profile-id]) +;; (p/then' su/raise-not-found-if-nil)))) + +;; (s/def ::copy-icon +;; (s/keys :req-un [:us/id ::collection-id ::profile-id])) + +;; (sm/defmutation ::copy-icon +;; [{:keys [profile-id id collection-id] :as params}] +;; (db/with-atomic [conn db/pool] +;; (-> (retrieve-icon conn {:profile-id profile-id :id id}) +;; (p/then (fn [icon] +;; (let [icon (-> (dissoc icon :id) +;; (assoc :collection-id collection-id))] +;; (create-icon conn icon))))))) ;; --- Delete Collection -(s/def ::delete-icons-collection - (s/keys :req-un [::user ::id])) +(def ^:private sql:mark-collection-deleted + "update icon_collection + set deleted_at = clock_timestamp() + where id = $1 + returning id") -(sm/defmutation ::delete-icons-collection - [{:keys [user id] :as params}] - (let [sql "update icon_collections - set deleted_at = clock_timestamp() - where id = $1 - and user_id = $2 - returning id"] - (-> (db/query-one db/pool [sql id user]) - (p/then' su/raise-not-found-if-nil) +(s/def ::delete-icon-collection + (s/keys :req-un [::profile-id ::id])) + +(sm/defmutation ::delete-icon-collection + [{:keys [profile-id id] :as params}] + (db/with-atomic [conn db/pool] + (check-collection-edition-permissions! conn profile-id id) + (-> (db/query-one conn [sql:mark-collection-deleted id]) (p/then' su/constantly-nil)))) + + ;; --- Mutation: Create Icon (Upload) -(def ^:private create-icon-sql - "insert into icons (user_id, name, collection_id, content, metadata) - values ($1, $2, $3, $4, $5) returning *") - -(defn create-icon - [conn {:keys [id user name collection-id metadata content]}] - (let [id (or id (uuid/next)) - sqlv [create-icon-sql user name - collection-id - content - (blob/encode metadata)]] - (-> (db/query-one conn sqlv) - (p/then' decode-icon-row)))) +(declare create-icon) (s/def ::create-icon - (s/keys :req-un [::user ::name ::metadata ::content] - :opt-un [::id ::collection-id])) + (s/keys :req-un [::profile-id ::name ::metadata ::content ::collection-id] + :opt-un [::id])) (sm/defmutation ::create-icon - [params] - (create-icon db/pool params)) + [{:keys [profile-id collection-id] :as params}] + (db/with-atomic [conn db/pool] + (check-collection-edition-permissions! conn profile-id collection-id) + (create-icon conn params))) + +(def ^:private sql:create-icon + "insert into icon (id, profile_id, name, collection_id, content, metadata) + values ($1, $2, $3, $4, $5, $6) returning *") + +(defn create-icon + [conn {:keys [id profile-id name collection-id metadata content]}] + (let [id (or id (uuid/next))] + (-> (db/query-one conn [sql:create-icon id profile-id name + collection-id content (blob/encode metadata)]) + (p/then' decode-row)))) + + ;; --- Mutation: Update Icon +(def ^:private sql:update-icon + "update icon + set name = $3, + collection_id = $4 + where id = $1 + and profile_id = $2 + returning *") + (s/def ::update-icon - (s/keys :req-un [::id ::user ::name ::collection-id])) + (s/keys :req-un [::id ::profile-id ::name ::collection-id])) (sm/defmutation ::update-icon - [{:keys [id name user collection-id] :as params}] - (let [sql "update icons - set name = $1, - collection_id = $2 - where id = $3 - and user_id = $4 - returning *"] - (-> (db/query-one db/pool [sql name collection-id id user]) + [{:keys [id name profile-id collection-id] :as params}] + (db/with-atomic [conn db/pool] + (check-collection-edition-permissions! conn profile-id collection-id) + (-> (db/query-one db/pool [sql:update-icon id profile-id name collection-id]) (p/then' su/raise-not-found-if-nil)))) + + ;; --- Mutation: Delete Icon +(def ^:private sql:mark-icon-deleted + "update icon + set deleted_at = clock_timestamp() + where id = $1 + and profile_id = $2 + returning id") + (s/def ::delete-icon - (s/keys :req-un [::user ::id])) + (s/keys :req-un [::profile-id ::id])) (sm/defmutation ::delete-icon - [{:keys [id user] :as params}] - (let [sql "update icons - set deleted_at = clock_timestamp() - where id = $1 - and user_id = $2 - returning id"] - (-> (db/query-one db/pool [sql id user]) - (p/then' su/raise-not-found-if-nil) - (p/then' su/constantly-nil)))) + [{:keys [id profile-id] :as params}] + (db/with-atomic [conn db/pool] + (-> (db/query-one conn [sql:mark-icon-deleted id profile-id]) + (p/then' su/raise-not-found-if-nil)) + + ;; Schedule object deletion + (tasks/schedule! conn {:name "delete-object" + :delay cfg/default-deletion-delay + :props {:id id :type :icon}}) + + nil)) + + diff --git a/backend/src/uxbox/services/mutations/images.clj b/backend/src/uxbox/services/mutations/images.clj index 57b75a092c..839cfa4323 100644 --- a/backend/src/uxbox/services/mutations/images.clj +++ b/backend/src/uxbox/services/mutations/images.clj @@ -2,6 +2,9 @@ ;; 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/. ;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; ;; Copyright (c) 2019 Andrey Antukh (ns uxbox.services.mutations.images @@ -13,9 +16,11 @@ [promesa.exec :as px] [uxbox.common.exceptions :as ex] [uxbox.common.spec :as us] + [uxbox.config :as cfg] [uxbox.db :as db] [uxbox.media :as media] [uxbox.images :as images] + [uxbox.tasks :as tasks] [uxbox.services.mutations :as sm] [uxbox.services.util :as su] [uxbox.util.blob :as blob] @@ -32,69 +37,102 @@ (s/def ::id ::us/uuid) (s/def ::name ::us/string) -(s/def ::user ::us/uuid) +(s/def ::profile-id ::us/uuid) +(s/def ::collection-id ::us/uuid) + + ;; --- Create Collection -(declare create-images-collection) +(declare create-image-collection) -(s/def ::create-images-collection - (s/keys :req-un [::user ::us/name] +(s/def ::create-image-collection + (s/keys :req-un [::profile-id ::name] :opt-un [::id])) -(sm/defmutation ::create-images-collection - [{:keys [id user name] :as params}] +(sm/defmutation ::create-image-collection + [{:keys [id profile-id name] :as params}] (db/with-atomic [conn db/pool] - (create-images-collection conn params))) + (create-image-collection conn params))) -(defn create-images-collection - [conn {:keys [id user name] :as params}] - (let [id (or id (uuid/next)) - sql "insert into image_collections (id, user_id, name) - values ($1, $2, $3) - on conflict (id) do nothing - returning *;"] - (db/query-one db/pool [sql id user name]))) - -;; --- Update Collection - -(def ^:private - sql:rename-images-collection - "update image_collections - set name = $3 - where id = $1 - and user_id = $2 +(def ^:private sql:create-image-collection + "insert into image_collection (id, profile_id, name) + values ($1, $2, $3) returning *;") -(s/def ::rename-images-collection - (s/keys :req-un [::id ::user ::us/name])) +(defn- create-image-collection + [conn {:keys [id profile-id name] :as params}] + (let [id (or id (uuid/next))] + (db/query-one conn [sql:create-image-collection id profile-id name]))) -(sm/defmutation ::rename-images-collection - [{:keys [id user name] :as params}] + + +;; --- Collection Permissions Check + +(def ^:private sql:select-collection + "select id, profile_id + from image_collection + where id=$1 and deleted_at is null + for update") + +(defn- check-collection-edition-permissions! + [conn profile-id coll-id] + (p/let [coll (-> (db/query-one conn [sql:select-collection coll-id]) + (p/then' su/raise-not-found-if-nil))] + (when (not= (:profile-id coll) profile-id) + (ex/raise :type :validation + :code :not-authorized)))) + + + +;; --- Rename Collection + +(def ^:private sql:rename-image-collection + "update image_collection + set name = $2 + where id = $1 + returning *;") + +(s/def ::rename-image-collection + (s/keys :req-un [::id ::profile-id ::us/name])) + +(sm/defmutation ::rename-image-collection + [{:keys [id profile-id name] :as params}] (db/with-atomic [conn db/pool] - (db/query-one conn [sql:rename-images-collection id user name]))) + (check-collection-edition-permissions! conn profile-id id) + (db/query-one conn [sql:rename-image-collection id name]))) + + ;; --- Delete Collection -(s/def ::delete-images-collection - (s/keys :req-un [::user ::id])) +(s/def ::delete-image-collection + (s/keys :req-un [::profile-id ::id])) -(def ^:private - sql:delete-images-collection - "update image_collections +(def ^:private sql:mark-image-collection-as-deleted + "update image_collection set deleted_at = clock_timestamp() where id = $1 - and user_id = $2 returning id") -(sm/defmutation ::delete-images-collection - [{:keys [id user] :as params}] - (-> (db/query-one db/pool [sql:delete-images-collection id user]) - (p/then' su/raise-not-found-if-nil))) +(sm/defmutation ::delete-image-collection + [{:keys [id profile-id] :as params}] + (db/with-atomic [conn db/pool] + (check-collection-edition-permissions! conn profile-id id) + + ;; Schedule object deletion + (tasks/schedule! conn {:name "delete-object" + :delay cfg/default-deletion-delay + :props {:id id :type :image-collection}}) + + (-> (db/query-one conn [sql:mark-image-collection-as-deleted id]) + (p/then' su/raise-not-found-if-nil) + (p/then' su/constantly-nil)))) + + ;; --- Create Image (Upload) -(declare select-collection-for-update) (declare create-image) (declare persist-image-on-fs) (declare persist-image-thumbnail-on-fs) @@ -113,31 +151,27 @@ :uxbox$upload/path :uxbox$upload/mtype])) -(s/def ::collection-id ::us/uuid) (s/def ::content ::upload) (s/def ::upload-image - (s/keys :req-un [::user ::name ::content ::collection-id] + (s/keys :req-un [::profile-id ::name ::content ::collection-id] :opt-un [::id])) (sm/defmutation ::upload-image - [{:keys [collection-id user] :as params}] + [{:keys [collection-id profile-id] :as params}] (db/with-atomic [conn db/pool] - (p/let [coll (select-collection-for-update conn collection-id)] - (when (not= (:user-id coll) user) - (ex/raise :type :validation - :code :not-authorized)) - (create-image conn params)))) + (check-collection-edition-permissions! conn profile-id collection-id) + (create-image conn params))) (def ^:private sql:insert-image - "insert into images - (id, collection_id, user_id, name, path, width, height, mtype, + "insert into image + (id, collection_id, profile_id, name, path, width, height, mtype, thumb_path, thumb_width, thumb_height, thumb_quality, thumb_mtype) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) returning *") (defn create-image - [conn {:keys [id content collection-id user name] :as params}] + [conn {:keys [id content collection-id profile-id name] :as params}] (when-not (valid-image-types? (:mtype content)) (ex/raise :type :validation :code :image-type-not-allowed @@ -151,7 +185,7 @@ sqlv [sql:insert-image id collection-id - user + profile-id name (str image-path) (:width image-opts) @@ -167,16 +201,6 @@ (p/then' #(images/resolve-urls % :path :uri)) (p/then' #(images/resolve-urls % :thumb-path :thumb-uri))))) -(defn- select-collection-for-update - [conn id] - (let [sql "select c.id, c.user_id - from image_collections as c - where c.id = $1 - and c.deleted_at is null - for update;"] - (-> (db/query-one conn [sql id]) - (p/then' su/raise-not-found-if-nil)))) - (defn persist-image-on-fs [{:keys [name path] :as upload}] (vu/blocking @@ -193,32 +217,37 @@ (str "thumbnail-" filename))] (ust/save! media/media-storage thumb-name thumb-data)))) + + ;; --- Update Image (s/def ::update-image - (s/keys :req-un [::id ::user ::name ::collection-id])) + (s/keys :req-un [::id ::profile-id ::name ::collection-id])) (def ^:private sql:update-image - "update images + "update image set name = $3, collection_id = $2 where id = $1 - and user_id = $4 + and profile_id = $4 returning *;") (sm/defmutation ::update-image - [{:keys [id name user collection-id] :as params}] - (db/query-one db/pool [sql:update-image id collection-id name user])) + [{:keys [id name profile-id collection-id] :as params}] + (-> (db/query-one db/pool [sql:update-image id + collection-id name profile-id]) + (p/then' su/raise-not-found-if-nil))) + ;; --- Copy Image -(declare retrieve-image) +;; (declare retrieve-image) ;; (s/def ::copy-image -;; (s/keys :req-un [::id ::collection-id ::user])) +;; (s/keys :req-un [::id ::collection-id ::profile-id])) ;; (sm/defmutation ::copy-image -;; [{:keys [user id collection-id] :as params}] +;; [{:keys [profile-id id collection-id] :as params}] ;; (letfn [(copy-image [conn {:keys [path] :as image}] ;; (-> (ds/lookup media/images-storage (:path image)) ;; (p/then (fn [path] (ds/save media/images-storage (fs/name path) path))) @@ -229,28 +258,33 @@ ;; (p/then (partial store-image-in-db conn))))] ;; (db/with-atomic [conn db/pool] -;; (-> (retrieve-image conn {:id id :user user}) +;; (-> (retrieve-image conn {:id id :profile-id profile-id}) ;; (p/then su/raise-not-found-if-nil) ;; (p/then (partial copy-image conn)))))) ;; --- Delete Image -;; TODO: this need to be performed in the GC process -;; (defn- delete-image-from-storage -;; [{:keys [path] :as image}] -;; (when @(ds/exists? media/images-storage path) -;; @(ds/delete media/images-storage path)) -;; (when @(ds/exists? media/thumbnails-storage path) -;; @(ds/delete media/thumbnails-storage path))) +(def ^:private sql:mark-image-deleted + "update image + set deleted_at = clock_timestamp() + where id = $1 + and profile_id = $2 + returning id") (s/def ::delete-image - (s/keys :req-un [::id ::user])) + (s/keys :req-un [::id ::profile-id])) (sm/defmutation ::delete-image - [{:keys [user id] :as params}] - (let [sql "update images - set deleted_at = clock_timestamp() - where id = $1 - and user_id = $2 - returning *"] - (db/query-one db/pool [sql id user]))) + [{:keys [profile-id id] :as params}] + (db/with-atomic [conn db/pool] + (-> (db/query-one conn [sql:mark-image-deleted id profile-id]) + (p/then' su/raise-not-found-if-nil)) + + ;; Schedule object deletion + (tasks/schedule! conn {:name "delete-object" + :delay cfg/default-deletion-delay + :props {:id id :type :image}}) + + nil)) + + diff --git a/backend/src/uxbox/services/mutations/pages.clj b/backend/src/uxbox/services/mutations/pages.clj new file mode 100644 index 0000000000..baead82f7c --- /dev/null +++ b/backend/src/uxbox/services/mutations/pages.clj @@ -0,0 +1,258 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2019-2020 Andrey Antukh + +(ns uxbox.services.mutations.pages + (:require + [clojure.spec.alpha :as s] + [promesa.core :as p] + [uxbox.common.pages :as cp] + [uxbox.common.exceptions :as ex] + [uxbox.common.spec :as us] + [uxbox.config :as cfg] + [uxbox.db :as db] + [uxbox.services.queries.files :as files] + [uxbox.services.mutations :as sm] + [uxbox.services.queries.pages :refer [decode-row]] + [uxbox.services.util :as su] + [uxbox.tasks :as tasks] + [uxbox.util.blob :as blob] + [uxbox.util.sql :as sql] + [uxbox.util.uuid :as uuid] + [vertx.eventbus :as ve])) + +;; --- Helpers & Specs + +(s/def ::id ::us/uuid) +(s/def ::name ::us/string) +(s/def ::data ::cp/data) +(s/def ::profile-id ::us/uuid) +(s/def ::project-id ::us/uuid) +(s/def ::ordering ::us/number) +(s/def ::file-id ::us/uuid) + +;; --- Mutation: Create Page + +(declare create-page) + +(s/def ::create-page + (s/keys :req-un [::profile-id ::file-id ::name ::ordering ::data] + :opt-un [::id])) + +(sm/defmutation ::create-page + [{:keys [profile-id file-id] :as params}] + (db/with-atomic [conn db/pool] + (files/check-edition-permissions! conn profile-id file-id) + (create-page conn params))) + +(def ^:private sql:create-page + "insert into page (id, file_id, name, ordering, data) + values ($1, $2, $3, $4, $5) + returning *") + +(defn- create-page + [conn {:keys [id file-id name ordering data] :as params}] + (let [id (or id (uuid/next)) + data (blob/encode data)] + (-> (db/query-one conn [sql:create-page + id file-id name + ordering data]) + (p/then' decode-row)))) + + + +;; --- Mutation: Rename Page + +(declare rename-page) +(declare select-page-for-update) + +(s/def ::rename-page + (s/keys :req-un [::id ::name ::profile-id])) + +(sm/defmutation ::rename-page + [{:keys [id name profile-id]}] + (db/with-atomic [conn db/pool] + (p/let [page (select-page-for-update conn id)] + (files/check-edition-permissions! conn profile-id (:file-id page)) + (rename-page conn (assoc page :name name))))) + +(def ^:private sql:select-page-for-update + "select p.id, p.revn, p.file_id, p.data + from page as p + where p.id = $1 + and deleted_at is null + for update;") + +(defn- select-page-for-update + [conn id] + (-> (db/query-one conn [sql:select-page-for-update id]) + (p/then' su/raise-not-found-if-nil))) + +(def ^:private sql:rename-page + "update page + set name = $2 + where id = $1 + and deleted_at is null") + +(defn- rename-page + [conn {:keys [id name] :as params}] + (-> (db/query-one conn [sql:rename-page id name]) + (p/then su/constantly-nil))) + + + +;; --- Mutation: Update Page + +;; A generic, Changes based (granular) page update method. + +(s/def ::changes + (s/coll-of map? :kind vector?)) + +(s/def ::revn ::us/integer) +(s/def ::update-page + (s/keys :req-un [::id ::profile-id ::revn ::changes])) + +(declare update-page) +(declare retrieve-lagged-changes) +(declare update-page-data) +(declare insert-page-change) + +(sm/defmutation ::update-page + [{:keys [id profile-id] :as params}] + (db/with-atomic [conn db/pool] + (p/let [{:keys [file-id] :as page} (select-page-for-update conn id)] + (files/check-edition-permissions! conn profile-id file-id) + (update-page conn page params)))) + +(defn- update-page + [conn page params] + (when (> (:revn params) + (:revn page)) + (ex/raise :type :validation + :code :revn-conflict + :hint "The incoming revision number is greater that stored version." + :context {:incoming-revn (:revn params) + :stored-revn (:revn page)})) + (let [changes (:changes params) + data (-> (:data page) + (blob/decode) + (cp/process-changes changes) + (blob/encode)) + + page (assoc page + :data data + :revn (inc (:revn page)) + :changes (blob/encode changes))] + + (-> (update-page-data conn page) + (p/then (fn [_] (insert-page-change conn page))) + (p/then (fn [s] + (let [topic (str "internal.uxbox.file." (:file-id page))] + (p/do! (ve/publish! uxbox.core/system topic + {:type :page-change + :profile-id (:profile-id params) + :page-id (:page-id s) + :revn (:revn s) + :changes changes}) + (retrieve-lagged-changes conn s params)))))))) + +(def ^:private sql:update-page-data + "update page + set revn = $1, + data = $2 + where id = $3") + +(defn- update-page-data + [conn {:keys [id name revn data]}] + (-> (db/query-one conn [sql:update-page-data revn data id]) + (p/then' su/constantly-nil))) + +(def ^:private sql:insert-page-change + "insert into page_change (id, page_id, revn, data, changes) + values ($1, $2, $3, $4, $5) + returning id, page_id, revn, changes") + +(defn- insert-page-change + [conn {:keys [revn data changes] :as page}] + (let [id (uuid/next) + page-id (:id page)] + (db/query-one conn [sql:insert-page-change id + page-id revn data changes]))) + +(def ^:private sql:lagged-changes + "select s.id, s.changes + from page_change as s + where s.page_id = $1 + and s.revn > $2 + order by s.created_at asc") + +(defn- retrieve-lagged-changes + [conn snapshot params] + (-> (db/query conn [sql:lagged-changes (:id params) (:revn params)]) + (p/then (fn [rows] + {:page-id (:id params) + :revn (:revn snapshot) + :changes (into [] (comp (map decode-row) + (map :changes) + (mapcat identity)) + rows)})))) + + +;; --- Mutation: Delete Page + +(declare mark-page-deleted) + +(s/def ::delete-page + (s/keys :req-un [::profile-id ::id])) + +(sm/defmutation ::delete-page + [{:keys [id profile-id]}] + (db/with-atomic [conn db/pool] + (p/let [page (select-page-for-update conn id)] + (files/check-edition-permissions! conn profile-id (:file-id page)) + + ;; Schedule object deletion + (tasks/schedule! conn {:name "delete-object" + :delay cfg/default-deletion-delay + :props {:id id :type :page}}) + + (mark-page-deleted conn id)))) + +(def ^:private sql:mark-page-deleted + "update page + set deleted_at = clock_timestamp() + where id = $1 + and deleted_at is null") + +(defn- mark-page-deleted + [conn id] + (-> (db/query-one conn [sql:mark-page-deleted id]) + (p/then su/constantly-nil))) + + +;; --- Update Page History + +;; (defn update-page-history +;; [conn {:keys [profile-id id label pinned]}] +;; (let [sqlv (sql/update-page-history {:profile-id profile-id +;; :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 [::profile-id ::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/profile.clj b/backend/src/uxbox/services/mutations/profile.clj index 919223bc4c..b06bc1648f 100644 --- a/backend/src/uxbox/services/mutations/profile.clj +++ b/backend/src/uxbox/services/mutations/profile.clj @@ -26,6 +26,8 @@ [uxbox.media :as media] [uxbox.services.mutations :as sm] [uxbox.services.mutations.images :as imgs] + [uxbox.services.mutations.teams :as mt.teams] + [uxbox.services.mutations.projects :as mt.projects] [uxbox.services.queries.profile :as profile] [uxbox.services.util :as su] [uxbox.util.blob :as blob] @@ -40,14 +42,14 @@ (s/def ::fullname ::us/string) (s/def ::lang ::us/string) (s/def ::path ::us/string) -(s/def ::user ::us/uuid) +(s/def ::profile-id ::us/uuid) (s/def ::password ::us/string) (s/def ::old-password ::us/string) ;; --- Mutation: Login -(declare retrieve-user-by-email) +(declare retrieve-profile-by-email) (s/def ::email ::us/email) (s/def ::scope ::us/string) @@ -58,31 +60,34 @@ (sm/defmutation ::login [{:keys [email password scope] :as params}] - (letfn [(check-password [user password] - (let [result (sodi.pwhash/verify password (:password user))] + (letfn [(check-password [profile password] + (let [result (sodi.pwhash/verify password (:password profile))] (:valid result))) - (check-user [user] - (when-not user + (check-profile [profile] + (when-not profile (ex/raise :type :validation :code ::wrong-credentials)) - (when-not (check-password user password) + (when-not (check-password profile password) (ex/raise :type :validation :code ::wrong-credentials)) + profile)] + (db/with-atomic [conn db/pool] + (p/let [prof (-> (retrieve-profile-by-email conn email) + (p/then' check-profile) + (p/then' profile/strip-private-attrs)) + addt (profile/retrieve-additional-data conn (:id prof))] + (merge prof addt))))) - {:id (:id user)})] - (-> (retrieve-user-by-email db/pool email) - (p/then' check-user)))) - -(def sql:user-by-email +(def sql:profile-by-email "select u.* - from users as u + from profile as u where u.email=$1 and u.deleted_at is null") -(defn- retrieve-user-by-email +(defn- retrieve-profile-by-email [conn email] - (db/query-one conn [sql:user-by-email email])) + (db/query-one conn [sql:profile-by-email email])) ;; --- Mutation: Add additional email @@ -97,7 +102,7 @@ ;; --- Mutation: Update Profile (own) (def ^:private sql:update-profile - "update users + "update profile set fullname = $2, lang = $3 where id = $1 @@ -123,27 +128,27 @@ ;; --- Mutation: Update Password (defn- validate-password! - [conn {:keys [user old-password] :as params}] - (p/let [profile (profile/retrieve-profile conn user) + [conn {:keys [profile-id old-password] :as params}] + (p/let [profile (profile/retrieve-profile conn profile-id) result (sodi.pwhash/verify old-password (:password profile))] (when-not (:valid result) (ex/raise :type :validation :code ::old-password-not-match)))) (defn update-password - [conn {:keys [user password]}] - (let [sql "update users + [conn {:keys [profile-id password]}] + (let [sql "update profile set password = $2 where id = $1 and deleted_at is null returning id" password (sodi.pwhash/derive password)] - (-> (db/query-one conn [sql user password]) + (-> (db/query-one conn [sql profile-id password]) (p/then' su/raise-not-found-if-nil) (p/then' su/constantly-nil)))) (s/def ::update-profile-password - (s/keys :req-un [::user ::password ::old-password])) + (s/keys :req-un [::profile-id ::password ::old-password])) (sm/defmutation ::update-profile-password [params] @@ -159,22 +164,22 @@ (s/def ::file ::imgs/upload) (s/def ::update-profile-photo - (s/keys :req-un [::user ::file])) + (s/keys :req-un [::profile-id ::file])) (sm/defmutation ::update-profile-photo - [{:keys [user file] :as params}] + [{:keys [profile-id file] :as params}] (db/with-atomic [conn db/pool] - (p/let [profile (profile/retrieve-profile conn user) + (p/let [profile (profile/retrieve-profile conn profile-id) photo (upload-photo conn params)] ;; Schedule deletion of old photo (tasks/schedule! conn {:name "remove-media" :props {:path (:photo profile)}}) ;; Save new photo - (update-profile-photo conn user photo)))) + (update-profile-photo conn profile-id photo)))) (defn- upload-photo - [conn {:keys [file user]}] + [conn {:keys [file profile-id]}] (when-not (imgs/valid-image-types? (:mtype file)) (ex/raise :type :validation :code :image-type-not-allowed @@ -191,12 +196,16 @@ (ust/save! media/media-storage name photo)))) (defn- update-profile-photo - [conn user path] - (let [sql "update users set photo=$1 where id=$2 and deleted_at is null returning id"] - (-> (db/query-one conn [sql (str path) user]) + [conn profile-id path] + (let [sql "update profile set photo=$1 + where id=$2 + and deleted_at is null + returning id"] + (-> (db/query-one conn [sql (str path) profile-id]) (p/then' su/raise-not-found-if-nil)))) + ;; --- Mutation: Register Profile (declare check-profile-existence!) @@ -212,18 +221,26 @@ :code :registration-disabled)) (db/with-atomic [conn db/pool] (check-profile-existence! conn params) - (register-profile conn params))) + (-> (register-profile conn params) + (p/then (fn [profile] + ;; TODO: send a correct link for email verification + (let [data {:to (:email params) + :name (:fullname params)}] + (p/do! + (emails/send! conn emails/register data) + profile))))))) -(def ^:private sql:insert-user - "insert into users (id, fullname, email, password, photo) - values ($1, $2, $3, $4, '') returning *") + +(def ^:private sql:insert-profile + "insert into profile (id, fullname, email, password, photo, is_demo) + values ($1, $2, $3, $4, '', $5) returning *") (def ^:private sql:insert-email - "insert into user_emails (user_id, email, is_main) + "insert into profile_email (profile_id, email, is_main) values ($1, $2, true)") (def ^:private sql:profile-existence - "select exists (select * from users + "select exists (select * from profile where email = $1 and deleted_at is null) as val") @@ -236,33 +253,40 @@ :code ::email-already-exists)) params)))) -(defn create-profile - "Create the user entry on the database with limited input +(defn- create-profile + "Create the profile entry on the database with limited input filling all the other fields with defaults." - [conn {:keys [id fullname email password] :as params}] + [conn {:keys [id fullname email password demo?] :as params}] (let [id (or id (uuid/next)) - password (sodi.pwhash/derive password) - sqlv1 [sql:insert-user - id - fullname - email - password] - sqlv2 [sql:insert-email id email]] - (p/let [profile (db/query-one conn sqlv1)] - (db/query-one conn sqlv2) - profile))) + demo? (if (boolean? demo?) demo? false) + password (sodi.pwhash/derive password)] + (db/query-one conn [sql:insert-profile id fullname email password demo?]))) + +(defn- create-profile-email + [conn {:keys [id email] :as profile}] + (-> (db/query-one conn [sql:insert-email id email]) + (p/then' su/constantly-nil))) (defn register-profile [conn params] - (-> (create-profile conn params) - (p/then' profile/strip-private-attrs) - (p/then (fn [profile] - ;; TODO: send a correct link for email verification - (let [data {:to (:email params) - :name (:fullname params)}] - (p/do! - (emails/send! conn emails/register data) - profile)))))) + (p/let [prof (create-profile conn params) + _ (create-profile-email conn prof) + + team (mt.teams/create-team conn {:profile-id (:id prof) + :name "Default" + :default? true}) + _ (mt.teams/create-team-profile conn {:team-id (:id team) + :profile-id (:id prof)}) + + proj (mt.projects/create-project conn {:profile-id (:id prof) + :team-id (:id team) + :name "Drafts" + :default? true}) + _ (mt.projects/create-project-profile conn {:project-id (:id proj) + :profile-id (:id prof)})] + (merge (profile/strip-private-attrs prof) + {:default-team team + :default-project proj}))) ;; --- Mutation: Request Profile Recovery @@ -270,24 +294,24 @@ (s/keys :req-un [::email])) (def sql:insert-recovery-token - "insert into tokens (user_id, token) values ($1, $2)") + "insert into password_recovery_token (profile_id, token) values ($1, $2)") (sm/defmutation ::request-profile-recovery [{:keys [email] :as params}] - (letfn [(create-recovery-token [conn {:keys [id] :as user}] + (letfn [(create-recovery-token [conn {:keys [id] :as profile}] (let [token (-> (sodi.prng/random-bytes 32) (sodi.util/bytes->b64s)) sql sql:insert-recovery-token] (-> (db/query-one conn [sql id token]) - (p/then (constantly (assoc user :token token)))))) - (send-email-notification [conn user] + (p/then (constantly (assoc profile :token token)))))) + (send-email-notification [conn profile] (emails/send! conn emails/password-recovery - {:to (:email user) - :token (:token user) - :name (:fullname user)}))] + {:to (:email profile) + :token (:token profile) + :name (:fullname profile)}))] (db/with-atomic [conn db/pool] - (-> (retrieve-user-by-email conn email) + (-> (retrieve-profile-by-email conn email) (p/then' su/raise-not-found-if-nil) (p/then #(create-recovery-token conn %)) (p/then #(send-email-notification conn %)) @@ -300,23 +324,77 @@ (s/keys :req-un [::token ::password])) (def sql:remove-recovery-token - "delete from tokenes where user_id=$1 and token=$2") + "delete from password_recovery_token where profile_id=$1 and token=$2") (sm/defmutation ::recover-profile [{:keys [token password]}] (letfn [(validate-token [conn token] - (let [sql "delete from tokens where token=$1 returning *" - sql "select * from tokens where token=$1"] + (let [sql "delete from password_recovery_token + where token=$1 returning *" + sql "select * from password_recovery_token + where token=$1"] (-> (db/query-one conn [sql token]) - (p/then' :user-id) + (p/then' :profile-id) (p/then' su/raise-not-found-if-nil)))) - (update-password [conn user-id] - (let [sql "update users set password=$2 where id=$1" + (update-password [conn profile-id] + (let [sql "update profile set password=$2 where id=$1" pwd (sodi.pwhash/derive password)] - (-> (db/query-one conn [sql user-id pwd]) + (-> (db/query-one conn [sql profile-id pwd]) (p/then' (constantly nil)))))] (db/with-atomic [conn db/pool] (-> (validate-token conn token) - (p/then (fn [user-id] (update-password conn user-id))))))) + (p/then (fn [profile-id] (update-password conn profile-id))))))) + +;; --- Mutation: Delete Profile + +(declare check-teams-ownership!) +(declare mark-profile-as-deleted!) + +(s/def ::delete-profile + (s/keys :req-un [::profile-id])) + +(sm/defmutation ::delete-profile + [{:keys [profile-id] :as params}] + (db/with-atomic [conn db/pool] + (check-teams-ownership! conn profile-id) + + ;; Schedule a complete deletion of profile + (tasks/schedule! conn {:name "delete-profile" + :delay (tm/duration {:hours 48}) + :props {:profile-id profile-id}}) + + (mark-profile-as-deleted! conn profile-id))) + +(def ^:private sql:teams-ownership-check + "with teams as ( + select tpr.team_id as id + from team_profile_rel as tpr + where tpr.profile_id = $1 + and tpr.is_owner is true + ) + select tpr.team_id, + count(tpr.profile_id) as num_profiles + from team_profile_rel as tpr + where tpr.team_id in (select id from teams) + group by tpr.team_id + having count(tpr.profile_id) > 1") + +(defn- check-teams-ownership! + [conn profile-id] + (-> (db/query conn [sql:teams-ownership-check profile-id]) + (p/then' (fn [rows] + (when-not (empty? rows) + (ex/raise :type :validation + :code :owner-teams-with-people + :hint "The user need to transfer ownership of owned teams." + :context {:teams (mapv :team-id rows)})))))) + +(def ^:private sql:mark-profile-deleted + "update profile set deleted_at=now() where id=$1") + +(defn- mark-profile-as-deleted! + [conn profile-id] + (-> (db/query-one conn [sql:mark-profile-deleted profile-id]) + (p/then' su/constantly-nil))) diff --git a/backend/src/uxbox/services/mutations/project_files.clj b/backend/src/uxbox/services/mutations/project_files.clj deleted file mode 100644 index aa70cd149d..0000000000 --- a/backend/src/uxbox/services/mutations/project_files.clj +++ /dev/null @@ -1,250 +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/. -;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2019-2020 Andrey Antukh - -(ns uxbox.services.mutations.project-files - (:require - [clojure.spec.alpha :as s] - [promesa.core :as p] - [datoteka.core :as fs] - [uxbox.db :as db] - [uxbox.media :as media] - [uxbox.images :as images] - [uxbox.common.exceptions :as ex] - [uxbox.common.spec :as us] - [uxbox.common.pages :as cp] - [uxbox.services.mutations :as sm] - [uxbox.services.mutations.projects :as proj] - [uxbox.services.mutations.images :as imgs] - [uxbox.services.util :as su] - [uxbox.util.blob :as blob] - [uxbox.util.uuid :as uuid] - [uxbox.util.storage :as ust] - [vertx.util :as vu])) - -;; --- 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 File - -(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) - page (create-page conn (assoc params :file-id (:id file)))] - (assoc file :pages [(:id page)])))) - -(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 cp/default-page-data) - 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: Rename File - -(declare rename-file) - -(s/def ::rename-project-file - (s/keys :req-un [::user ::name ::id])) - -(sm/defmutation ::rename-project-file - [{:keys [id user] :as params}] - (db/with-atomic [conn db/pool] - (check-edition-permissions! conn user id) - (rename-file conn params))) - -(def sql:rename-file - "update project_files - set name = $2 - where id = $1 - and deleted_at is null") - -(defn- rename-file - [conn {:keys [id name] :as params}] - (let [sql sql:rename-file] - (-> (db/query-one conn [sql id name]) - (p/then' su/constantly-nil)))) - - -;; --- Mutation: Delete Project File - -(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)))) - -;; --- Mutation: Upload File Image - -(s/def ::file-id ::us/uuid) -(s/def ::content ::imgs/upload) - -(s/def ::upload-project-file-image - (s/keys :req-un [::user ::file-id ::name ::content] - :opt-un [::id])) - -(declare create-file-image) - -(sm/defmutation ::upload-project-file-image - [{:keys [user file-id] :as params}] - (db/with-atomic [conn db/pool] - (check-edition-permissions! conn user file-id) - (create-file-image conn params))) - -(def ^:private - sql:insert-file-image - "insert into project_file_images - (file_id, user_id, name, path, width, height, mtype, - thumb_path, thumb_width, thumb_height, thumb_quality, thumb_mtype) - values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) - returning *") - -(defn- create-file-image - [conn {:keys [content file-id user name] :as params}] - (when-not (imgs/valid-image-types? (:mtype content)) - (ex/raise :type :validation - :code :image-type-not-allowed - :hint "Seems like you are uploading an invalid image.")) - - (p/let [image-opts (vu/blocking (images/info (:path content))) - image-path (imgs/persist-image-on-fs content) - thumb-opts imgs/thumbnail-options - thumb-path (imgs/persist-image-thumbnail-on-fs thumb-opts image-path) - - sqlv [sql:insert-file-image - file-id - user - name - (str image-path) - (:width image-opts) - (:height image-opts) - (:mtype content) - (str thumb-path) - (:width thumb-opts) - (:height thumb-opts) - (:quality thumb-opts) - (images/format->mtype (:format thumb-opts))]] - (-> (db/query-one db/pool sqlv) - (p/then' #(images/resolve-urls % :path :uri)) - (p/then' #(images/resolve-urls % :thumb-path :thumb-uri))))) - -;; --- Mutation: Import from collection - -(declare copy-image!) - -(s/def ::import-image-to-file - (s/keys :req-un [::image-id ::file-id ::user])) - -(def ^:private sql:select-image-by-id - "select img.* from images as img where id=$1") - -(sm/defmutation ::import-image-to-file - [{:keys [image-id file-id user]}] - (db/with-atomic [conn db/pool] - (p/let [image (-> (db/query-one conn [sql:select-image-by-id image-id]) - (p/then' su/raise-not-found-if-nil)) - image-path (copy-image! (:path image)) - thumb-path (copy-image! (:thumb-path image)) - sqlv [sql:insert-file-image - file-id - user - (:name image) - (str image-path) - (:width image) - (:height image) - (:mtype image) - (str thumb-path) - (:thumb-width image) - (:thumb-height image) - (:thumb-quality image) - (:thumb-mtype image)]] - (-> (db/query-one db/pool sqlv) - (p/then' #(images/resolve-urls % :path :uri)) - (p/then' #(images/resolve-urls % :thumb-path :thumb-uri)))))) - -(defn- copy-image! - [path] - (vu/blocking - (let [image-path (ust/lookup media/media-storage path)] - (ust/save! media/media-storage (fs/name image-path) image-path)))) diff --git a/backend/src/uxbox/services/mutations/project_pages.clj b/backend/src/uxbox/services/mutations/project_pages.clj deleted file mode 100644 index 53d94c3681..0000000000 --- a/backend/src/uxbox/services/mutations/project_pages.clj +++ /dev/null @@ -1,245 +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 - -(ns uxbox.services.mutations.project-pages - (:require - [clojure.spec.alpha :as s] - [promesa.core :as p] - [uxbox.common.pages :as cp] - [uxbox.common.exceptions :as ex] - [uxbox.common.spec :as us] - [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.sql :as sql] - [uxbox.util.uuid :as uuid] - [vertx.eventbus :as ve])) - -;; --- Helpers & Specs - -(s/def ::id ::us/uuid) -(s/def ::name ::us/string) -(s/def ::data ::cp/data) -(s/def ::user ::us/uuid) -(s/def ::project-id ::us/uuid) -(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 ::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] :as params}] - (let [sql "insert into project_pages (id, user_id, file_id, name, - ordering, data, version) - values ($1, $2, $3, $4, $5, $6, 0) - returning *" - id (or id (uuid/next)) - data (blob/encode data)] - (-> (db/query-one conn [sql id user file-id name ordering data]) - (p/then' decode-row)))) - -;; --- Mutation: Update Page Data - -(declare select-page-for-update) -(declare update-page-data) -(declare insert-page-snapshot) - -(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-data conn params) - (insert-page-snapshot conn params) - (select-keys params [:id :version])))))) - -(defn- select-page-for-update - [conn id] - (let [sql "select p.id, p.version, p.file_id, p.data - 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-data - [conn {:keys [id name version data]}] - (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- insert-page-snapshot - [conn {:keys [user-id id version data changes]}] - (let [sql "insert into project_page_snapshots (user_id, page_id, version, data, changes) - values ($1, $2, $3, $4, $5) - returning id, page_id, user_id, version, changes"] - (db/query-one conn [sql user-id id version data changes]))) - -;; --- 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 conn [sql id name]) - (p/then su/constantly-nil)))) - -;; --- Mutation: Update Page - -;; A generic, Changes based (granular) page update method. - -(s/def ::changes - (s/coll-of map? :kind vector?)) - -(s/def ::update-project-page - (s/keys :opt-un [::id ::user ::version ::changes])) - -(declare update-project-page) -(declare retrieve-lagged-changes) - -(sm/defmutation ::update-project-page - [{:keys [id user] :as params}] - (db/with-atomic [conn db/pool] - (p/let [{:keys [file-id] :as page} (select-page-for-update conn id)] - (files/check-edition-permissions! conn user file-id) - (update-project-page conn page params)))) - -(defn- update-project-page - [conn page params] - (when (> (:version params) - (:version page)) - (ex/raise :type :validation - :code :version-conflict - :hint "The incoming version is greater that stored version." - :context {:incoming-version (:version params) - :stored-version (:version page)})) - (let [changes (:changes params) - data (-> (:data page) - (blob/decode) - (cp/process-changes changes) - (blob/encode)) - - page (assoc page - :user-id (:user params) - :data data - :version (inc (:version page)) - :changes (blob/encode changes))] - - (-> (update-page-data conn page) - (p/then (fn [_] (insert-page-snapshot conn page))) - (p/then (fn [s] - (let [topic (str "internal.uxbox.file." (:file-id page))] - (p/do! (ve/publish! uxbox.core/system topic {:type :page-snapshot - :user-id (:user-id s) - :page-id (:page-id s) - :version (:version s) - :changes changes}) - (retrieve-lagged-changes conn s params)))))))) - -(def sql:lagged-snapshots - "select s.id, s.changes - from project_page_snapshots as s - where s.page_id = $1 - and s.version > $2 - order by s.created_at asc") - -(defn- retrieve-lagged-changes - [conn snapshot params] - (let [sql sql:lagged-snapshots] - (-> (db/query conn [sql (:id params) (:version params) #_(:id snapshot)]) - (p/then (fn [rows] - {:page-id (:id params) - :version (:version snapshot) - :changes (into [] (comp (map decode-row) - (map :changes) - (mapcat identity)) - rows)}))))) - -;; --- 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)))) - -(def sql:delete-page - "update project_pages - set deleted_at = clock_timestamp() - where id = $1 - and deleted_at is null") - -(defn- delete-page - [conn id] - (let [sql sql:delete-page] - (-> (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 aedbb6df73..5eae542c8d 100644 --- a/backend/src/uxbox/services/mutations/projects.clj +++ b/backend/src/uxbox/services/mutations/projects.clj @@ -2,15 +2,20 @@ ;; 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 +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2019-2020 Andrey Antukh (ns uxbox.services.mutations.projects (:require [clojure.spec.alpha :as s] [promesa.core :as p] + [uxbox.config :as cfg] [uxbox.db :as db] [uxbox.common.exceptions :as ex] [uxbox.common.spec :as us] + [uxbox.tasks :as tasks] [uxbox.services.mutations :as sm] [uxbox.services.util :as su] [uxbox.util.blob :as blob] @@ -20,65 +25,95 @@ (s/def ::id ::us/uuid) (s/def ::name ::us/string) -(s/def ::token ::us/string) -(s/def ::user ::us/uuid) +(s/def ::profile-id ::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;") + "select tpr.is_owner, + tpr.is_admin, + tpr.can_edit + from team_profile_rel as tpr + inner join project as p on (p.team_id = tpr.team_id) + where p.id = $1 + and tpr.profile_id = $2 + union all + select ppr.is_owner, + ppr.is_admin, + ppr.can_edit + from project_profile_rel as ppr + where ppr.project_id = $1 + and ppr.profile_id = $2") (defn check-edition-permissions! - [conn user project-id] - (-> (db/query-one conn [sql:project-permissions user project-id]) + [conn profile-id project-id] + (-> (db/query conn [sql:project-permissions project-id profile-id]) + (p/then' seq) (p/then' su/raise-not-found-if-nil) - (p/then' (fn [{:keys [id can-edit] :as proj}] - (when-not can-edit + (p/then' (fn [rows] + (when-not (or (some :can-edit rows) + (some :is-admin rows) + (some :is-owner rows)) (ex/raise :type :validation :code :not-authorized)))))) + + ;; --- Mutation: Create Project (declare create-project) +(declare create-project-profile) +(s/def ::team-id ::us/uuid) (s/def ::create-project - (s/keys :req-un [::user ::name] + (s/keys :req-un [::profile-id ::team-id ::name] :opt-un [::id])) (sm/defmutation ::create-project [params] (db/with-atomic [conn db/pool] - (create-project conn params))) + (p/let [proj (create-project conn params)] + (create-project-profile conn (assoc params :project-id (:id proj))) + proj))) + +(def ^:private sql:insert-project + "insert into project (id, team_id, name, is_default) + values ($1, $2, $3, $4) + returning *") (defn create-project - [conn {:keys [id user name] :as params}] + [conn {:keys [id profile-id team-id name default?] :as params}] (let [id (or id (uuid/next)) - sql "insert into projects (id, user_id, name) - values ($1, $2, $3) returning *"] - (db/query-one conn [sql id user name]))) + default? (if (boolean? default?) default? false)] + (db/query-one conn [sql:insert-project id team-id name default?]))) -;; --- Mutation: Update Project +(def ^:private sql:create-project-profile + "insert into project_profile_rel (project_id, profile_id, is_owner, is_admin, can_edit) + values ($1, $2, true, true, true) + returning *") + +(defn create-project-profile + [conn {:keys [project-id profile-id] :as params}] + (-> (db/query-one conn [sql:create-project-profile project-id profile-id]) + (p/then' su/constantly-nil))) + + + +;; --- Mutation: Rename Project (declare rename-project) (s/def ::rename-project - (s/keys :req-un [::user ::name ::id])) + (s/keys :req-un [::profile-id ::name ::id])) (sm/defmutation ::rename-project - [{:keys [id user] :as params}] + [{:keys [id profile-id] :as params}] (db/with-atomic [conn db/pool] - (check-edition-permissions! conn user id) + (check-edition-permissions! conn profile-id id) (rename-project conn params))) -(def sql:rename-project - "update projects +(def ^:private sql:rename-project + "update project set name = $2 where id = $1 and deleted_at is null @@ -86,31 +121,36 @@ (defn rename-project [conn {:keys [id name] :as params}] - (let [sql sql:rename-project] - (db/query-one conn [sql id name]))) + (db/query-one conn [sql:rename-project id name])) + + ;; --- Mutation: Delete Project -(declare delete-project) +(declare mark-project-deleted) (s/def ::delete-project - (s/keys :req-un [::id ::user])) + (s/keys :req-un [::id ::profile-id])) (sm/defmutation ::delete-project - [{:keys [id user] :as params}] + [{:keys [id profile-id] :as params}] (db/with-atomic [conn db/pool] - (check-edition-permissions! conn user id) - (delete-project conn params))) + (check-edition-permissions! conn profile-id id) -(def ^:private sql:delete-project - "update projects + ;; Schedule object deletion + (tasks/schedule! conn {:name "delete-object" + :delay cfg/default-deletion-delay + :props {:id id :type :project}}) + + (mark-project-deleted conn params))) + +(def ^:private sql:mark-project-deleted + "update project 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)))) +(defn mark-project-deleted + [conn {:keys [id profile-id] :as params}] + (-> (db/query-one conn [sql:mark-project-deleted id]) + (p/then' su/constantly-nil))) diff --git a/backend/src/uxbox/services/mutations/teams.clj b/backend/src/uxbox/services/mutations/teams.clj new file mode 100644 index 0000000000..36e69a9151 --- /dev/null +++ b/backend/src/uxbox/services/mutations/teams.clj @@ -0,0 +1,65 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 Andrey Antukh + +(ns uxbox.services.mutations.teams + (:require + [clojure.spec.alpha :as s] + [promesa.core :as p] + [uxbox.db :as db] + [uxbox.common.exceptions :as ex] + [uxbox.common.spec :as us] + [uxbox.services.mutations :as sm] + [uxbox.services.util :as su] + [uxbox.util.blob :as blob] + [uxbox.util.uuid :as uuid])) + +;; --- Helpers & Specs + +(s/def ::id ::us/uuid) +(s/def ::name ::us/string) +(s/def ::profile-id ::us/uuid) + +;; --- Mutation: Create Team + +(declare create-team) +(declare create-team-profile) + +(s/def ::create-team + (s/keys :req-un [::profile-id ::name] + :opt-un [::id])) + +(sm/defmutation ::create-team + [params] + (db/with-atomic [conn db/pool] + (p/let [team (create-team conn params)] + (create-team-profile conn (assoc params :team-id (:id team))) + team))) + +(def ^:private sql:insert-team + "insert into team (id, name, photo, is_default) + values ($1, $2, '', $3) + returning *") + +(defn create-team + [conn {:keys [id profile-id name default?] :as params}] + (let [id (or id (uuid/next)) + default? (if (boolean? default?) default? false)] + (db/query-one conn [sql:insert-team id name default?]))) + +(def ^:private sql:create-team-profile + "insert into team_profile_rel (team_id, profile_id, is_owner, is_admin, can_edit) + values ($1, $2, true, true, true) + returning *") + +(defn create-team-profile + [conn {:keys [team-id profile-id] :as params}] + (-> (db/query-one conn [sql:create-team-profile team-id profile-id]) + (p/then' su/constantly-nil))) + + diff --git a/backend/src/uxbox/services/queries/colors.clj b/backend/src/uxbox/services/queries/colors.clj new file mode 100644 index 0000000000..e1b9640e86 --- /dev/null +++ b/backend/src/uxbox/services/queries/colors.clj @@ -0,0 +1,102 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2019 Andrey Antukh + +(ns uxbox.services.queries.colors + (:require + [clojure.spec.alpha :as s] + [promesa.core :as p] + [promesa.exec :as px] + [uxbox.common.exceptions :as ex] + [uxbox.common.spec :as us] + [uxbox.db :as db] + [uxbox.media :as media] + [uxbox.images :as images] + [uxbox.services.queries :as sq] + [uxbox.services.util :as su] + [uxbox.util.blob :as blob] + [uxbox.util.data :as data] + [uxbox.util.uuid :as uuid] + [vertx.core :as vc])) + +;; --- Helpers & Specs + +(s/def ::id ::us/uuid) +(s/def ::profile-id ::us/uuid) +(s/def ::collection-id (s/nilable ::us/uuid)) + +(defn decode-row + [{:keys [metadata] :as row}] + (when row + (cond-> row + metadata (assoc :metadata (blob/decode metadata))))) + + + +;; --- Query: Collections + +(def ^:private sql:collections + "select *, + (select count(*) from color where collection_id = ic.id) as num_colors + from color_collection as ic + where (ic.profile_id = $1 or + ic.profile_id = '00000000-0000-0000-0000-000000000000'::uuid) + and ic.deleted_at is null + order by ic.created_at desc") + +(s/def ::color-collections + (s/keys :req-un [::profile-id])) + +(sq/defquery ::color-collections + [{:keys [profile-id] :as params}] + (let [sqlv [sql:collections profile-id]] + (db/query db/pool sqlv))) + + + +;; --- Colors By Collection ID + +(def ^:private sql:colors + "select * + from color as i + where (i.profile_id = $1 or + i.profile_id = '00000000-0000-0000-0000-000000000000'::uuid) + and i.deleted_at is null + and i.collection_id = $2 + order by i.created_at desc") + +(s/def ::colors + (s/keys :req-un [::profile-id ::collection-id])) + +(sq/defquery ::colors + [{:keys [profile-id collection-id] :as params}] + (-> (db/query db/pool [sql:colors profile-id collection-id]) + (p/then' #(mapv decode-row %)))) + + + +;; --- Query: Color (by ID) + +(declare retrieve-color) + +(s/def ::id ::us/uuid) +(s/def ::color + (s/keys :req-un [::profile-id ::id])) + +(sq/defquery ::color + [{:keys [id] :as params}] + (-> (retrieve-color db/pool id) + (p/then' su/raise-not-found-if-nil))) + +(defn retrieve-color + [conn id] + (let [sql "select * from color + where id = $1 + and deleted_at is null;"] + (-> (db/query-one conn [sql id]) + (p/then' su/raise-not-found-if-nil)))) diff --git a/backend/src/uxbox/services/queries/files.clj b/backend/src/uxbox/services/queries/files.clj new file mode 100644 index 0000000000..c798832d21 --- /dev/null +++ b/backend/src/uxbox/services/queries/files.clj @@ -0,0 +1,203 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2019-2020 Andrey Antukh + +(ns uxbox.services.queries.files + (:require + [clojure.spec.alpha :as s] + [promesa.core :as p] + [uxbox.common.exceptions :as ex] + [uxbox.common.spec :as us] + [uxbox.db :as db] + [uxbox.images :as images] + [uxbox.services.queries :as sq] + [uxbox.services.util :as su] + [uxbox.util.blob :as blob])) + +(declare decode-row) + +;; --- Helpers & Specs + +(s/def ::id ::us/uuid) +(s/def ::name ::us/string) +(s/def ::project-id ::us/uuid) +(s/def ::file-id ::us/uuid) +(s/def ::profile-id ::us/uuid) + +;; --- Query: Draft Files + +(def ^:private sql:files + "select distinct + f.*, + array_agg(pg.id) over pages_w as pages, + first_value(pg.data) over pages_w as data + from file as f + inner join file_profile_rel as fp_r on (fp_r.file_id = f.id) + left join page as pg on (f.id = pg.file_id) + where fp_r.profile_id = $1 + and f.project_id = $2 + and f.deleted_at is null + and pg.deleted_at is null + and (fp_r.is_admin = true or + fp_r.is_owner = true or + fp_r.can_edit = true) + window pages_w as (partition by f.id order by pg.created_at + range between unbounded preceding + and unbounded following) + order by f.created_at") + +(s/def ::project-id ::us/uuid) +(s/def ::files + (s/keys :req-un [::profile-id ::project-id])) + +(sq/defquery ::files + [{:keys [profile-id project-id] :as params}] + (-> (db/query db/pool [sql:files profile-id project-id]) + (p/then (partial mapv decode-row)))) + +;; --- Query: File Permissions + +(def ^:private sql:file-permissions + "select fpr.is_owner, + fpr.is_admin, + fpr.can_edit + from file_profile_rel as fpr + where fpr.file_id = $1 + and fpr.profile_id = $2 + union all + select tpr.is_owner, + tpr.is_admin, + tpr.can_edit + from team_profile_rel as tpr + inner join project as p on (p.team_id = tpr.team_id) + inner join file as f on (p.id = f.project_id) + where f.id = $1 + and tpr.profile_id = $2 + union all + select ppr.is_owner, + ppr.is_admin, + ppr.can_edit + from project_profile_rel as ppr + inner join file as f on (f.project_id = ppr.project_id) + where f.id = $1 + and ppr.profile_id = $2;") + +(defn check-edition-permissions! + [conn profile-id file-id] + (-> (db/query conn [sql:file-permissions file-id profile-id]) + (p/then' seq) + (p/then' su/raise-not-found-if-nil) + (p/then' (fn [rows] + (when-not (or (some :can-edit rows) + (some :is-admin rows) + (some :is-owner rows)) + (ex/raise :type :validation + :code :not-authorized)))))) + +;; --- Query: Images of the File + +(declare retrieve-file-images) + +(s/def ::file-images + (s/keys :req-un [::profile-id ::file-id])) + +(sq/defquery ::file-images + [{:keys [profile-id file-id] :as params}] + (db/with-atomic [conn db/pool] + (check-edition-permissions! conn profile-id file-id) + (retrieve-file-images conn params))) + +(def ^:private sql:file-images + "select fi.* + from file_image as fi + where fi.file_id = $1") + +(defn retrieve-file-images + [conn {:keys [file-id] :as params}] + (let [sqlv [sql:file-images file-id] + xf (comp (map #(images/resolve-urls % :path :uri)) + (map #(images/resolve-urls % :thumb-path :thumb-uri)))] + (-> (db/query conn sqlv) + (p/then' #(into [] xf %))))) + +;; --- Query: File (By ID) + +(def ^:private sql:file + "select f.*, + array_agg(pg.id) over pages_w as pages + from file as f + left join page as pg on (f.id = pg.file_id) + where f.id = $1 + and f.deleted_at is null + and pg.deleted_at is null + window pages_w as (partition by f.id order by pg.created_at + range between unbounded preceding + and unbounded following)") + +(def ^:private sql:file-users + "select pf.id, pf.fullname, pf.photo + from profile as pf + inner join file_profile_rel as fpr on (fpr.profile_id = pf.id) + where fpr.file_id = $1 + union + select pf.id, pf.fullname, pf.photo + from profile as pf + inner join team_profile_rel as tpr on (tpr.profile_id = pf.id) + inner join project as p on (tpr.team_id = p.team_id) + inner join file as f on (p.id = f.project_id) + where f.id = $1") + +(s/def ::file-with-users + (s/keys :req-un [::profile-id ::id])) + +(sq/defquery ::file-with-users + [{:keys [profile-id id] :as params}] + (db/with-atomic [conn db/pool] + (check-edition-permissions! conn profile-id id) + (p/let [file (-> (db/query-one conn [sql:file id]) + (p/then' su/raise-not-found-if-nil) + (p/then' decode-row)) + users (db/query conn [sql:file-users id])] + (assoc file :users users)))) + +(s/def ::file + (s/keys :req-un [::profile-id ::id])) + +(sq/defquery ::file + [{:keys [profile-id id] :as params}] + (db/with-atomic [conn db/pool] + (check-edition-permissions! conn profile-id id) + (-> (db/query-one conn [sql:file id]) + (p/then' su/raise-not-found-if-nil) + (p/then' decode-row)))) + +;; --- Query: Project Files + +;; (declare retrieve-project-files) + +;; (s/def ::project-files +;; (s/keys :req-un [::profile-id] +;; :opt-un [::project-id])) + +;; (sq/defquery ::project-files +;; [{:keys [project-id] :as params}] +;; (retrieve-project-files db/pool params)) + +;; (defn retrieve-project-files +;; [conn {:keys [profile-id project-id]}] +;; (-> (db/query conn [sql:project-files profile-id project-id]) +;; (p/then' (partial mapv decode-row)))) + +;; --- Helpers + +(defn decode-row + [{:keys [pages data] :as row}] + (when row + (cond-> row + data (assoc :data (blob/decode data)) + pages (assoc :pages (vec (remove nil? pages)))))) diff --git a/backend/src/uxbox/services/queries/icons.clj b/backend/src/uxbox/services/queries/icons.clj index e9b23f56f5..64e0558047 100644 --- a/backend/src/uxbox/services/queries/icons.clj +++ b/backend/src/uxbox/services/queries/icons.clj @@ -2,68 +2,101 @@ ;; 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/. ;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; ;; Copyright (c) 2019 Andrey Antukh (ns uxbox.services.queries.icons (:require [clojure.spec.alpha :as s] [promesa.core :as p] + [promesa.exec :as px] [uxbox.common.exceptions :as ex] [uxbox.common.spec :as us] [uxbox.db :as db] + [uxbox.media :as media] + [uxbox.images :as images] [uxbox.services.queries :as sq] - [uxbox.util.blob :as blob])) + [uxbox.services.util :as su] + [uxbox.util.blob :as blob] + [uxbox.util.data :as data] + [uxbox.util.uuid :as uuid] + [vertx.core :as vc])) ;; --- Helpers & Specs (s/def ::id ::us/uuid) -(s/def ::user ::us/uuid) +(s/def ::profile-id ::us/uuid) (s/def ::collection-id (s/nilable ::us/uuid)) -(defn decode-icon-row +(defn decode-row [{:keys [metadata] :as row}] (when row (cond-> row metadata (assoc :metadata (blob/decode metadata))))) + + ;; --- Query: Collections -(def sql:icons-collections +(def ^:private sql:collections "select *, - (select count(*) from icons where collection_id = ic.id) as num_icons - from icon_collections as ic - where (ic.user_id = $1 or - ic.user_id = '00000000-0000-0000-0000-000000000000'::uuid) + (select count(*) from icon where collection_id = ic.id) as num_icons + from icon_collection as ic + where (ic.profile_id = $1 or + ic.profile_id = '00000000-0000-0000-0000-000000000000'::uuid) and ic.deleted_at is null order by ic.created_at desc") -(s/def ::icons-collections - (s/keys :req-un [::user])) +(s/def ::icon-collections + (s/keys :req-un [::profile-id])) -(sq/defquery ::icons-collections - [{:keys [user] :as params}] - (let [sqlv [sql:icons-collections user]] +(sq/defquery ::icon-collections + [{:keys [profile-id] :as params}] + (let [sqlv [sql:collections profile-id]] (db/query db/pool sqlv))) + + ;; --- Icons By Collection ID -(def ^:private icons-by-collection-sql +(def ^:private sql:icons "select * - from icons as i - where (i.user_id = $1 or - i.user_id = '00000000-0000-0000-0000-000000000000'::uuid) + from icon as i + where (i.profile_id = $1 or + i.profile_id = '00000000-0000-0000-0000-000000000000'::uuid) and i.deleted_at is null - and case when $2::uuid is null then i.collection_id is null - else i.collection_id = $2::uuid - end + and i.collection_id = $2 order by i.created_at desc") -(s/def ::icons-by-collection - (s/keys :req-un [::user] - :opt-un [::collection-id])) +(s/def ::icons + (s/keys :req-un [::profile-id ::collection-id])) + +(sq/defquery ::icons + [{:keys [profile-id collection-id] :as params}] + (-> (db/query db/pool [sql:icons profile-id collection-id]) + (p/then' #(mapv decode-row %)))) + + +;; --- Query: Icon (by ID) + +(declare retrieve-icon) + +(s/def ::id ::us/uuid) +(s/def ::icon + (s/keys :req-un [::profile-id ::id])) + +(sq/defquery ::icon + [{:keys [id] :as params}] + (-> (retrieve-icon db/pool id) + (p/then' su/raise-not-found-if-nil))) + +(defn retrieve-icon + [conn id] + (let [sql "select * from icon + where id = $1 + and deleted_at is null;"] + (-> (db/query-one conn [sql id]) + (p/then' su/raise-not-found-if-nil)))) -(sq/defquery ::icons-by-collection - [{:keys [user collection-id] :as params}] - (let [sqlv [icons-by-collection-sql user collection-id]] - (-> (db/query db/pool sqlv) - (p/then' #(mapv decode-icon-row %))))) diff --git a/backend/src/uxbox/services/queries/images.clj b/backend/src/uxbox/services/queries/images.clj index 5b5f60a95b..60e08d4526 100644 --- a/backend/src/uxbox/services/queries/images.clj +++ b/backend/src/uxbox/services/queries/images.clj @@ -2,6 +2,9 @@ ;; 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/. ;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; ;; Copyright (c) 2019 Andrey Antukh (ns uxbox.services.queries.images @@ -23,73 +26,74 @@ (s/def ::id ::us/uuid) (s/def ::name ::us/string) -(s/def ::user ::us/uuid) +(s/def ::profile-id ::us/uuid) (s/def ::collection-id (s/nilable ::us/uuid)) -;; --- Query: Images Collections +;; --- Query: Image Collections (def ^:private sql:collections "select *, - (select count(*) from images where collection_id = ic.id) as num_images - from image_collections as ic - where (ic.user_id = $1 or - ic.user_id = '00000000-0000-0000-0000-000000000000'::uuid) + (select count(*) from image where collection_id = ic.id) as num_images + from image_collection as ic + where (ic.profile_id = $1 or + ic.profile_id = '00000000-0000-0000-0000-000000000000'::uuid) and ic.deleted_at is null order by ic.created_at desc;") -(s/def ::images-collections - (s/keys :req-un [::user])) +(s/def ::image-collections + (s/keys :req-un [::profile-id])) -(sq/defquery ::images-collections - [{:keys [user] :as params}] - (db/query db/pool [sql:collections user])) +(sq/defquery ::image-collections + [{:keys [profile-id] :as params}] + (db/query db/pool [sql:collections profile-id])) -;; --- Query: Image by ID -(defn retrieve-image - [conn id] - (let [sql "select * from images - where id = $1 - and deleted_at is null;"] - (db/query-one conn [sql id]))) +;; --- Query: Image (by ID) + +(declare retrieve-image) (s/def ::id ::us/uuid) -(s/def ::image-by-id - (s/keys :req-un [::user ::id])) +(s/def ::image + (s/keys :req-un [::profile-id ::id])) -(sq/defquery ::image-by-id - [params] - (-> (retrieve-image db/pool (:id params)) +(sq/defquery ::image + [{:keys [id] :as params}] + (-> (retrieve-image db/pool id) (p/then' #(images/resolve-urls % :path :uri)) (p/then' #(images/resolve-urls % :thumb-path :thumb-uri)))) -;; --- Query: Images by collection ID +(defn retrieve-image + [conn id] + (let [sql "select * from image + where id = $1 + and deleted_at is null;"] + (-> (db/query-one conn [sql id]) + (p/then' su/raise-not-found-if-nil)))) -(def sql:images-by-collection - "select * from images - where (user_id = $1 or - user_id = '00000000-0000-0000-0000-000000000000'::uuid) + + +;; --- Query: Images (by collection) + +(def ^:private sql:images + "select * + from image + where (profile_id = $1 or + profile_id = '00000000-0000-0000-0000-000000000000'::uuid) and deleted_at is null + and collection_id = $2 order by created_at desc") -(def sql:images-by-collection - (str "with images as (" sql:images-by-collection ") - select im.* from images as im - where im.collection_id = $2")) - -(s/def ::images-by-collection - (s/keys :req-un [::user] - :opt-un [::collection-id])) +(s/def ::images + (s/keys :req-un [::profile-id ::collection-id])) ;; TODO: check if we can resolve url with transducer for reduce ;; garbage generation for each request -(sq/defquery ::images-by-collection - [{:keys [user collection-id] :as params}] - (let [sqlv [sql:images-by-collection user collection-id]] - (-> (db/query db/pool sqlv) - (p/then' (fn [rows] - (->> rows - (mapv #(images/resolve-urls % :path :uri)) - (mapv #(images/resolve-urls % :thumb-path :thumb-uri)))))))) +(sq/defquery ::images + [{:keys [profile-id collection-id] :as params}] + (-> (db/query db/pool [sql:images profile-id collection-id]) + (p/then' (fn [rows] + (->> rows + (mapv #(images/resolve-urls % :path :uri)) + (mapv #(images/resolve-urls % :thumb-path :thumb-uri))))))) diff --git a/backend/src/uxbox/services/queries/pages.clj b/backend/src/uxbox/services/queries/pages.clj new file mode 100644 index 0000000000..50efc85acd --- /dev/null +++ b/backend/src/uxbox/services/queries/pages.clj @@ -0,0 +1,138 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2019-2020 Andrey Antukh + +(ns uxbox.services.queries.pages + (:require + [clojure.spec.alpha :as s] + [promesa.core :as p] + [uxbox.common.spec :as us] + [uxbox.db :as db] + [uxbox.services.queries :as sq] + [uxbox.services.util :as su] + [uxbox.services.queries.files :as files] + [uxbox.util.blob :as blob] + [uxbox.util.sql :as sql])) + +;; --- Helpers & Specs + +(declare decode-row) + +(s/def ::id ::us/uuid) +(s/def ::profile-id ::us/uuid) +(s/def ::project-id ::us/uuid) +(s/def ::file-id ::us/uuid) + + + +;; --- Query: Pages (By File ID) + +(declare retrieve-pages) + +(s/def ::pages + (s/keys :req-un [::profile-id ::file-id])) + +(sq/defquery ::pages + [{:keys [profile-id file-id] :as params}] + (db/with-atomic [conn db/pool] + (files/check-edition-permissions! conn profile-id file-id) + (retrieve-pages conn params))) + +(def ^:private sql:pages + "select p.* + from page as p + where p.file_id = $1 + and p.deleted_at is null + order by p.created_at asc") + +(defn- retrieve-pages + [conn {:keys [profile-id file-id] :as params}] + (-> (db/query conn [sql:pages file-id]) + (p/then (partial mapv decode-row)))) + + + +;; --- Query: Single Page (By ID) + +(declare retrieve-page) + +(s/def ::page + (s/keys :req-un [::profile-id ::id])) + +(sq/defquery ::page + [{:keys [profile-id id] :as params}] + (db/with-atomic [conn db/pool] + (p/let [page (retrieve-page conn id)] + (files/check-edition-permissions! conn profile-id (:file-id page)) + page))) + +(def ^:private sql:page + "select p.* from page as p where id=$1") + +(defn retrieve-page + [conn id] + (-> (db/query-one conn [sql:page id]) + (p/then' su/raise-not-found-if-nil) + (p/then' decode-row))) + + + +;; --- 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 ::project-page-snapshots +;; (s/keys :req-un [::page-id ::user] +;; :opt-un [::max ::pinned ::since])) + +;; (defn retrieve-page-snapshots +;; [conn {:keys [page-id user since max pinned] :or {since Long/MAX_VALUE max 10}}] +;; (let [sql (-> (sql/from ["project_page_snapshots" "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 conn (sql/fmt sql)) +;; (p/then (partial mapv decode-row))))) + +;; (sq/defquery ::project-page-snapshots +;; [{:keys [page-id user] :as params}] +;; (db/with-atomic [conn db/pool] +;; (p/do! (retrieve-page conn {:id page-id :user user}) +;; (retrieve-page-snapshots conn params)))) + +;; --- Helpers + +(defn decode-row + [{:keys [data metadata changes] :as row}] + (when row + (cond-> row + data (assoc :data (blob/decode data)) + changes (assoc :changes (blob/decode changes))))) diff --git a/backend/src/uxbox/services/queries/profile.clj b/backend/src/uxbox/services/queries/profile.clj index d8ae24fbc4..d15e19b442 100644 --- a/backend/src/uxbox/services/queries/profile.clj +++ b/backend/src/uxbox/services/queries/profile.clj @@ -28,27 +28,59 @@ (s/def ::password ::us/string) (s/def ::path ::us/string) (s/def ::user ::us/uuid) -(s/def ::username ::us/string) +(s/def ::profile-id ::us/uuid) ;; --- Query: Profile (own) (defn retrieve-profile [conn id] - (let [sql "select * from users where id=$1 and deleted_at is null"] + (let [sql "select * from profile where id=$1 and deleted_at is null"] (db/query-one db/pool [sql id]))) + +;; NOTE: this query make the assumption that union all preserves the +;; order so the first id will always be the team id and the second the +;; project_id; this is a postgresql behavior because UNION ALL works +;; like APPEND operation. + +(def ^:private sql:default-team-and-project + "select t.id + from team as t + inner join team_profile_rel as tpr on (tpr.team_id = t.id) + where tpr.profile_id = $1 + and tpr.is_owner is true + and t.is_default is true + union all + select p.id + from project as p + inner join project_profile_rel as tpr on (tpr.project_id = p.id) + where tpr.profile_id = $1 + and tpr.is_owner is true + and p.is_default is true") + +(defn retrieve-additional-data + [conn id] + (-> (db/query conn [sql:default-team-and-project id]) + (p/then' (fn [[team project]] + {:default-team-id (:id team) + :default-project-id (:id project)})))) + (s/def ::profile - (s/keys :req-un [::user])) + (s/keys :req-un [::profile-id])) (sq/defquery ::profile - [{:keys [user] :as params}] - (-> (retrieve-profile db/pool user) - (p/then' strip-private-attrs) - (p/then' #(images/resolve-media-uris % [:photo :photo-uri])))) + [{:keys [profile-id] :as params}] + (db/with-atomic [conn db/pool] + (p/let [prof (-> (retrieve-profile conn profile-id) + (p/then' su/raise-not-found-if-nil) + (p/then' strip-private-attrs) + (p/then' #(images/resolve-media-uris % [:photo :photo-uri]))) + addt (retrieve-additional-data conn profile-id)] + (merge prof addt)))) ;; --- Attrs Helpers (defn strip-private-attrs - "Only selects a publicy visible user attrs." + "Only selects a publicy visible profile attrs." [profile] (select-keys profile [:id :fullname :lang :email :created-at :photo])) diff --git a/backend/src/uxbox/services/queries/project_files.clj b/backend/src/uxbox/services/queries/project_files.clj deleted file mode 100644 index f65c9859b7..0000000000 --- a/backend/src/uxbox/services/queries/project_files.clj +++ /dev/null @@ -1,199 +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/. -;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2019-2020 Andrey Antukh - -(ns uxbox.services.queries.project-files - (:require - [clojure.spec.alpha :as s] - [promesa.core :as p] - [uxbox.common.spec :as us] - [uxbox.db :as db] - [uxbox.images :as images] - [uxbox.services.queries :as sq] - [uxbox.services.util :as su] - [uxbox.util.blob :as blob])) - -(declare decode-row) - -;; --- Helpers & Specs - -(s/def ::id ::us/uuid) -(s/def ::name ::us/string) -(s/def ::project-id ::us/uuid) -(s/def ::file-id ::us/uuid) -(s/def ::user ::us/uuid) - -;; --- Query: Project Files - -(declare retrieve-recent-files) -(declare retrieve-project-files) - -(s/def ::project-files - (s/keys :req-un [::user] - :opt-un [::project-id])) - -(sq/defquery ::project-files - [{:keys [project-id] :as params}] - (if (nil? project-id) - (retrieve-recent-files db/pool params) - (retrieve-project-files db/pool params))) - -(def ^:private sql:generic-project-files - "select distinct - pf.*, - array_agg(pp.id) over pages_w as pages, - first_value(pp.data) over pages_w as data, - p.name as project_name - from project_users as pu - inner join project_files as pf on (pf.project_id = pu.project_id) - inner join projects as p on (p.id = pf.project_id) - left join project_pages as pp on (pf.id = pp.file_id) - where pu.user_id = $1 - and pu.can_edit = true - window pages_w as (partition by pf.id order by pp.created_at - range between unbounded preceding - and unbounded following) - order by pf.created_at") - -(def ^:private sql:project-files - (str "with files as (" sql:generic-project-files ") " - "select * from files where project_id = $2")) - -(defn retrieve-project-files - [conn {:keys [user project-id]}] - (-> (db/query conn [sql:project-files user project-id]) - (p/then' (partial mapv decode-row)))) - -(def ^:private sql:recent-files - "with project_files as ( - (select pf.*, - array_agg(pp.id) over pages_w as pages, - first_value(pp.data) over pages_w as data, - p.name as project_name - from project_users as pu - inner join project_files as pf on (pf.project_id = pu.project_id) - inner join projects as p on (p.id = pf.project_id) - left join project_pages as pp on (pf.id = pp.file_id) - where pu.user_id = $1 - and pu.can_edit = true - window pages_w as (partition by pf.id order by pp.created_at - range between unbounded preceding - and unbounded following)) - union - (select pf.*, - array_agg(pp.id) over pages_w as pages, - first_value(pp.data) over pages_w as data, - p.name as project_name - from project_file_users as pfu - inner join project_files as pf on (pfu.file_id = pf.id) - inner join projects as p on (p.id = pf.project_id) - left join project_pages as pp on (pf.id = pp.file_id) - where pfu.user_id = $1 - and pfu.can_edit = true - window pages_w as (partition by pf.id order by pp.created_at - range between unbounded preceding - and unbounded following)) - ) select pf1.* - from project_files as pf1 - order by pf1.modified_at desc - limit $2;") - - -(defn retrieve-recent-files - [conn {:keys [user]}] - (-> (db/query conn [sql:recent-files user 20]) - (p/then' (partial mapv decode-row)))) - -;; --- Query: Project File (By ID) - -(def ^:private sql:project-file - (str "with files as (" sql:generic-project-files ") " - "select * from files where id = $2")) - -(s/def ::project-file - (s/keys :req-un [::user ::id])) - -(sq/defquery ::project-file - [{:keys [user id] :as params}] - (-> (db/query-one db/pool [sql:project-file user id]) - (p/then' decode-row))) - -;; --- Query: Users of the File - -(declare retrieve-minimal-file) -(declare retrieve-file-users) - -(s/def ::project-file-users - (s/keys :req-un [::user ::file-id])) - -(sq/defquery ::project-file-users - [{:keys [user file-id] :as params}] - (db/with-atomic [conn db/pool] - (-> (retrieve-minimal-file conn user file-id) - (p/then #(retrieve-file-users conn %))))) - -(def ^:private sql:minimal-file - (str "with files as (" sql:generic-project-files ") " - "select id, project_id from files where id = $2")) - -(defn- retrieve-minimal-file - [conn user-id file-id] - (-> (db/query-one conn [sql:minimal-file user-id file-id]) - (p/then' su/raise-not-found-if-nil))) - -(def ^:private sql:file-users - "select u.id, u.fullname, u.photo - from users as u - join project_file_users as pfu on (pfu.user_id = u.id) - where pfu.file_id = $1 - union all - select u.id, u.fullname, u.photo - from users as u - join project_users as pu on (pu.user_id = u.id) - where pu.project_id = $2") - -(defn- retrieve-file-users - [conn {:keys [id project-id] :as file}] - (let [sqlv [sql:file-users id project-id]] - (db/query conn sqlv))) - - -;; --- Query: Images of the File - -(declare retrieve-file-images) - -(s/def ::project-file-images - (s/keys :req-un [::user ::file-id])) - -(sq/defquery ::project-file-images - [{:keys [user file-id] :as params}] - (db/with-atomic [conn db/pool] - (-> (retrieve-minimal-file conn user file-id) - (p/then #(retrieve-file-images conn %))))) - -(def ^:private sql:file-images - "select pfi.* - from project_file_images as pfi - where pfi.file_id = $1") - -(defn retrieve-file-images - [conn {:keys [id] :as file}] - (let [sqlv [sql:file-images id] - xf (comp (map #(images/resolve-urls % :path :uri)) - (map #(images/resolve-urls % :thumb-path :thumb-uri)))] - (-> (db/query conn sqlv) - (p/then' #(into [] xf %))))) - -;; --- Helpers - -(defn decode-row - [{:keys [pages data] :as row}] - (when row - (cond-> row - data (assoc :data (blob/decode data)) - pages (assoc :pages (vec (remove nil? pages)))))) diff --git a/backend/src/uxbox/services/queries/project_pages.clj b/backend/src/uxbox/services/queries/project_pages.clj deleted file mode 100644 index cffdaaa4a7..0000000000 --- a/backend/src/uxbox/services/queries/project_pages.clj +++ /dev/null @@ -1,128 +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 - -(ns uxbox.services.queries.project-pages - (:require - [clojure.spec.alpha :as s] - [promesa.core :as p] - [uxbox.common.spec :as us] - [uxbox.db :as db] - [uxbox.services.queries :as sq] - [uxbox.services.util :as su] - [uxbox.util.blob :as blob] - [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 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)) - and pp.deleted_at is null - order by pp.created_at") - -;; --- Query: Project Pages (By File ID) - -(def sql:project-pages - (str "with pages as (" sql:generic-project-pages ")" - " select * from pages where file_id = $2")) - -(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 ::project-page-snapshots - (s/keys :req-un [::page-id ::user] - :opt-un [::max ::pinned ::since])) - -(defn retrieve-page-snapshots - [conn {:keys [page-id user since max pinned] :or {since Long/MAX_VALUE max 10}}] - (let [sql (-> (sql/from ["project_page_snapshots" "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 conn (sql/fmt sql)) - (p/then (partial mapv decode-row))))) - -(sq/defquery ::project-page-snapshots - [{:keys [page-id user] :as params}] - (db/with-atomic [conn db/pool] - (p/do! (retrieve-page conn {:id page-id :user user}) - (retrieve-page-snapshots conn params)))) - -;; --- Helpers - -(defn decode-row - [{:keys [data metadata changes] :as row}] - (when row - (cond-> row - data (assoc :data (blob/decode data)) - metadata (assoc :metadata (blob/decode metadata)) - changes (assoc :changes (blob/decode changes))))) diff --git a/backend/src/uxbox/services/queries/projects.clj b/backend/src/uxbox/services/queries/projects.clj index 9bdf66364f..61dd6edad3 100644 --- a/backend/src/uxbox/services/queries/projects.clj +++ b/backend/src/uxbox/services/queries/projects.clj @@ -16,37 +16,38 @@ (declare decode-row) -;; --- Helpers & Specs - -(s/def ::id ::us/uuid) -(s/def ::name ::us/string) -(s/def ::token ::us/string) -(s/def ::user ::us/uuid) - - ;; --- Query: Projects -(def sql:projects - "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 - order by p.created_at asc") +(def ^:private sql:projects + "with projects as ( + select p.* + from project as p + inner join team_profile_rel as tpr on (tpr.team_id = p.team_id) + where tpr.profile_id = $1 + and (tpr.is_admin = true or + tpr.is_owner = true or + tpr.can_edit = true) + union + select p.* + from project as p + inner join project_profile_rel as ppr on (ppr.project_id = p.id) + where ppr.profile_id = $1 + and (ppr.is_admin = true or + ppr.is_owner = true or + ppr.can_edit = true) + ) + select * + from projects + where team_id = $2 + order by created_at asc") -(s/def ::projects - (s/keys :req-un [::user])) +(s/def ::team-id ::us/uuid) +(s/def ::profile-id ::us/uuid) -(sq/defquery ::projects - [{:keys [user] :as params}] - (-> (db/query db/pool [sql:projects user]) - (p/then' (partial mapv decode-row)))) +(s/def ::projects-by-team + (s/keys :req-un [::profile-id ::team-id])) +(sq/defquery ::projects-by-team + [{:keys [profile-id team-id] :as params}] + (db/query db/pool [sql:projects profile-id team-id])) -;; --- 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/util.clj b/backend/src/uxbox/services/util.clj index 6d73a37308..704194c3d8 100644 --- a/backend/src/uxbox/services/util.clj +++ b/backend/src/uxbox/services/util.clj @@ -14,17 +14,6 @@ [uxbox.util.uuid :as uuid] [uxbox.util.dispatcher :as uds])) -;; (def logging-interceptor -;; {:enter (fn [data] -;; (let [type (get-in data [:request ::type])] -;; (assoc data ::start-time (System/nanoTime)))) -;; :leave (fn [data] -;; (let [elapsed (- (System/nanoTime) (::start-time data)) -;; elapsed (str (quot elapsed 1000000) "ms") -;; type (get-in data [:request ::type])] -;; (log/info "service" type "processed in" elapsed) -;; data))}) - (defn raise-not-found-if-nil [v] (if (nil? v) diff --git a/backend/src/uxbox/tasks.clj b/backend/src/uxbox/tasks.clj index 3d188c5c10..c6a0963a3f 100644 --- a/backend/src/uxbox/tasks.clj +++ b/backend/src/uxbox/tasks.clj @@ -20,7 +20,8 @@ [uxbox.db :as db] [uxbox.tasks.sendmail] [uxbox.tasks.remove-media] - [uxbox.tasks.remove-demo-profile] + [uxbox.tasks.delete-profile] + [uxbox.tasks.delete-object] [uxbox.tasks.impl :as impl] [uxbox.util.time :as dt] [vertx.core :as vc] @@ -42,7 +43,8 @@ ;; need to perform a maintenance and delete some old tasks. (def ^:private tasks - {"remove-demo-profile" #'uxbox.tasks.remove-demo-profile/handler + {"delete-profile" #'uxbox.tasks.delete-profile/handler + "delete-object" #'uxbox.tasks.delete-object/handler "remove-media" #'uxbox.tasks.remove-media/handler "sendmail" #'uxbox.tasks.sendmail/handler}) @@ -54,7 +56,7 @@ ;; (def ^:private schedule ;; [{:id "every 1 hour" ;; :cron (dt/cron "1 1 */1 * * ? *") -;; :fn #'uxbox.tasks.demo-gc/handler +;; :fn #'uxbox.tasks.gc/handler ;; :props {:foo 1}}]) ;; (defstate scheduler diff --git a/backend/src/uxbox/tasks/delete_object.clj b/backend/src/uxbox/tasks/delete_object.clj new file mode 100644 index 0000000000..52812c69f3 --- /dev/null +++ b/backend/src/uxbox/tasks/delete_object.clj @@ -0,0 +1,81 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 Andrey Antukh + +(ns uxbox.tasks.delete-object + "Generic task for permanent deletion of objects." + (:require + [clojure.spec.alpha :as s] + [clojure.tools.logging :as log] + [promesa.core :as p] + [uxbox.common.exceptions :as ex] + [uxbox.common.spec :as us] + [uxbox.db :as db] + [uxbox.media :as media] + [uxbox.util.storage :as ust] + [vertx.util :as vu])) + +(s/def ::type keyword?) +(s/def ::id ::us/uuid) + +(s/def ::props + (s/keys :req-un [::id ::type])) + +(defmulti handle-deletion (fn [conn props] (:type props))) + +(defmethod handle-deletion :default + [conn {:keys [type id] :as props}] + (log/warn "no handler found for" type)) + +(defn handler + [{:keys [props] :as task}] + (us/verify ::props props) + (db/with-atomic [conn db/pool] + (handle-deletion conn props))) + +(defmethod handle-deletion :image + [conn {:keys [id] :as props}] + (let [sql "delete from image where id=$1 and deleted_at is not null"] + (db/query-one conn [sql id]))) + +(defmethod handle-deletion :image-collection + [conn {:keys [id] :as props}] + (let [sql "delete from image_collection + where id=$1 and deleted_at is not null"] + (db/query-one conn [sql id]))) + +(defmethod handle-deletion :icon + [conn {:keys [id] :as props}] + (let [sql "delete from icon where id=$1 and deleted_at is not null"] + (db/query-one conn [sql id]))) + +(defmethod handle-deletion :icon-collection + [conn {:keys [id] :as props}] + (let [sql "delete from icon_collection + where id=$1 and deleted_at is not null"] + (db/query-one conn [sql id]))) + +(defmethod handle-deletion :file + [conn {:keys [id] :as props}] + (let [sql "delete from file where id=$1 and deleted_at is not null"] + (db/query-one conn [sql id]))) + +(defmethod handle-deletion :file-image + [conn {:keys [id] :as props}] + (let [sql "delete from file_image where id=$1 and deleted_at is not null"] + (db/query-one conn [sql id]))) + +(defmethod handle-deletion :page + [conn {:keys [id] :as props}] + (let [sql "delete from page where id=$1 and deleted_at is not null"] + (db/query-one conn [sql id]))) + +(defmethod handle-deletion :page-version + [conn {:keys [id] :as props}] + (let [sql "delete from page_version where id=$1 and deleted_at is not null"] + (db/query-one conn [sql id]))) diff --git a/backend/src/uxbox/tasks/delete_profile.clj b/backend/src/uxbox/tasks/delete_profile.clj new file mode 100644 index 0000000000..557428310f --- /dev/null +++ b/backend/src/uxbox/tasks/delete_profile.clj @@ -0,0 +1,110 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 Andrey Antukh + +(ns uxbox.tasks.delete-profile + "Task for permanent deletion of profiles." + (:require + [clojure.spec.alpha :as s] + [clojure.tools.logging :as log] + [promesa.core :as p] + [uxbox.common.exceptions :as ex] + [uxbox.common.spec :as us] + [uxbox.db :as db] + [uxbox.media :as media] + [uxbox.util.storage :as ust] + [vertx.util :as vu])) + +(declare select-profile) +(declare delete-profile-data) +(declare delete-teams) +(declare delete-files) +(declare delete-profile) + +(s/def ::profile-id ::us/uuid) +(s/def ::props + (s/keys :req-un [::profile-id])) + +(defn handler + [{:keys [props] :as task}] + (us/verify ::props props) + (db/with-atomic [conn db/pool] + (-> (select-profile conn (:profile-id props)) + (p/then (fn [profile] + (if (or (:is-demo profile) + (not (nil? (:deleted-at profile)))) + (delete-profile-data conn (:id profile)) + (log/warn "Profile " (:id profile) + "does not match constraints for deletion"))))))) + +(defn- delete-profile-data + [conn profile-id] + (log/info "Proceding to delete all data related to profile" profile-id) + (p/do! + (delete-teams conn profile-id) + (delete-files conn profile-id) + (delete-profile conn profile-id))) + +(def ^:private sql:select-profile + "select id, is_demo, deleted_at + from profile + where id=$1 for update") + +(defn- select-profile + [conn profile-id] + (db/query-one conn [sql:select-profile profile-id])) + + +(def ^:private sql:remove-owned-teams + "with teams as ( + select distinct + tpr.team_id as id + from team_profile_rel as tpr + where tpr.profile_id = $1 + and tpr.is_owner is true + ), to_delete_teams as ( + select tpr.team_id as id + from team_profile_rel as tpr + where tpr.team_id in (select id from teams) + group by tpr.team_id + having count(tpr.profile_id) = 1 + ) + delete from team + where id in (select id from to_delete_teams) + returning id") + +(defn- delete-teams + [conn profile-id] + (-> (db/query-one conn [sql:remove-owned-teams profile-id]) + (p/then' (constantly nil)))) + +(def ^:private sql:remove-owned-files + "with files_to_delete as ( + select distinct + fpr.file_id as id + from file_profile_rel as fpr + inner join file as f on (fpr.file_id = f.id) + where fpr.profile_id = $1 + and fpr.is_owner is true + and f.project_id is null + ) + delete from file + where id in (select id from files_to_delete) + returning id") + +(defn- delete-files + [conn profile-id] + (-> (db/query-one conn [sql:remove-owned-files profile-id]) + (p/then' (constantly nil)))) + +(defn delete-profile + [conn profile-id] + (let [sql "delete from profile where id=$1"] + (-> (db/query conn [sql profile-id]) + (p/then' (constantly profile-id))))) + diff --git a/backend/src/uxbox/tasks/gc.clj b/backend/src/uxbox/tasks/gc.clj index ae4da990ce..c4db5c9d95 100644 --- a/backend/src/uxbox/tasks/gc.clj +++ b/backend/src/uxbox/tasks/gc.clj @@ -20,31 +20,34 @@ [uxbox.db :as db] [uxbox.util.blob :as blob])) -;; TODO: add images-gc with proper resource removal -;; TODO: add icons-gc -;; TODO: add pages-gc -;; TODO: test this +;; TODO: delete media referenced in pendint_to_delete table -;; --- Delete Projects +;; (def ^:private sql:delete-item +;; "with items_part as ( +;; select i.id +;; from pending_to_delete as i +;; order by i.created_at +;; limit 1 +;; for update skip locked +;; ) +;; delete from pending_to_delete +;; where id in (select id from items_part) +;; returning *") -(def ^:private sql:delete-project - "delete from projects - where id = $1 - and deleted_at is not null;") +;; (defn- remove-items +;; [] +;; (vu/loop [] +;; (db/with-atomic [conn db/pool] +;; (-> (db/query-one conn sql:delete-item) +;; (p/then decode-row) +;; (p/then (vu/wrap-blocking remove-media)) +;; (p/then (fn [item] +;; (when (not (empty? items)) +;; (p/recur)))))))) -(s/def ::id ::us/uuid) -(s/def ::delete-project - (s/keys :req-un [::id])) - -(defn- delete-project - "Clean deleted projects." - [{:keys [id] :as props}] - (us/verify ::delete-project props) - (db/with-atomic [conn db/pool] - (-> (db/query-one conn [sql:delete-project id]) - (p/then (constantly nil))))) - -(defn handler - {:uxbox.tasks/name "delete-project"} - [{:keys [props] :as task}] - (delete-project props)) +;; (defn- remove-media +;; [{:keys +;; (doseq [item files] +;; (ust/delete! media/media-storage (:path item)) +;; (ust/delete! media/media-storage (:thumb-path item))) +;; files) diff --git a/backend/src/uxbox/tasks/impl.clj b/backend/src/uxbox/tasks/impl.clj index 6d92ce2b5d..dc299d02a6 100644 --- a/backend/src/uxbox/tasks/impl.clj +++ b/backend/src/uxbox/tasks/impl.clj @@ -38,7 +38,7 @@ (.printStackTrace err (java.io.PrintWriter. *out*)))) (def ^:private sql:mark-as-retry - "update tasks + "update task set scheduled_at = clock_timestamp() + '5 seconds'::interval, error = $1, status = 'retry', @@ -53,7 +53,7 @@ (p/then' (constantly nil))))) (def ^:private sql:mark-as-failed - "update tasks + "update task set scheduled_at = clock_timestamp() + '5 seconds'::interval, error = $1, status = 'failed' @@ -67,7 +67,7 @@ (p/then' (constantly nil))))) (def ^:private sql:mark-as-completed - "update tasks + "update task set completed_at = clock_timestamp(), status = 'completed' where id = $1") @@ -87,7 +87,7 @@ nil)))) (def ^:private sql:select-next-task - "select * from tasks as t + "select * from task as t where t.scheduled_at <= now() and t.queue = $1 and (t.status = 'new' or (t.status = 'retry' and t.retry_num <= $2)) @@ -141,7 +141,7 @@ (event-loop-handler (assoc options ::counter (inc counter))))))))) (def ^:private sql:insert-new-task - "insert into tasks (name, props, queue, scheduled_at) + "insert into task (name, props, queue, scheduled_at) values ($1, $2, $3, clock_timestamp()+cast($4::text as interval)) returning id") @@ -162,7 +162,7 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (def ^:privatr sql:upsert-scheduled-task - "insert into scheduled_tasks (id, cron_expr) + "insert into scheduled_task (id, cron_expr) values ($1, $2) on conflict (id) do update set cron_expr=$2") @@ -178,7 +178,7 @@ (p/run! (partial synchronize-schedule-item conn) schedule))) (def ^:private sql:lock-scheduled-task - "select id from scheduled_tasks where id=$1 for update skip locked") + "select id from scheduled_task where id=$1 for update skip locked") (declare schedule-task) diff --git a/backend/src/uxbox/tasks/remove_demo_profile.clj b/backend/src/uxbox/tasks/remove_demo_profile.clj deleted file mode 100644 index ce4ca85856..0000000000 --- a/backend/src/uxbox/tasks/remove_demo_profile.clj +++ /dev/null @@ -1,93 +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/. -;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 Andrey Antukh - -(ns uxbox.tasks.remove-demo-profile - "Demo accounts garbage collector." - (:require - [clojure.spec.alpha :as s] - [clojure.tools.logging :as log] - [promesa.core :as p] - [uxbox.common.exceptions :as ex] - [uxbox.common.spec :as us] - [uxbox.db :as db] - [uxbox.media :as media] - [uxbox.util.storage :as ust] - [vertx.util :as vu])) - -(declare remove-file-images) -(declare remove-images) -(declare remove-profile) - -(s/def ::id ::us/uuid) -(s/def ::props - (s/keys :req-un [::id])) - -(defn handler - [{:keys [props] :as task}] - (us/verify ::props props) - (db/with-atomic [conn db/pool] - (remove-file-images conn (:id props)) - (remove-images conn (:id props)) - (remove-profile conn (:id props)))) - -(defn- remove-files - [files] - (doseq [item files] - (ust/delete! media/media-storage (:path item)) - (ust/delete! media/media-storage (:thumb-path item))) - files) - -(def ^:private sql:delete-file-images - "with images_part as ( - select pfi.id - from project_file_images as pfi - inner join project_files as pf on (pf.id = pfi.file_id) - inner join projects as p on (p.id = pf.project_id) - where p.user_id = $1 - limit 10 - ) - delete from project_file_images - where id in (select id from images_part) - returning id, path, thumb_path") - -(defn remove-file-images - [conn id] - (vu/loop [] - (-> (db/query conn [sql:delete-file-images id]) - (p/then (vu/wrap-blocking remove-files)) - (p/then (fn [images] - (when (not (empty? images)) - (p/recur))))))) - -(def ^:private sql:delete-images - "with images_part as ( - select img.id - from images as img - where img.user_id = $1 - limit 10 - ) - delete from images - where id in (select id from images_part) - returning id, path, thumb_path") - -(defn- remove-images - [conn id] - (vu/loop [] - (-> (db/query conn [sql:delete-images id]) - (p/then (vu/wrap-blocking remove-files)) - (p/then (fn [images] - (when (not (empty? images)) - (p/recur))))))) - -(defn remove-profile - [conn id] - (let [sql "delete from users where id=$1"] - (db/query conn [sql id]))) - - diff --git a/backend/src/uxbox/util/dispatcher.clj b/backend/src/uxbox/util/dispatcher.clj index df6b84014e..dfe06987e8 100644 --- a/backend/src/uxbox/util/dispatcher.clj +++ b/backend/src/uxbox/util/dispatcher.clj @@ -11,8 +11,6 @@ [clojure.spec.alpha :as s] [promesa.core :as p] [expound.alpha :as expound] - [sieppari.core :as sp] - [sieppari.context :as spx] [uxbox.common.exceptions :as ex]) (:import clojure.lang.IDeref @@ -45,8 +43,7 @@ (let [key (get params attr) f (.get ^Map reg key)] (when (nil? f) - (ex/raise :type :not-found - :code :method-not-found + (ex/raise :type :method-not-found :hint "No method found for the current request.")) (f params)))) diff --git a/backend/tests/uxbox/tests/helpers.clj b/backend/tests/uxbox/tests/helpers.clj index 5d24948c4f..cbf238e442 100644 --- a/backend/tests/uxbox/tests/helpers.clj +++ b/backend/tests/uxbox/tests/helpers.clj @@ -7,9 +7,12 @@ [environ.core :refer [env]] [uxbox.services.mutations.profile :as profile] [uxbox.services.mutations.projects :as projects] - [uxbox.services.mutations.project-files :as files] - [uxbox.services.mutations.project-pages :as pages] + [uxbox.services.mutations.teams :as teams] + [uxbox.services.mutations.files :as files] + [uxbox.services.mutations.pages :as pages] [uxbox.services.mutations.images :as images] + [uxbox.services.mutations.icons :as icons] + [uxbox.services.mutations.colors :as colors] [uxbox.fixtures :as fixtures] [uxbox.migrations] [uxbox.media] @@ -67,51 +70,66 @@ ;; --- Profile creation -(defn create-user +(defn create-profile [conn i] - (profile/create-profile conn {:id (mk-uuid "user" i) - :fullname (str "User " i) - :email (str "user" i ".test@nodomain.com") - :password "123123" - :metadata {}})) + (#'profile/register-profile conn {:id (mk-uuid "profile" i) + :fullname (str "Profile " i) + :email (str "profile" i ".test@nodomain.com") + :password "123123"})) + +(defn create-team + [conn profile-id i] + (#'teams/create-team conn {:id (mk-uuid "team" i) + :profile-id profile-id + :name (str "team" i)})) (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)})) + [conn profile-id team-id i] + (#'projects/create-project conn {:id (mk-uuid "project" i) + :profile-id profile-id + :team-id team-id + :name (str "project" i)})) + +(defn create-file + [conn profile-id project-id i] + (#'files/create-file conn {:id (mk-uuid "file" i) + :profile-id profile-id + :project-id project-id + :name (str "file" i)})) + +(defn create-page + [conn profile-id file-id i] + (#'pages/create-page conn {:id (mk-uuid "page" i) + :profile-id profile-id + :file-id file-id + :name (str "page" i) + :ordering i + :data {:version 1 + :shapes [] + :options {} + :canvas [] + :shapes-by-id {}}})) -(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-image-collection + [conn profile-id i] + (#'images/create-image-collection conn {:id (mk-uuid "imgcoll" i) + :profile-id profile-id + :name (str "image collection " i)})) - -(defn create-project-page - [conn user-id file-id i] - (pages/create-page conn {:id (mk-uuid "page" i) - :user user-id - :file-id file-id - :name (str "page" i) - :ordering i - :data {:version 1 - :shapes [] - :options {} - :canvas [] - :shapes-by-id {}}})) - -(defn create-images-collection - [conn user-id i] - (images/create-images-collection conn {:id (mk-uuid "imgcoll" i) - :user user-id - :name (str "image collection " i)})) +(defn create-icon-collection + [conn profile-id i] + (#'icons/create-icon-collection conn {:id (mk-uuid "imgcoll" i) + :profile-id profile-id + :name (str "icon collection " i)})) +(defn create-color-collection + [conn profile-id i] + (#'colors/create-color-collection conn {:id (mk-uuid "imgcoll" i) + :profile-id profile-id + :name (str "color collection " i)})) (defn handle-error - [err] + [^Throwable err] (if (instance? java.util.concurrent.ExecutionException err) (handle-error (.getCause err)) err)) @@ -128,10 +146,11 @@ [expr] `(try (let [d# (p/deferred)] - (->> #(p/finally ~expr (fn [v# e#] - (if e# - (p/reject! d# e#) - (p/resolve! d# v#)))) + (->> #(p/finally (p/do! ~expr) + (fn [v# e#] + (if e# + (p/reject! d# e#) + (p/resolve! d# v#)))) (vu/run-on-context! *context*)) (array-map :error nil :result (deref d#))) @@ -155,8 +174,11 @@ (= :spec-validation (:code data)) (println (:explain data)) + (= :service-error (:type data)) + (print-error! (.getCause ^Throwable error)) + :else - (.printStackTrace error)))) + (.printStackTrace ^Throwable error)))) (defn print-result! [{:keys [error result]}] diff --git a/backend/tests/uxbox/tests/test_icons.clj b/backend/tests/uxbox/tests/test_icons.clj deleted file mode 100644 index 4abac86a83..0000000000 --- a/backend/tests/uxbox/tests/test_icons.clj +++ /dev/null @@ -1,162 +0,0 @@ -(ns uxbox.tests.test-icons - #_(:require [clojure.test :as t] - [promesa.core :as p] - [suricatta.core :as sc] - [uxbox.db :as db] - [uxbox.sql :as sql] - [uxbox.http :as http] - [uxbox.services.icons :as icons] - [uxbox.services :as usv] - [uxbox.tests.helpers :as th])) - -;; (t/use-fixtures :once th/state-init) -;; (t/use-fixtures :each th/database-reset) - -;; (t/deftest test-http-list-icon-collections -;; (with-open [conn (db/connection)] -;; (let [user (th/create-user conn 1) -;; data {:user (:id user) -;; :name "coll1"} -;; coll (icons/create-collection conn data)] -;; (th/with-server {:handler @http/app} -;; (let [uri (str th/+base-url+ "/api/library/icon-collections") -;; [status data] (th/http-get user uri)] -;; ;; (println "RESPONSE:" status data) -;; (t/is (= 200 status)) -;; (t/is (= 1 (count data)))))))) - -;; (t/deftest test-http-create-icon-collection -;; (with-open [conn (db/connection)] -;; (let [user (th/create-user conn 1)] -;; (th/with-server {:handler @http/app} -;; (let [uri (str th/+base-url+ "/api/library/icon-collections") -;; data {:user (:id user) -;; :name "coll1"} -;; params {:body data} -;; [status data] (th/http-post user uri params)] -;; ;; (println "RESPONSE:" status data) -;; (t/is (= 201 status)) -;; (t/is (= (:user data) (:id user))) -;; (t/is (= (:name data) "coll1"))))))) - -;; (t/deftest test-http-update-icon-collection -;; (with-open [conn (db/connection)] -;; (let [user (th/create-user conn 1) -;; data {:user (:id user) -;; :name "coll1"} -;; coll (icons/create-collection conn data)] -;; (th/with-server {:handler @http/app} -;; (let [uri (str th/+base-url+ "/api/library/icon-collections/" (:id coll)) -;; params {:body (assoc coll :name "coll2")} -;; [status data] (th/http-put user uri params)] -;; ;; (println "RESPONSE:" status data) -;; (t/is (= 200 status)) -;; (t/is (= (:user data) (:id user))) -;; (t/is (= (:name data) "coll2"))))))) - -;; (t/deftest test-http-icon-collection-delete -;; (with-open [conn (db/connection)] -;; (let [user (th/create-user conn 1) -;; data {:user (:id user) -;; :name "coll1" -;; :data #{1}} -;; coll (icons/create-collection conn data)] -;; (th/with-server {:handler @http/app} -;; (let [uri (str th/+base-url+ "/api/library/icon-collections/" (:id coll)) -;; [status data] (th/http-delete user uri)] -;; (t/is (= 204 status)) -;; (let [sqlv (sql/get-icon-collections {:user (:id user)}) -;; result (sc/fetch conn sqlv)] -;; (t/is (empty? result)))))))) - -;; (t/deftest test-http-create-icon -;; (with-open [conn (db/connection)] -;; (let [user (th/create-user conn 1)] -;; (th/with-server {:handler @http/app} -;; (let [uri (str th/+base-url+ "/api/library/icons") -;; data {:name "sample.jpg" -;; :content "" -;; :metadata {:width 200 -;; :height 200 -;; :view-box [0 0 200 200]} -;; :collection nil} -;; params {:body data} -;; [status data] (th/http-post user uri params)] -;; ;; (println "RESPONSE:" status data) -;; (t/is (= 201 status)) -;; (t/is (= (:user data) (:id user))) -;; (t/is (= (:name data) "sample.jpg")) -;; (t/is (= (:metadata data) {:width 200 -;; :height 200 -;; :view-box [0 0 200 200]}))))))) - -;; (t/deftest test-http-update-icon -;; (with-open [conn (db/connection)] -;; (let [user (th/create-user conn 1) -;; data {:user (:id user) -;; :name "test.svg" -;; :content "" -;; :metadata {} -;; :collection nil} -;; icon (icons/create-icon conn data)] -;; (th/with-server {:handler @http/app} -;; (let [uri (str th/+base-url+ "/api/library/icons/" (:id icon)) -;; params {:body (assoc icon :name "my stuff")} -;; [status data] (th/http-put user uri params)] -;; ;; (println "RESPONSE:" status data) -;; (t/is (= 200 status)) -;; (t/is (= (:user data) (:id user))) -;; (t/is (= (:name data) "my stuff"))))))) - -;; (t/deftest test-http-copy-icon -;; (with-open [conn (db/connection)] -;; (let [user (th/create-user conn 1) -;; data {:user (:id user) -;; :name "test.svg" -;; :content "" -;; :metadata {} -;; :collection nil} -;; icon (icons/create-icon conn data)] -;; (th/with-server {:handler @http/app} -;; (let [uri (str th/+base-url+ "/api/library/icons/" (:id icon) "/copy") -;; body {:collection nil} -;; params {:body body} -;; [status data] (th/http-put user uri params)] -;; ;; (println "RESPONSE:" status data) -;; (t/is (= status 200)) -;; (let [sqlv (sql/get-icons {:user (:id user) :collection nil}) -;; result (sc/fetch conn sqlv)] -;; (t/is (= 2 (count result))))))))) - -;; (t/deftest test-http-delete-icon -;; (with-open [conn (db/connection)] -;; (let [user (th/create-user conn 1) -;; data {:user (:id user) -;; :name "test.svg" -;; :content "" -;; :metadata {} -;; :collection nil} -;; icon (icons/create-icon conn data)] -;; (th/with-server {:handler @http/app} -;; (let [uri (str th/+base-url+ "/api/library/icons/" (:id icon)) -;; [status data] (th/http-delete user uri)] -;; (t/is (= 204 status)) -;; (let [sqlv (sql/get-icons {:user (:id user) :collection nil}) -;; result (sc/fetch conn sqlv)] -;; (t/is (empty? result)))))))) - -;; (t/deftest test-http-list-icons -;; (with-open [conn (db/connection)] -;; (let [user (th/create-user conn 1) -;; data {:user (:id user) -;; :name "test.png" -;; :content "" -;; :metadata {} -;; :collection nil} -;; icon (icons/create-icon conn data)] -;; (th/with-server {:handler @http/app} -;; (let [uri (str th/+base-url+ "/api/library/icons") -;; [status data] (th/http-get user uri)] -;; ;; (println "RESPONSE:" status data) -;; (t/is (= 200 status)) -;; (t/is (= 1 (count data)))))))) diff --git a/backend/tests/uxbox/tests/test_services_colors.clj b/backend/tests/uxbox/tests/test_services_colors.clj new file mode 100644 index 0000000000..bdfbb47fa8 --- /dev/null +++ b/backend/tests/uxbox/tests/test_services_colors.clj @@ -0,0 +1,159 @@ +(ns uxbox.tests.test-services-colors + (:require + [clojure.test :as t] + [promesa.core :as p] + [datoteka.core :as fs] + [clojure.java.io :as io] + [uxbox.db :as db] + [uxbox.core :refer [system]] + [uxbox.services.mutations :as sm] + [uxbox.services.queries :as sq] + [uxbox.util.storage :as ust] + [uxbox.util.uuid :as uuid] + [uxbox.tests.helpers :as th] + [vertx.core :as vc])) + +(t/use-fixtures :once th/state-init) +(t/use-fixtures :each th/database-reset) + +(t/deftest color-collections-crud + (let [id (uuid/next) + profile @(th/create-profile db/pool 2)] + + (t/testing "create collection" + (let [data {::sm/type :create-color-collection + :name "sample collection" + :profile-id (:id profile) + :id id} + out (th/try-on! (sm/handle data))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + + (let [result (:result out)] + (t/is (= (:id profile) (:profile-id result))) + (t/is (= (:name data) (:name result)))))) + + (t/testing "update collection" + (let [data {::sm/type :rename-color-collection + :name "sample collection renamed" + :profile-id (:id profile) + :id id} + out (th/try-on! (sm/handle data))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + + (t/is (= id (get-in out [:result :id]))) + (t/is (= (:id profile) (get-in out [:result :profile-id]))) + (t/is (= (:name data) (get-in out [:result :name]))))) + + (t/testing "query collections" + (let [data {::sq/type :color-collections + :profile-id (:id profile)} + 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 profile) (get-in out [:result 0 :profile-id]))) + (t/is (= id (get-in out [:result 0 :id]))))) + + (t/testing "delete collection" + (let [data {::sm/type :delete-color-collection + :profile-id (:id profile) + :id id} + + out (th/try-on! (sm/handle data))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (nil? (:result out))))) + + (t/testing "query collections after delete" + (let [data {::sq/type :color-collections + :profile-id (:id profile)} + out (th/try-on! (sq/handle data))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (= 0 (count (:result out)))))) + )) + +(t/deftest colors-crud + (let [profile @(th/create-profile db/pool 1) + coll @(th/create-color-collection db/pool (:id profile) 1) + color-id (uuid/next)] + + (t/testing "upload color to collection" + (let [data {::sm/type :create-color + :id color-id + :profile-id (:id profile) + :collection-id (:id coll) + :name "testfile" + :content "#222222"} + out (th/try-on! (sm/handle data))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + + (let [result (:result out)] + (t/is (= (:id data) (:id result))) + (t/is (= (:name data) (:name result))) + (t/is (= (:content data) (:content result)))))) + + (t/testing "list colors by collection" + (let [data {::sq/type :colors + :profile-id (:id profile) + :collection-id (:id coll)} + out (th/try-on! (sq/handle data))] + ;; (th/print-result! out) + + (t/is (= color-id (get-in out [:result 0 :id]))) + (t/is (= "testfile" (get-in out [:result 0 :name]))))) + + (t/testing "single color" + (let [data {::sq/type :color + :profile-id (:id profile) + :id color-id} + out (th/try-on! (sq/handle data))] + ;; (th/print-result! out) + + (t/is (= color-id (get-in out [:result :id]))) + (t/is (= "testfile" (get-in out [:result :name]))))) + + (t/testing "delete colors" + (let [data {::sm/type :delete-color + :profile-id (:id profile) + :id color-id} + out (th/try-on! (sm/handle data))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (nil? (get-in out [:result]))))) + + (t/testing "query color after delete" + (let [data {::sq/type :color + :profile-id (:id profile) + :id color-id} + out (th/try-on! (sq/handle data))] + + ;; (th/print-result! out) + (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))))) + + (t/testing "query colors after delete" + (let [data {::sq/type :colors + :profile-id (:id profile) + :collection-id (:id coll)} + out (th/try-on! (sq/handle data))] + ;; (th/print-result! out) + (let [result (:result out)] + (t/is (= 0 (count result)))))) + )) diff --git a/backend/tests/uxbox/tests/test_services_files.clj b/backend/tests/uxbox/tests/test_services_files.clj new file mode 100644 index 0000000000..42cd609d14 --- /dev/null +++ b/backend/tests/uxbox/tests/test_services_files.clj @@ -0,0 +1,220 @@ +(ns uxbox.tests.test-services-files + (:require + [clojure.test :as t] + [promesa.core :as p] + [datoteka.core :as fs] + [uxbox.db :as db] + [uxbox.media :as media] + [uxbox.core :refer [system]] + [uxbox.http :as http] + [uxbox.services.mutations :as sm] + [uxbox.services.queries :as sq] + [uxbox.tests.helpers :as th] + [uxbox.util.storage :as ust] + [uxbox.util.uuid :as uuid] + [vertx.util :as vu])) + +(t/use-fixtures :once th/state-init) +(t/use-fixtures :each th/database-reset) + +(t/deftest files-crud + (let [prof @(th/create-profile db/pool 1) + team (:default-team prof) + proj (:default-project prof) + file-id (uuid/next) + page-id (uuid/next)] + + (t/testing "create file" + (let [data {::sm/type :create-file + :profile-id (:id prof) + :project-id (:id proj) + :id file-id + :name "test file"} + out (th/try-on! (sm/handle data))] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + + (let [result (:result out)] + (t/is (= (:name data) (:name result))) + (t/is (= (:id proj) (:project-id result)))))) + + (t/testing "rename file" + (let [data {::sm/type :rename-file + :id file-id + :name "new name" + :profile-id (:id prof)} + out (th/try-on! (sm/handle data))] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (nil? (:result out))))) + + (t/testing "query files" + (let [data {::sq/type :files + :project-id (:id proj) + :profile-id (:id prof)} + out (th/try-on! (sq/handle data))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + + (let [result (:result out)] + (t/is (= 1 (count result))) + (t/is (= file-id (get-in result [0 :id]))) + (t/is (= "new name" (get-in result [0 :name]))) + (t/is (= 1 (count (get-in result [0 :pages]))))))) + + (t/testing "query single file with users" + (let [data {::sq/type :file-with-users + :profile-id (:id prof) + :id file-id} + out (th/try-on! (sq/handle data))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + + (let [result (:result out)] + (t/is (= file-id (:id result))) + (t/is (= "new name" (:name result))) + (t/is (vector? (:pages result))) + (t/is (= 1 (count (:pages result)))) + (t/is (vector? (:users result))) + (t/is (= (:id prof) (get-in result [:users 0 :id])))))) + + (t/testing "query single file without users" + (let [data {::sq/type :file + :profile-id (:id prof) + :id file-id} + out (th/try-on! (sq/handle data))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + + (let [result (:result out)] + (t/is (= file-id (:id result))) + (t/is (= "new name" (:name result))) + (t/is (vector? (:pages result))) + (t/is (= 1 (count (:pages result)))) + (t/is (nil? (:users result)))))) + + (t/testing "delete file" + (let [data {::sm/type :delete-file + :id file-id + :profile-id (:id prof)} + out (th/try-on! (sm/handle data))] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (nil? (:result out))))) + + (t/testing "query single file after delete" + (let [data {::sq/type :file + :profile-id (:id prof) + :id file-id} + out (th/try-on! (sq/handle data))] + + ;; (th/print-result! out) + + (let [error (:error out) + error-data (ex-data error)] + (t/is (th/ex-info? error)) + (t/is (= (:type error-data) :service-error)) + (t/is (= (:name error-data) :uxbox.services.queries.files/file))) + + (let [error (ex-cause (:error out)) + error-data (ex-data error)] + (t/is (th/ex-info? error)) + (t/is (= (:type error-data) :not-found))))) + + (t/testing "query list files after delete" + (let [data {::sq/type :files + :project-id (:id proj) + :profile-id (:id prof)} + out (th/try-on! (sq/handle data))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + + (let [result (:result out)] + (t/is (= 0 (count result)))))) + )) + +(t/deftest file-images-crud + (let [prof @(th/create-profile db/pool 1) + team (:default-team prof) + proj (:default-project prof) + file @(th/create-file db/pool (:id prof) (:id proj) 1)] + + (t/testing "upload file image" + (let [content {:name "sample.jpg" + :path "tests/uxbox/tests/_files/sample.jpg" + :mtype "image/jpeg" + :size 312043} + data {::sm/type :upload-file-image + :profile-id (:id prof) + :file-id (:id file) + :name "testfile" + :content content + :width 800 + :height 800} + + out (th/try-on! (sm/handle data))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + + (let [result (:result out)] + (t/is (= (:id file) (:file-id result))) + (t/is (= (:name data) (:name result))) + (t/is (= (:width data) (:width result))) + (t/is (= (:height data) (:height result))) + (t/is (= (:mtype content) (:mtype result))) + + (t/is (string? (:path result))) + (t/is (string? (:uri result))) + (t/is (string? (:thumb-path result))) + (t/is (string? (:thumb-uri result)))))) + + (t/testing "import from collection" + (let [coll @(th/create-image-collection db/pool (:id prof) 1) + image-id (uuid/next) + + content {:name "sample.jpg" + :path "tests/uxbox/tests/_files/sample.jpg" + :mtype "image/jpeg" + :size 312043} + + data {::sm/type :upload-image + :id image-id + :profile-id (:id prof) + :collection-id (:id coll) + :name "testfile" + :content content} + out1 (th/try-on! (sm/handle data))] + + ;; (th/print-result! out1) + (t/is (nil? (:error out1))) + + (let [result (:result out1)] + (t/is (= image-id (:id result))) + (t/is (= "testfile" (:name result))) + (t/is (= "image/jpeg" (:mtype result))) + (t/is (= "image/webp" (:thumb-mtype result)))) + + (let [data2 {::sm/type :import-image-to-file + :image-id image-id + :file-id (:id file) + :profile-id (:id prof)} + out2 (th/try-on! (sm/handle data2))] + + ;; (th/print-result! out2) + (t/is (nil? (:error out2))) + + (let [result1 (:result out1) + result2 (:result out2)] + (t/is (not= (:path result2) + (:path result1))) + (t/is (not= (:thumb-path result2) + (:thumb-path result1))))))) + )) + + +;; TODO: delete file image diff --git a/backend/tests/uxbox/tests/test_services_icons.clj b/backend/tests/uxbox/tests/test_services_icons.clj new file mode 100644 index 0000000000..ca374d6efd --- /dev/null +++ b/backend/tests/uxbox/tests/test_services_icons.clj @@ -0,0 +1,163 @@ +(ns uxbox.tests.test-services-icons + (:require + [clojure.test :as t] + [promesa.core :as p] + [datoteka.core :as fs] + [clojure.java.io :as io] + [uxbox.db :as db] + [uxbox.core :refer [system]] + [uxbox.services.mutations :as sm] + [uxbox.services.queries :as sq] + [uxbox.util.storage :as ust] + [uxbox.util.uuid :as uuid] + [uxbox.tests.helpers :as th] + [vertx.core :as vc])) + +(t/use-fixtures :once th/state-init) +(t/use-fixtures :each th/database-reset) + +(t/deftest icon-collections-crud + (let [id (uuid/next) + profile @(th/create-profile db/pool 2)] + + (t/testing "create collection" + (let [data {::sm/type :create-icon-collection + :name "sample collection" + :profile-id (:id profile) + :id id} + out (th/try-on! (sm/handle data))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + + (let [result (:result out)] + (t/is (= (:id profile) (:profile-id result))) + (t/is (= (:name data) (:name result)))))) + + (t/testing "update collection" + (let [data {::sm/type :rename-icon-collection + :name "sample collection renamed" + :profile-id (:id profile) + :id id} + out (th/try-on! (sm/handle data))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + + (t/is (= id (get-in out [:result :id]))) + (t/is (= (:id profile) (get-in out [:result :profile-id]))) + (t/is (= (:name data) (get-in out [:result :name]))))) + + (t/testing "query collections" + (let [data {::sq/type :icon-collections + :profile-id (:id profile)} + 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 profile) (get-in out [:result 0 :profile-id]))) + (t/is (= id (get-in out [:result 0 :id]))))) + + (t/testing "delete collection" + (let [data {::sm/type :delete-icon-collection + :profile-id (:id profile) + :id id} + + out (th/try-on! (sm/handle data))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (nil? (:result out))))) + + (t/testing "query collections after delete" + (let [data {::sq/type :icon-collections + :profile-id (:id profile)} + out (th/try-on! (sq/handle data))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (= 0 (count (:result out)))))) + )) + +(t/deftest icons-crud + (let [profile @(th/create-profile db/pool 1) + coll @(th/create-icon-collection db/pool (:id profile) 1) + icon-id (uuid/next)] + + (t/testing "upload icon to collection" + (let [data {::sm/type :create-icon + :id icon-id + :profile-id (:id profile) + :collection-id (:id coll) + :name "testfile" + :content "" + :metadata {:width 100 + :height 100 + :view-box [0 0 100 100] + :mimetype "text/svg"}} + out (th/try-on! (sm/handle data))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + + (let [result (:result out)] + (t/is (= (:id data) (:id result))) + (t/is (= (:name data) (:name result))) + (t/is (= (:content data) (:content result)))))) + + (t/testing "list icons by collection" + (let [data {::sq/type :icons + :profile-id (:id profile) + :collection-id (:id coll)} + out (th/try-on! (sq/handle data))] + ;; (th/print-result! out) + + (t/is (= icon-id (get-in out [:result 0 :id]))) + (t/is (= "testfile" (get-in out [:result 0 :name]))))) + + (t/testing "single icon" + (let [data {::sq/type :icon + :profile-id (:id profile) + :id icon-id} + out (th/try-on! (sq/handle data))] + ;; (th/print-result! out) + + (t/is (= icon-id (get-in out [:result :id]))) + (t/is (= "testfile" (get-in out [:result :name]))))) + + (t/testing "delete icons" + (let [data {::sm/type :delete-icon + :profile-id (:id profile) + :id icon-id} + out (th/try-on! (sm/handle data))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (nil? (get-in out [:result]))))) + + (t/testing "query icon after delete" + (let [data {::sq/type :icon + :profile-id (:id profile) + :id icon-id} + out (th/try-on! (sq/handle data))] + + ;; (th/print-result! out) + (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))))) + + (t/testing "query icons after delete" + (let [data {::sq/type :icons + :profile-id (:id profile) + :collection-id (:id coll)} + out (th/try-on! (sq/handle data))] + ;; (th/print-result! out) + (let [result (:result out)] + (t/is (= 0 (count result)))))) + )) diff --git a/backend/tests/uxbox/tests/test_images.clj b/backend/tests/uxbox/tests/test_services_images.clj similarity index 63% rename from backend/tests/uxbox/tests/test_images.clj rename to backend/tests/uxbox/tests/test_services_images.clj index f465cc9845..576c90188e 100644 --- a/backend/tests/uxbox/tests/test_images.clj +++ b/backend/tests/uxbox/tests/test_services_images.clj @@ -1,4 +1,4 @@ -(ns uxbox.tests.test-images +(ns uxbox.tests.test-services-images (:require [clojure.test :as t] [promesa.core :as p] @@ -16,26 +16,28 @@ (t/use-fixtures :once th/state-init) (t/use-fixtures :each th/database-reset) -(t/deftest images-collections-crud - (let [id (uuid/next) - user @(th/create-user db/pool 2)] +(t/deftest image-collections-crud + (let [id (uuid/next) + profile @(th/create-profile db/pool 2)] (t/testing "create collection" - (let [data {::sm/type :create-images-collection + (let [data {::sm/type :create-image-collection :name "sample collection" - :user (:id user) + :profile-id (:id profile) :id id} out (th/try-on! (sm/handle data))] ;; (th/print-result! out) (t/is (nil? (:error out))) - (t/is (= (:id user) (get-in out [:result :user-id]))) - (t/is (= (:name data) (get-in out [:result :name]))))) + + (let [result (:result out)] + (t/is (= (:id profile) (:profile-id result))) + (t/is (= (:name data) (:name result)))))) (t/testing "update collection" - (let [data {::sm/type :rename-images-collection + (let [data {::sm/type :rename-image-collection :name "sample collection renamed" - :user (:id user) + :profile-id (:id profile) :id id} out (th/try-on! (sm/handle data))] @@ -43,35 +45,35 @@ (t/is (nil? (:error out))) (t/is (= id (get-in out [:result :id]))) - (t/is (= (:id user) (get-in out [:result :user-id]))) + (t/is (= (:id profile) (get-in out [:result :profile-id]))) (t/is (= (:name data) (get-in out [:result :name]))))) (t/testing "query collections" - (let [data {::sq/type :images-collections - :user (:id user)} + (let [data {::sq/type :image-collections + :profile-id (:id profile)} 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 user) (get-in out [:result 0 :user-id]))) + (t/is (= (:id profile) (get-in out [:result 0 :profile-id]))) (t/is (= id (get-in out [:result 0 :id]))))) (t/testing "delete collection" - (let [data {::sm/type :delete-images-collection - :user (:id user) + (let [data {::sm/type :delete-image-collection + :profile-id (:id profile) :id id} out (th/try-on! (sm/handle data))] ;; (th/print-result! out) (t/is (nil? (:error out))) - (t/is (= id (get-in out [:result :id]))))) + (t/is (nil? (:result out))))) (t/testing "query collections after delete" - (let [data {::sq/type :images-collections - :user (:id user)} + (let [data {::sq/type :image-collections + :profile-id (:id profile)} out (th/try-on! (sq/handle data))] ;; (th/print-result! out) @@ -80,8 +82,8 @@ )) (t/deftest images-crud - (let [user @(th/create-user db/pool 1) - coll @(th/create-images-collection db/pool (:id user) 1) + (let [profile @(th/create-profile db/pool 1) + coll @(th/create-image-collection db/pool (:id profile) 1) image-id (uuid/next)] (t/testing "upload image to collection" @@ -91,13 +93,11 @@ :size 312043} data {::sm/type :upload-image :id image-id - :user (:id user) + :profile-id (:id profile) :collection-id (:id coll) :name "testfile" :content content} out (th/try-on! (sm/handle data))] - ;; out (with-redefs [vc/*context* (vc/get-or-create-context system)] - ;; (th/try-on! (sm/handle data)))] ;; (th/print-result! out) (t/is (nil? (:error out))) @@ -114,10 +114,9 @@ (t/is (string? (get-in out [:result :uri]))) (t/is (string? (get-in out [:result :thumb-uri]))))) - (t/testing "list images by collection" - (let [data {::sq/type :images-by-collection - :user (:id user) + (let [data {::sq/type :images + :profile-id (:id profile) :collection-id (:id coll)} out (th/try-on! (sq/handle data))] ;; (th/print-result! out) @@ -134,9 +133,9 @@ (t/is (string? (get-in out [:result 0 :uri]))) (t/is (string? (get-in out [:result 0 :thumb-uri]))))) - (t/testing "get image by id" - (let [data {::sq/type :image-by-id - :user (:id user) + (t/testing "single image" + (let [data {::sq/type :image + :profile-id (:id profile) :id image-id} out (th/try-on! (sq/handle data))] ;; (th/print-result! out) @@ -152,7 +151,38 @@ (t/is (string? (get-in out [:result :thumb-path]))) (t/is (string? (get-in out [:result :uri]))) (t/is (string? (get-in out [:result :thumb-uri]))))) + + (t/testing "delete images" + (let [data {::sm/type :delete-image + :profile-id (:id profile) + :id image-id} + out (th/try-on! (sm/handle data))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (nil? (get-in out [:result]))))) + + (t/testing "query image after delete" + (let [data {::sq/type :image + :profile-id (:id profile) + :id image-id} + out (th/try-on! (sq/handle data))] + + ;; (th/print-result! out) + (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))))) + + (t/testing "query images after delete" + (let [data {::sq/type :images + :profile-id (:id profile) + :collection-id (:id coll)} + out (th/try-on! (sq/handle data))] + ;; (th/print-result! out) + (let [result (:result out)] + (t/is (= 0 (count result)))))) )) - -;; TODO: (soft) delete image - diff --git a/backend/tests/uxbox/tests/test_services_pages.clj b/backend/tests/uxbox/tests/test_services_pages.clj new file mode 100644 index 0000000000..17221f317b --- /dev/null +++ b/backend/tests/uxbox/tests/test_services_pages.clj @@ -0,0 +1,190 @@ +(ns uxbox.tests.test-services-pages + (:require + [clojure.spec.alpha :as s] + [clojure.test :as t] + [promesa.core :as p] + [uxbox.common.pages :as cp] + [uxbox.db :as db] + [uxbox.http :as http] + [uxbox.services.mutations :as sm] + [uxbox.services.queries :as sq] + [uxbox.util.uuid :as uuid] + [uxbox.tests.helpers :as th])) + +(t/use-fixtures :once th/state-init) +(t/use-fixtures :each th/database-reset) + +(t/deftest pages-crud + (let [prof @(th/create-profile db/pool 1) + team (:default-team prof) + proj (:default-project prof) + file @(th/create-file db/pool (:id prof) (:id proj) 1) + page-id (uuid/next)] + + (t/testing "create page" + (let [data {::sm/type :create-page + :data cp/default-page-data + :file-id (:id file) + :id page-id + :ordering 1 + :name "test page" + :profile-id (:id prof)} + out (th/try-on! (sm/handle data))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + + (let [result (:result out)] + (t/is (uuid? (:id result))) + (t/is (= (:id data) (:id result))) + (t/is (= (:name data) (:name result))) + (t/is (= (:data data) (:data result))) + (t/is (= 0 (:version result))) + (t/is (= 0 (:revn result)))))) + + (t/testing "query pages" + (let [data {::sq/type :pages + :file-id (:id file) + :profile-id (:id prof)} + out (th/try-on! (sq/handle data))] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + + (let [result (:result out)] + (t/is (vector? result)) + (t/is (= 1 (count result))) + (t/is (= page-id (get-in result [0 :id]))) + (t/is (= "test page" (get-in result [0 :name]))) + (t/is (:id file) (get-in result [0 :file-id]))))) + + (t/testing "delete page" + (let [data {::sm/type :delete-page + :id page-id + :profile-id (:id prof)} + out (th/try-on! (sm/handle data))] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (nil? (:result out))))) + + (t/testing "query pages after delete" + (let [data {::sq/type :pages + :file-id (:id file) + :profile-id (:id prof)} + out (th/try-on! (sq/handle data))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + + (let [result (:result out)] + (t/is (vector? result)) + (t/is (= 0 (count result)))))) + )) + +(t/deftest update-page-data + (let [prof @(th/create-profile db/pool 1) + team (:default-team prof) + proj (:default-project prof) + file @(th/create-file db/pool (:id prof) (:id proj) 1) + page-id (uuid/next)] + + (t/testing "create empty page" + (let [data {::sm/type :create-page + :data cp/default-page-data + :file-id (:id file) + :id page-id + :ordering 1 + :name "test page" + :profile-id (:id prof)} + out (th/try-on! (sm/handle data))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + + (let [result (:result out)] + (t/is (uuid? (:id result))) + (t/is (= (:id data) (:id result)))))) + + + (t/testing "successfully update data" + (let [sid (uuid/next) + data {::sm/type :update-page + :id page-id + :revn 0 + :profile-id (:id prof) + :changes [{:type :add-shape + :id sid + :session-id (uuid/next) + :shape {:id sid + :name "Rect" + :type :rect}}]} + + out (th/try-on! (sm/handle data))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + + (let [result (:result out)] + (t/is (= 1 (:revn result))) + (t/is (= (:id data) (:page-id result))) + (t/is (vector (:changes result))) + (t/is (= 1 (count (:changes result)))) + (t/is (= :add-shape (get-in result [:changes 0 :type])))))) + + (t/testing "conflict error" + (let [data {::sm/type :update-page + :id page-id + :revn 99 + :profile-id (:id prof) + :changes []} + out (th/try-on! (sm/handle data))] + + ;; (th/print-result! out) + (let [error (:error out) + error-data (ex-data error)] + (t/is (th/ex-info? error)) + (t/is (= (:type error-data) :service-error)) + (t/is (= (:name error-data) :uxbox.services.mutations.pages/update-page))) + + (let [error (ex-cause (:error out)) + error-data (ex-data error)] + (t/is (th/ex-info? error)) + (t/is (= (:type error-data) :validation)) + (t/is (= (:code error-data) :revn-conflict))))) + )) + + +(t/deftest update-page-data-2 + (let [prof @(th/create-profile db/pool 1) + team (:default-team prof) + proj (:default-project prof) + file @(th/create-file db/pool (:id prof) (:id proj) 1) + page @(th/create-page db/pool (:id prof) (:id file) 1)] + (t/testing "lagging changes" + (let [sid (uuid/next) + data {::sm/type :update-page + :id (:id page) + :revn 0 + :profile-id (:id prof) + :changes [{:type :add-shape + :id sid + :session-id (uuid/next) + :shape {:id sid + :name "Rect" + :type :rect}}]} + out1 (th/try-on! (sm/handle data)) + out2 (th/try-on! (sm/handle data))] + + ;; (th/print-result! out1) + ;; (th/print-result! out2) + + (t/is (nil? (:error out1))) + (t/is (nil? (:error out2))) + + (t/is (= 1 (count (get-in out1 [:result :changes])))) + (t/is (= 2 (count (get-in out2 [:result :changes])))) + + (t/is (= (:id data) (get-in out1 [:result :page-id]))) + (t/is (= (:id data) (get-in out2 [:result :page-id]))))) + )) + + diff --git a/backend/tests/uxbox/tests/test_services_profile.clj b/backend/tests/uxbox/tests/test_services_profile.clj index 85ccfaf06b..8d5cd13841 100644 --- a/backend/tests/uxbox/tests/test_services_profile.clj +++ b/backend/tests/uxbox/tests/test_services_profile.clj @@ -11,6 +11,7 @@ (:require [clojure.test :as t] [clojure.java.io :as io] + [mockery.core :refer [with-mocks]] [promesa.core :as p] [cuerdas.core :as str] [datoteka.core :as fs] @@ -22,130 +23,175 @@ (t/use-fixtures :once th/state-init) (t/use-fixtures :each th/database-reset) -(t/deftest login-with-failed-auth - (let [user @(th/create-user db/pool 1) - event {::sm/type :login - :email "user1.test@nodomain.com" - :password "foobar" - :scope "foobar"} - out (th/try-on! (sm/handle event))] +(t/deftest profile-login + (let [profile @(th/create-profile db/pool 1)] + (t/testing "failed" + (let [event {::sm/type :login + :email "profile1.test@nodomain.com" + :password "foobar" + :scope "foobar"} + out (th/try-on! (sm/handle event))] - ;; (th/print-result! out) - (let [error (:error out)] - (t/is (th/ex-info? error)) - (t/is (th/ex-of-type? error :service-error))) + ;; (th/print-result! out) + (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.profile/wrong-credentials))))) + (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.profile/wrong-credentials))))) -(t/deftest login-with-success-auth - (let [user @(th/create-user db/pool 1) - event {::sm/type :login - :email "user1.test@nodomain.com" - :password "123123" - :scope "foobar"} - out (th/try-on! (sm/handle event))] - ;; (th/print-result! out) - (t/is (nil? (:error out))) - (t/is (= (get-in out [:result :id]) (:id user))))) + (t/testing "success" + (let [event {::sm/type :login + :email "profile1.test@nodomain.com" + :password "123123" + :scope "foobar"} + out (th/try-on! (sm/handle event))] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (= (:id profile) (get-in out [:result :id]))))) + )) -(t/deftest query-profile - (let [user @(th/create-user db/pool 1) - data {::sq/type :profile - :user (:id user)} +(t/deftest profile-query-and-manipulation + (let [profile @(th/create-profile db/pool 1)] - 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.test@nodomain.com" (get-in out [:result :email]))) - (t/is (not (contains? (:result out) :password))))) + (t/testing "query profile" + (let [data {::sq/type :profile + :profile-id (:id profile)} + out (th/try-on! (sq/handle data))] -(t/deftest mutation-update-profile - (let [user @(th/create-user db/pool 1) - data (assoc user - ::sm/type :update-profile - :fullname "Full Name" - :username "user222" - :lang "en") - 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 (= (:email data) (get-in out [:result :email]))) - (t/is (= (:metadata data) (get-in out [:result :metadata]))) - (t/is (not (contains? (:result out) :password))))) + ;; (th/print-result! out) + (t/is (nil? (:error out))) -(t/deftest mutation-update-profile-photo - (let [user @(th/create-user db/pool 1) - data {::sm/type :update-profile-photo - :user (:id user) - :file {:name "sample.jpg" - :path "tests/uxbox/tests/_files/sample.jpg" - :size 123123 - :mtype "image/jpeg"}} - out (th/try-on! (sm/handle data))] + (let [result (:result out)] + (t/is (= "Profile 1" (:fullname result))) + (t/is (= "profile1.test@nodomain.com" (:email result))) + (t/is (not (contains? result :password)))))) - ;; (th/print-result! out) - (t/is (nil? (:error out))) - (t/is (= (:id user) (get-in out [:result :id]))))) + (t/testing "update profile" + (let [data (assoc profile + ::sm/type :update-profile + :fullname "Full Name" + :name "profile222" + :lang "en") + out (th/try-on! (sm/handle data))] -;; (t/deftest test-mutation-register-profile -;; (let[data {:fullname "Full Name" -;; :username "user222" -;; :email "user222@uxbox.io" -;; :password "user222" -;; ::sv/type :register-profile} -;; [err rsp] (th/try-on (sm/handle data))] -;; (println "RESPONSE:" err rsp))) + ;; (th/print-result! out) + (t/is (nil? (:error out))) -;; (t/deftest test-http-validate-recovery-token -;; (with-open [conn (db/connection)] -;; (let [user (th/create-user conn 1)] -;; (with-server {:handler (uft/routes)} -;; (let [token (#'usu/request-password-recovery conn "user1") -;; uri1 (str th/+base-url+ "/api/auth/recovery/not-existing") -;; uri2 (str th/+base-url+ "/api/auth/recovery/" token) -;; [status1 data1] (th/http-get user uri1) -;; [status2 data2] (th/http-get user uri2)] -;; ;; (println "RESPONSE:" status1 data1) -;; ;; (println "RESPONSE:" status2 data2) -;; (t/is (= 404 status1)) -;; (t/is (= 204 status2))))))) + (let [result (:result out)] + (t/is (= (:fullname data) (:fullname result))) + (t/is (= (:email data) (:email result))) + (t/is (not (contains? result :password)))))) -;; (t/deftest test-http-request-password-recovery -;; (with-open [conn (db/connection)] -;; (let [user (th/create-user conn 1) -;; sql "select * from user_pswd_recovery" -;; res (sc/fetch-one conn sql)] + (t/testing "update photo" + (let [data {::sm/type :update-profile-photo + :profile-id (:id profile) + :file {:name "sample.jpg" + :path "tests/uxbox/tests/_files/sample.jpg" + :size 123123 + :mtype "image/jpeg"}} + out (th/try-on! (sm/handle data))] -;; ;; Initially no tokens exists -;; (t/is (nil? res)) + ;; (th/print-result! out) + (t/is (nil? (:error out))) -;; (with-server {:handler (uft/routes)} -;; (let [uri (str th/+base-url+ "/api/auth/recovery") -;; data {:username "user1"} -;; [status data] (th/http-post user uri {:body data})] -;; ;; (println "RESPONSE:" status data) -;; (t/is (= 204 status))) + (let [result (:result out)] + (t/is (= (:id profile) (:id result)))))) + )) -;; (let [res (sc/fetch-one conn sql)] -;; (t/is (not (nil? res))) -;; (t/is (= (:user res) (:id user)))))))) +(t/deftest profile-deletion + (let [prof @(th/create-profile db/pool 1) + team (:default-team prof) + proj (:default-project prof) + file @(th/create-file db/pool (:id prof) (:id proj) 1) + page @(th/create-page db/pool (:id prof) (:id file) 1)] -;; (t/deftest test-http-validate-recovery-token -;; (with-open [conn (db/connection)] -;; (let [user (th/create-user conn 1)] -;; (with-server {:handler (uft/routes)} -;; (let [token (#'usu/request-password-recovery conn (:username user)) -;; uri (str th/+base-url+ "/api/auth/recovery") -;; data {:token token :password "mytestpassword"} -;; [status data] (th/http-put user uri {:body data}) + (t/testing "try to delete profile not marked for deletion" + (let [params {:props {:profile-id (:id prof)}} + out (th/try-on! (uxbox.tasks.delete-profile/handler params))] -;; user' (usu/find-full-user-by-id conn (:id user))] -;; (t/is (= status 204)) -;; (t/is (hashers/check "mytestpassword" (:password user')))))))) + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (nil? (:result out))))) + (t/testing "query profile after delete" + (let [data {::sq/type :profile + :profile-id (:id prof)} + out (th/try-on! (sq/handle data))] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + + (let [result (:result out)] + (t/is (= (:fullname prof) (:fullname result)))))) + + (t/testing "mark profile for deletion" + (with-mocks + [mock {:target 'uxbox.tasks/schedule! :return nil}] + + (let [data {::sm/type :delete-profile + :profile-id (:id prof)} + out (th/try-on! (sm/handle data))] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (nil? (:result out)))) + + ;; check the mock + (let [mock (deref mock) + mock-params (second (:call-args mock))] + (t/is (true? (:called? mock))) + (t/is (= 1 (:call-count mock))) + (t/is (= "delete-profile" (:name mock-params))) + (t/is (= (:id prof) (get-in mock-params [:props :profile-id])))))) + + (t/testing "query files after profile soft deletion" + (let [data {::sq/type :files + :project-id (:id proj) + :profile-id (:id prof)} + out (th/try-on! (sq/handle data))] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (= 1 (count (:result out)))))) + + (t/testing "try to delete profile marked for deletion" + (let [params {:props {:profile-id (:id prof)}} + out (th/try-on! (uxbox.tasks.delete-profile/handler params))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (= (:id prof) (:result out))))) + + (t/testing "query profile after delete" + (let [data {::sq/type :profile + :profile-id (:id prof)} + out (th/try-on! (sq/handle data))] + + ;; (th/print-result! out) + + (let [error (:error out) + error-data (ex-data error)] + (t/is (th/ex-info? error)) + (t/is (= (:type error-data) :service-error)) + (t/is (= (:name error-data) :uxbox.services.queries.profile/profile))) + + (let [error (ex-cause (:error out)) + error-data (ex-data error)] + (t/is (th/ex-info? error)) + (t/is (= (:type error-data) :not-found))))) + + (t/testing "query files after profile permanent deletion" + (let [data {::sq/type :files + :project-id (:id proj) + :profile-id (:id prof)} + out (th/try-on! (sq/handle data))] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (= 0 (count (:result out)))))) + )) + +;; TODO: profile deletion with teams +;; TODO: profile deletion with owner teams +;; TODO: profile registration +;; TODO: profile password recovery diff --git a/backend/tests/uxbox/tests/test_services_project_files.clj b/backend/tests/uxbox/tests/test_services_project_files.clj deleted file mode 100644 index ccabdecb6e..0000000000 --- a/backend/tests/uxbox/tests/test_services_project_files.clj +++ /dev/null @@ -1,156 +0,0 @@ -(ns uxbox.tests.test-services-project-files - (:require - [clojure.test :as t] - [promesa.core :as p] - [datoteka.core :as fs] - [uxbox.db :as db] - [uxbox.media :as media] - [uxbox.core :refer [system]] - [uxbox.http :as http] - [uxbox.services.mutations :as sm] - [uxbox.services.queries :as sq] - [uxbox.tests.helpers :as th] - [uxbox.util.storage :as ust] - [uxbox.util.uuid :as uuid] - [vertx.util :as vu])) - -(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-rename-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 :rename-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))))) - -(t/deftest mutation-upload-file-image - (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) - - content {:name "sample.jpg" - :path "tests/uxbox/tests/_files/sample.jpg" - :mtype "image/jpeg" - :size 312043} - data {::sm/type :upload-project-file-image - :user (:id user) - :file-id (:id pf) - :name "testfile" - :content content - :width 800 - :height 800} - - out (th/try-on! (sm/handle data))] - - ;; (th/print-result! out) - - (t/is (= (:id pf) (get-in out [:result :file-id]))) - (t/is (= (:name data) (get-in out [:result :name]))) - (t/is (= (:width data) (get-in out [:result :width]))) - (t/is (= (:height data) (get-in out [:result :height]))) - (t/is (= (:mimetype data) (get-in out [:result :mimetype]))) - - (t/is (string? (get-in out [:result :path]))) - (t/is (string? (get-in out [:result :thumb-path]))) - (t/is (string? (get-in out [:result :uri]))) - (t/is (string? (get-in out [:result :thumb-uri]))) - )) - -(t/deftest mutation-import-image-file-from-collection - (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) - coll @(th/create-images-collection db/pool (:id user) 1) - image-id (uuid/next) - - content {:name "sample.jpg" - :path "tests/uxbox/tests/_files/sample.jpg" - :mtype "image/jpeg" - :size 312043} - - data {::sm/type :upload-image - :id image-id - :user (:id user) - :collection-id (:id coll) - :name "testfile" - :content content} - out1 (th/try-on! (sm/handle data))] - - ;; (th/print-result! out1) - (t/is (nil? (:error out1))) - (t/is (= image-id (get-in out1 [:result :id]))) - (t/is (= "testfile" (get-in out1 [:result :name]))) - (t/is (= "image/jpeg" (get-in out1 [:result :mtype]))) - (t/is (= "image/webp" (get-in out1 [:result :thumb-mtype]))) - - (let [data2 {::sm/type :import-image-to-file - :image-id image-id - :file-id (:id pf) - :user (:id user)} - out2 (th/try-on! (sm/handle data2))] - - ;; (th/print-result! out2) - (t/is (nil? (:error out2))) - (t/is (not= (get-in out2 [:result :path]) - (get-in out1 [:result :path]))) - (t/is (not= (get-in out2 [:result :thumb-path]) - (get-in out1 [:result :thumb-path])))))) - - - diff --git a/backend/tests/uxbox/tests/test_services_project_pages.clj b/backend/tests/uxbox/tests/test_services_project_pages.clj deleted file mode 100644 index cc0e3c2d93..0000000000 --- a/backend/tests/uxbox/tests/test_services_project_pages.clj +++ /dev/null @@ -1,177 +0,0 @@ -(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.util.uuid :as uuid] - [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 {:canvas [] - :options {} - :shapes [] - :shapes-by-id {}} - :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 (= 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 [(uuid/next)] - :options {} - :canvas [] - :shapes-by-id {}} - :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-update-project-page-1 - (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 - :id (:id page) - :version 99 - :user (:id user) - :changes []} - - out (th/try-on! (sm/handle data))] - - ;; (th/print-result! out) - - (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 :version-conflict))))) - -(t/deftest mutation-update-project-page-2 - (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) - - sid (uuid/next) - data {::sm/type :update-project-page - :id (:id page) - :version 0 - :user (:id user) - :changes [{:type :add-shape - :id sid - :session-id (uuid/next) - :shape {:id sid - :name "Rect" - :type :rect}}]} - - out (th/try-on! (sm/handle data))] - - ;; (th/print-result! out) - (t/is (nil? (:error out))) - - (t/is (= 1 (get-in out [:result :version]))) - (t/is (= (:id page) (get-in out [:result :page-id]))) - (t/is (= :add-shape (get-in out [:result :changes 0 :type]))) - )) - -(t/deftest mutation-update-project-page-3 - (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) - - sid (uuid/next) - - data {::sm/type :update-project-page - :id (:id page) - :version 0 - :user (:id user) - :changes [{:type :add-shape - :id sid - :session-id (uuid/next) - :shape {:id sid - :name "Rect" - :type :rect}}]} - - out1 (th/try-on! (sm/handle data)) - out2 (th/try-on! (sm/handle data))] - - ;; (th/print-result! out1) - ;; (th/print-result! out2) - - (t/is (nil? (:error out1))) - (t/is (nil? (:error out2))) - - (t/is (= 1 (count (get-in out1 [:result :changes])))) - (t/is (= 2 (count (get-in out2 [:result :changes])))) - - (t/is (= (:id data) (get-in out1 [:result :page-id]))) - (t/is (= (:id data) (get-in out2 [:result :page-id]))) - )) - -(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/tests/uxbox/tests/test_services_projects.clj b/backend/tests/uxbox/tests/test_services_projects.clj index 365defa99a..5080c8ebda 100644 --- a/backend/tests/uxbox/tests/test_services_projects.clj +++ b/backend/tests/uxbox/tests/test_services_projects.clj @@ -6,60 +6,75 @@ [uxbox.http :as http] [uxbox.services.mutations :as sm] [uxbox.services.queries :as sq] - [uxbox.tests.helpers :as th])) + [uxbox.tests.helpers :as th] + [uxbox.util.uuid :as uuid])) (t/use-fixtures :once th/state-init) (t/use-fixtures :each th/database-reset) -(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]))) - (t/is (= (:name proj) (get-in out [:result 0 :name]))))) +(t/deftest projects-crud + (let [prof @(th/create-profile db/pool 1) + team @(th/create-team db/pool (:id prof) 1) + project-id (uuid/next)] -(t/deftest mutation-create-project - (let [user @(th/create-user db/pool 1) - data {::sm/type :create-project - :user (:id user) - :name "test project"} - 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/testing "create a project" + (let [data {::sm/type :create-project + :id project-id + :profile-id (:id prof) + :team-id (:id team) + :name "test project"} + out (th/try-on! (sm/handle data))] + ;; (th/print-result! out) -(t/deftest mutation-rename-project - (let [user @(th/create-user db/pool 1) - proj @(th/create-project db/pool (:id user) 1) - data {::sm/type :rename-project - :id (:id proj) - :name "test project mod" - :user (:id user)} - 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/is (nil? (:error out))) + (let [result (:result out)] + (t/is (= (:name data) (:name result)))))) -(t/deftest mutation-delete-project - (let [user @(th/create-user db/pool 1) - proj @(th/create-project db/pool (:id user) 1) - data {::sm/type :delete-project - :id (:id proj) - :user (:id user)} - out (th/try-on! (sm/handle data))] - ;; (th/print-result! out) - (t/is (nil? (:error out))) - (t/is (nil? (:result out))) + (t/testing "query a list of projects" + (let [data {::sq/type :projects-by-team + :team-id (:id team) + :profile-id (:id prof)} + out (th/try-on! (sq/handle data))] + ;; (th/print-result! out) - (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))))) + (t/is (nil? (:error out))) + (let [result (:result out)] + (t/is (= 1 (count result))) + (t/is project-id (get-in result [0 :id])) + (t/is "test project" (get-in result [0 :name]))))) -;; TODO: add permisions related tests + (t/testing "rename project" + (let [data {::sm/type :rename-project + :id project-id + :name "renamed project" + :profile-id (:id prof)} + out (th/try-on! (sm/handle data))] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + + (let [result (:result out)] + (t/is (= (:id data) (:id result))) + (t/is (= (:name data) (:name result))) + (t/is (= (:profile-id data) (:id prof)))))) + + (t/testing "delete project" + (let [data {::sm/type :delete-project + :id project-id + :profile-id (:id prof)} + out (th/try-on! (sm/handle data))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (nil? (:result out))))) + + (t/testing "query a list of projects after delete" + (let [data {::sq/type :projects-by-team + :team-id (:id team) + :profile-id (:id prof)} + out (th/try-on! (sq/handle data))] + ;; (th/print-result! out) + + (t/is (nil? (:error out))) + (let [result (:result out)] + (t/is (= 1 (count result)))))) + )) diff --git a/backend/tests/uxbox/tests/test_services_user_attrs.clj b/backend/tests/uxbox/tests/test_services_user_attrs.clj index db9826d9e9..6b4e4611ae 100644 --- a/backend/tests/uxbox/tests/test_services_user_attrs.clj +++ b/backend/tests/uxbox/tests/test_services_user_attrs.clj @@ -9,57 +9,57 @@ [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/use-fixtures :once th/state-init) +;; (t/use-fixtures :each th/database-reset) -(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-attr - :key "foobar" - :user id}))] - (t/is (nil? (:result out))) +;; (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-attr +;; :key "foobar" +;; :user id}))] +;; (t/is (nil? (:result out))) - (let [error (:error out)] - (t/is (th/ex-info? error)) - (t/is (th/ex-of-type? error :service-error))) +;; (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 [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}}}))] - ;; (th/print-result! out) - (t/is (nil? (:error out))) - (t/is (nil? (:result out)))) +;; (let [out (th/try-on! (sm/handle {::sm/type :upsert-user-attr +;; :user id +;; :key "foobar" +;; :val {:some #{:value}}}))] +;; ;; (th/print-result! out) +;; (t/is (nil? (:error out))) +;; (t/is (nil? (:result out)))) - (let [out (th/try-on! (sq/handle {::sq/type :user-attr - :key "foobar" - :user id}))] - ;; (th/print-result! out) - (t/is (nil? (:error out))) - (t/is (= {:some #{:value}} (get-in out [:result :val]))) - (t/is (= "foobar" (get-in out [:result :key])))) +;; (let [out (th/try-on! (sq/handle {::sq/type :user-attr +;; :key "foobar" +;; :user id}))] +;; ;; (th/print-result! out) +;; (t/is (nil? (:error out))) +;; (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-attr - :user id - :key "foobar"}))] - ;; (th/print-result! out) - (t/is (nil? (:error out))) - (t/is (nil? (:result out)))) +;; (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-attr - :key "foobar" - :user id}))] - ;; (th/print-result! out) - (t/is (nil? (:result out))) - (let [error (:error out)] - (t/is (th/ex-info? error)) - (t/is (th/ex-of-type? error :service-error))) +;; (let [out (th/try-on! (sq/handle {::sq/type :user-attr +;; :key "foobar" +;; :user id}))] +;; ;; (th/print-result! out) +;; (t/is (nil? (:result out))) +;; (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 [error (ex-cause (:error out))] +;; (t/is (th/ex-info? error)) +;; (t/is (th/ex-of-type? error :not-found)))))) diff --git a/frontend/src/uxbox/main.cljs b/frontend/src/uxbox/main.cljs index e5d87650cb..020119dd23 100644 --- a/frontend/src/uxbox/main.cljs +++ b/frontend/src/uxbox/main.cljs @@ -58,7 +58,7 @@ (st/emit! #(assoc % :router router)) (add-watch html-history/path ::main #(on-navigate router %4)) - (when (:auth storage) + (when (:profile storage) (st/emit! udu/fetch-profile)) (mf/mount (mf/element ui/app) (dom/get-element "app")) diff --git a/frontend/src/uxbox/main/data/auth.cljs b/frontend/src/uxbox/main/data/auth.cljs index 06fd1a4bba..4ef0371da9 100644 --- a/frontend/src/uxbox/main/data/auth.cljs +++ b/frontend/src/uxbox/main/data/auth.cljs @@ -30,14 +30,9 @@ (defn logged-in [data] (ptk/reify ::logged-in - ptk/UpdateEvent - (update [this state] - (assoc state :auth data)) - ptk/WatchEvent - (watch [this state s] - (swap! storage assoc :auth data) - (rx/of du/fetch-profile + (watch [this state stream] + (rx/of (du/profile-fetched data) (rt/navigate :dashboard-projects))))) ;; --- Login diff --git a/frontend/src/uxbox/main/data/dashboard.cljs b/frontend/src/uxbox/main/data/dashboard.cljs index ccd0ee4d33..18ec7c9dcf 100644 --- a/frontend/src/uxbox/main/data/dashboard.cljs +++ b/frontend/src/uxbox/main/data/dashboard.cljs @@ -5,49 +5,222 @@ ;; Copyright (c) 2015-2016 Andrey Antukh (ns uxbox.main.data.dashboard - (:require [beicon.core :as rx] - [uxbox.util.uuid :as uuid] - [potok.core :as ptk] - [uxbox.util.router :as r] - [uxbox.main.store :as st] - [uxbox.main.repo :as rp] - [uxbox.main.data.projects :as dp] - [uxbox.main.data.colors :as dc] - [uxbox.main.data.images :as di] - [uxbox.util.data :refer (deep-merge)])) + (:require + [beicon.core :as rx] + [cljs.spec.alpha :as s] + [cuerdas.core :as str] + [potok.core :as ptk] + [uxbox.common.data :as d] + [uxbox.common.pages :as cp] + [uxbox.common.spec :as us] + [uxbox.main.repo :as rp] + [uxbox.util.router :as rt] + [uxbox.util.time :as dt] + [uxbox.util.timers :as ts] + [uxbox.util.uuid :as uuid])) -;; --- Events +;; --- Specs -(defrecord InitializeDashboard [section] - ptk/UpdateEvent - (update [_ state] - (update state :dashboard assoc - :section section - :collection-type :builtin - :collection-id 1))) +(s/def ::id ::us/uuid) +(s/def ::name string?) +(s/def ::team-id ::us/uuid) +(s/def ::profile-id ::us/uuid) +(s/def ::project-id ::us/uuid) +(s/def ::created-at ::us/inst) +(s/def ::modified-at ::us/inst) -(defn initialize - [section] - (InitializeDashboard. section)) +(s/def ::team + (s/keys :req-un [::id + ::name + ::created-at + ::modified-at])) -(defn set-collection-type - [type] - {:pre [(contains? #{:builtin :own} type)]} - (letfn [(select-first [state] - (if (= type :builtin) - (assoc-in state [:dashboard :collection-id] 1) - (let [colls (sort-by :id (vals (:colors-by-id state)))] - (assoc-in state [:dashboard :collection-id] (:id (first colls))))))] - (reify - ptk/UpdateEvent - (update [_ state] - (as-> state $ - (assoc-in $ [:dashboard :collection-type] type) - (select-first $)))))) +(s/def ::project + (s/keys ::req-un [::id + ::name + ::team-id + ::version + ::profile-id + ::created-at + ::modified-at])) -(defn set-collection - [id] - (reify +(s/def ::file + (s/keys :req-un [::id + ::name + ::created-at + ::modified-at + ::project-id])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Initialization +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(declare fetch-files) + +(def initialize-drafts + (ptk/reify ::initialize ptk/UpdateEvent (update [_ state] - (assoc-in state [:dashboard :collection-id] id)))) + (let [profile (:profile state)] + (update state :dashboard-local assoc + :team-id (:default-team-id profile) + :project-id (:default-project-id profile)))) + + ptk/WatchEvent + (watch [_ state stream] + (let [local (:dashboard-local state)] + (rx/of (fetch-files (:project-id local))))))) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Data Fetching +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; --- Fetch Projects + +(declare projects-fetched) + +(def fetch-projects + (ptk/reify ::fetch-projects + ptk/WatchEvent + (watch [_ state stream] + (->> (rp/query :projects) + (rx/map projects-fetched))))) + +(defn projects-fetched + [projects] + (us/verify (s/every ::project) projects) + (ptk/reify ::projects-fetched + ptk/UpdateEvent + (update [_ state] + (let [assoc-project #(update-in %1 [:projects (:id %2)] merge %2)] + (reduce assoc-project state projects))))) + +;; --- Fetch Files + +(declare files-fetched) + +(defn fetch-files + [project-id] + (ptk/reify ::fetch-files + ptk/WatchEvent + (watch [_ state stream] + (let [params {:project-id project-id}] + (->> (rp/query :files params) + (rx/map files-fetched)))))) + +(defn files-fetched + [files] + (us/verify (s/every ::file) files) + (ptk/reify ::files-fetched + ptk/UpdateEvent + (update [_ state] + (let [state (dissoc state :files) + files (d/index-by :id files)] + (assoc state :files files))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Data Modification +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; --- Rename Project + +(defn rename-project + [id name] + {:pre [(uuid? id) (string? name)]} + (ptk/reify ::rename-project + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:projects id :name] name)) + + ptk/WatchEvent + (watch [_ state stream] + (let [params {:id id :name name}] + (->> (rp/mutation :rename-project params) + (rx/ignore)))))) + +;; --- Delete Project (by id) + +(defn delete-project + [id] + (us/verify ::us/uuid id) + (ptk/reify ::delete-project + ptk/UpdateEvent + (update [_ state] + (update state :projects dissoc id)) + + ptk/WatchEvent + (watch [_ state s] + (->> (rp/mutation :delete-project {:id id}) + (rx/ignore))))) + +;; --- Delete File (by id) + +(defn delete-file + [id] + (us/verify ::us/uuid id) + (ptk/reify ::delete-file + ptk/UpdateEvent + (update [_ state] + (update state :files dissoc id)) + + ptk/WatchEvent + (watch [_ state s] + (->> (rp/mutation :delete-file {:id id}) + (rx/ignore))))) + +;; --- Rename Project + +(defn rename-file + [id name] + {:pre [(uuid? id) (string? name)]} + (ptk/reify ::rename-file + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:files id :name] name)) + + ptk/WatchEvent + (watch [_ state stream] + (let [params {:id id :name name}] + (->> (rp/mutation :rename-file params) + (rx/ignore)))))) + + +;; --- Create File + +(declare file-created) + +(def create-file + (ptk/reify ::create-draft-file + ptk/WatchEvent + (watch [_ state stream] + (let [name (str "New File " (gensym "p")) + project-id (get-in state [:dashboard-local :project-id]) + params {:name name :project-id project-id}] + (->> (rp/mutation! :create-file params) + (rx/map file-created)))))) + +(defn file-created + [data] + (us/verify ::file data) + (ptk/reify ::create-draft-file + ptk/UpdateEvent + (update [this state] + (update state :files assoc (:id data) data)))) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; UI State Handling +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; --- Update Opts (Filtering & Ordering) + +(defn update-opts + [& {:keys [order filter] :as opts}] + (ptk/reify ::update-opts + ptk/UpdateEvent + (update [_ state] + (update state :dashboard-local merge + (when order {:order order}) + (when filter {:filter filter}))))) + diff --git a/frontend/src/uxbox/main/data/images.cljs b/frontend/src/uxbox/main/data/images.cljs index 1c6e6280ee..99f2ac642f 100644 --- a/frontend/src/uxbox/main/data/images.cljs +++ b/frontend/src/uxbox/main/data/images.cljs @@ -79,7 +79,7 @@ (ptk/reify ::fetch-collections ptk/WatchEvent (watch [_ state s] - (->> (rp/query! :images-collections) + (->> (rp/query! :image-collections) (rx/map collections-fetched))))) @@ -108,7 +108,7 @@ ptk/WatchEvent (watch [_ state s] (let [data {:name (tr "ds.default-library-title" (gensym "c"))}] - (->> (rp/mutation! :create-images-collection data) + (->> (rp/mutation! :create-image-collection data) (rx/map collection-created)))))) ;; --- Collection Created @@ -134,7 +134,7 @@ ptk/WatchEvent (watch [_ state s] (let [params {:id id :name name}] - (->> (rp/mutation! :rename-images-collection params) + (->> (rp/mutation! :rename-image-collection params) (rx/ignore)))))) ;; --- Delete Collection @@ -148,7 +148,7 @@ ptk/WatchEvent (watch [_ state s] - (->> (rp/mutation! :delete-images-collection {:id id}) + (->> (rp/mutation! :delete-image-collection {:id id}) (rx/tap on-success) (rx/ignore))))) diff --git a/frontend/src/uxbox/main/data/projects.cljs b/frontend/src/uxbox/main/data/projects.cljs index ba3cf265ed..d02f2d94fb 100644 --- a/frontend/src/uxbox/main/data/projects.cljs +++ b/frontend/src/uxbox/main/data/projects.cljs @@ -4,6 +4,11 @@ ;; ;; Copyright (c) 2015-2017 Andrey Antukh +;; NOTE: this namespace is deprecated and will be removed when new +;; dashboard is implemented. Is just maintained as a temporal solution +;; for have the old dashboard code "working". + + (ns uxbox.main.data.projects (:require [beicon.core :as rx] @@ -22,22 +27,16 @@ (s/def ::id ::us/uuid) (s/def ::name string?) -(s/def ::user ::us/uuid) -(s/def ::type keyword?) -(s/def ::file-id ::us/uuid) -(s/def ::project-id ::us/uuid) +(s/def ::profile-id ::us/uuid) +(s/def ::project-id (s/nilable ::us/uuid)) (s/def ::created-at ::us/inst) (s/def ::modified-at ::us/inst) -(s/def ::version ::us/number) -(s/def ::ordering ::us/number) -(s/def ::metadata (s/nilable ::cp/metadata)) -(s/def ::data ::cp/data) (s/def ::project (s/keys ::req-un [::id ::name ::version - ::user-id + ::profile-id ::created-at ::modified-at])) @@ -48,43 +47,17 @@ ::modified-at ::project-id])) -(s/def ::page - (s/keys :req-un [::id - ::name - ::file-id - ::version - ::created-at - ::modified-at - ::user-id - ::ordering - ::data])) - -;; --- Helpers - -(defn unpack-page - [state {:keys [id data] :as page}] - (-> state - (update :pages assoc id (dissoc page :data)) - (update :pages-data assoc id data))) - -(defn purge-page - "Remove page and all related stuff from the state." - [state id] - (if-let [file-id (get-in state [:pages id :file-id])] - (-> state - (update-in [:files file-id :pages] #(filterv (partial not= id) %)) - (update-in [:workspace-file :pages] #(filterv (partial not= id) %)) - (update :pages dissoc id) - (update :pages-data dissoc id)) - state)) ;; --- Initialize Dashboard (declare fetch-projects) - (declare fetch-files) +(declare fetch-draft-files) (declare initialized) +;; NOTE/WARN: this need to be refactored completly when new UI is +;; prototyped. + (defn initialize [id] (ptk/reify ::initialize @@ -95,7 +68,9 @@ ptk/WatchEvent (watch [_ state stream] (rx/merge - (rx/of (fetch-files id)) + (if (nil? id) + (rx/of fetch-draft-files) + (rx/of (fetch-files id))) (->> stream (rx/filter (ptk/type? ::files-fetched)) (rx/take 1) @@ -121,6 +96,7 @@ (when filter {:filter filter}))))) ;; --- Fetch Projects + (declare projects-fetched) (def fetch-projects @@ -151,9 +127,16 @@ ptk/WatchEvent (watch [_ state stream] (let [params (if (nil? project-id) {} {:project-id project-id})] - (->> (rp/query :project-files params) + (->> (rp/query :files params) (rx/map files-fetched)))))) +(def fetch-draft-files + (ptk/reify ::fetch-draft-files + ptk/WatchEvent + (watch [_ state stream] + (->> (rp/query :draft-files {}) + (rx/map files-fetched))))) + ;; --- Fetch File (by ID) (defn fetch-file @@ -162,23 +145,11 @@ (ptk/reify ::fetch-file ptk/WatchEvent (watch [_ state stream] - (->> (rp/query :project-file {:id id}) + (->> (rp/query :file {:id id}) (rx/map #(files-fetched [%])))))) ;; --- Files Fetched -(defn files-fetched - [files] - (us/verify (s/every ::file) files) - (ptk/reify ::files-fetched - cljs.core/IDeref - (-deref [_] files) - - ptk/UpdateEvent - (update [_ state] - (let [assoc-file #(assoc-in %1 [:files (:id %2)] %2)] - (reduce assoc-file state files))))) - ;; --- Create Project (declare project-created) @@ -201,12 +172,31 @@ (watch [this state stream] (let [name (str "New File " (gensym "p")) params {:name name :project-id project-id}] - (->> (rp/mutation! :create-project-file params) + (->> (rp/mutation! :create-file params) (rx/mapcat (fn [data] (rx/of (files-fetched [data]) #(update-in % [:dashboard-projects :files project-id] conj (:id data)))))))))) +(declare file-created) + +(def create-draft-file + (ptk/reify ::create-draft-file + ptk/WatchEvent + (watch [this state stream] + (let [name (str "New File " (gensym "p")) + params {:name name}] + (->> (rp/mutation! :create-draft-file params) + (rx/map file-created)))))) + +(defn file-created + [data] + (us/verify ::file data) + (ptk/reify ::create-draft-file + ptk/UpdateEvent + (update [this state] + (update state :files assoc (:id data) data)))) + ;; --- Rename Project (defn rename-project @@ -250,7 +240,7 @@ ptk/WatchEvent (watch [_ state s] - (->> (rp/mutation :delete-project-file {:id id}) + (->> (rp/mutation :delete-file {:id id}) (rx/ignore))))) ;; --- Rename Project @@ -266,7 +256,7 @@ ptk/WatchEvent (watch [_ state stream] (let [params {:id id :name name}] - (->> (rp/mutation :rename-project-file params) + (->> (rp/mutation :rename-file params) (rx/ignore)))))) ;; --- Go To Project @@ -291,178 +281,3 @@ (if (nil? id) (rx/of (rt/nav :dashboard-projects {} {})) (rx/of (rt/nav :dashboard-projects {} {:project-id (str id)})))))) - - -;; --- Fetch Pages (by File ID) - -(declare pages-fetched) - -(defn fetch-pages - [file-id] - (us/verify ::us/uuid file-id) - (reify - ptk/WatchEvent - (watch [_ state s] - (->> (rp/query :project-pages {:file-id file-id}) - (rx/map pages-fetched))))) - -;; --- Pages Fetched - -(defn pages-fetched - [pages] - (us/verify (s/every ::page) pages) - (ptk/reify ::pages-fetched - IDeref - (-deref [_] pages) - - ptk/UpdateEvent - (update [_ state] - (reduce unpack-page state pages)))) - -;; --- Fetch Page (By ID) - -(declare page-fetched) - -(defn fetch-page - "Fetch page by id." - [id] - (us/verify ::us/uuid id) - (reify - ptk/WatchEvent - (watch [_ state s] - (->> (rp/query :project-page {:id id}) - (rx/map page-fetched))))) - -;; --- Page Fetched - -(defn page-fetched - [data] - (us/verify ::page data) - (ptk/reify ::page-fetched - IDeref - (-deref [_] data) - - ptk/UpdateEvent - (update [_ state] - (unpack-page state data)))) - -;; --- Create Page - -(declare page-created) - -(def create-empty-page - (ptk/reify ::create-empty-page - ptk/WatchEvent - (watch [this state stream] - (let [file-id (get-in state [:workspace-page :file-id]) - name (str "Page " (gensym "p")) - ordering (count (get-in state [:files file-id :pages])) - params {:name name - :file-id file-id - :ordering ordering - :data cp/default-page-data}] - (->> (rp/mutation :create-project-page params) - (rx/map page-created)))))) - -;; --- Page Created - -(defn page-created - [{:keys [id file-id] :as page}] - (us/verify ::page page) - (ptk/reify ::page-created - cljs.core/IDeref - (-deref [_] page) - - ptk/UpdateEvent - (update [_ state] - (let [data (:data page) - page (dissoc page :data)] - (-> state - (update-in [:workspace-file :pages] (fnil conj []) id) - (update :pages assoc id page) - (update :pages-data assoc id data)))) - - ptk/WatchEvent - (watch [_ state stream] - (rx/of (fetch-file file-id))))) - -;; --- Rename Page - -(s/def ::rename-page - (s/keys :req-un [::id ::name])) - -(defn rename-page - [id name] - (us/verify ::us/uuid id) - (us/verify string? name) - (ptk/reify ::rename-page - ptk/UpdateEvent - (update [_ state] - (let [pid (get-in state [:workspace-page :id]) - state (assoc-in state [:pages id :name] name)] - (cond-> state - (= pid id) (assoc-in [:workspace-page :name] name)))) - - ptk/WatchEvent - (watch [_ state stream] - (let [params {:id id :name name}] - (->> (rp/mutation :rename-project-page params) - (rx/map #(ptk/data-event ::page-renamed params))))))) - -;; --- Delete Page (by ID) - -(defn delete-page - [id] - {:pre [(uuid? id)]} - (reify - ptk/UpdateEvent - (update [_ state] - (purge-page state id)) - - ptk/WatchEvent - (watch [_ state s] - (let [page (:workspace-page state)] - (rx/merge - (->> (rp/mutation :delete-project-page {:id id}) - (rx/flat-map (fn [_] - (if (= id (:id page)) - (rx/of (go-to (:file-id page))) - (rx/empty)))))))))) - -;; --- Persist Page - -(declare page-persisted) - -(def persist-current-page - (ptk/reify ::persist-page - ptk/WatchEvent - (watch [this state s] - (let [local (:workspace-local state) - page (:workspace-page state) - data (:workspace-data state)] - (if (:history local) - (rx/empty) - (let [page (assoc page :data data)] - (->> (rp/mutation :update-project-page-data page) - (rx/map (fn [res] (merge page res))) - (rx/map page-persisted) - (rx/catch (fn [err] (rx/of ::page-persist-error)))))))))) - -;; --- Page Persisted - -(defn page-persisted - [{:keys [id] :as page}] - (us/verify ::page page) - (ptk/reify ::page-persisted - cljs.core/IDeref - (-deref [_] page) - - ptk/UpdateEvent - (update [_ state] - (let [data (:data page) - page (dissoc page :data)] - (-> state - (assoc :workspace-data data) - (assoc :workspace-page page) - (update :pages assoc id page) - (update :pages-data assoc id data)))))) diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index 87febc1fe9..21909ef92b 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -58,12 +58,12 @@ (declare handle-who) (declare handle-pointer-update) (declare handle-pointer-send) -(declare handle-page-snapshot) +(declare handle-page-change) (declare shapes-changes-commited) (declare commit-changes) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Websockets Events +;; Workspace WebSocket ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; --- Initialize WebSocket @@ -93,7 +93,7 @@ (case type :who (handle-who msg) :pointer-update (handle-pointer-update msg) - :page-snapshot (handle-page-snapshot msg) + :page-change (handle-page-change msg) ::unknown)))) (->> stream @@ -160,11 +160,12 @@ :y (:y point)}] (ws/-send ws (t/encode msg)))))) -(defn handle-page-snapshot - [{:keys [user-id page-id version operations] :as msg}] - (ptk/reify ::handle-page-snapshot +(defn handle-page-change + [{:keys [profile-id page-id revn operations] :as msg}] + (ptk/reify ::handle-page-change ptk/WatchEvent (watch [_ state stream] + (prn "handle-page-change") (let [page-id' (get-in state [:workspace-page :id])] (when (= page-id page-id') (rx/of (shapes-changes-commited msg))))))) @@ -254,9 +255,9 @@ ;; (update [_ state] ;; (update :workspace-local dissoc :undo-index)))) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; General workspace events -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Workspace Initialization +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; --- Initialize Workspace @@ -275,6 +276,9 @@ (declare initialize-layout) (declare initialize-page) (declare initialize-file) +(declare fetch-file-with-users) +(declare fetch-pages) +(declare fetch-page) (defn initialize "Initialize the workspace state." @@ -286,21 +290,31 @@ (watch [_ state stream] (let [file (:workspace-file state)] (if (not= (:id file) file-id) + (do + ;; (reset! st/loader true) + (rx/merge + (rx/of (fetch-file-with-users file-id) + (fetch-pages file-id) + (initialize-layout file-id) + (fetch-images file-id)) + (->> (rx/zip (rx/filter (ptk/type? ::pages-fetched) stream) + (rx/filter (ptk/type? ::file-fetched) stream)) + (rx/take 1) + (rx/do (fn [_] + (uxbox.util.timers/schedule 500 #(reset! st/loader false)))) + (rx/mapcat (fn [_] + (rx/of (initialize-file file-id) + (initialize-page page-id) + #_(initialize-alignment page-id))))))) + (rx/merge - (rx/of (dp/fetch-file file-id) - (dp/fetch-pages file-id) - (initialize-layout file-id) - (fetch-users file-id) - (fetch-images file-id)) - (->> (rx/zip (rx/filter (ptk/type? ::dp/pages-fetched) stream) - (rx/filter (ptk/type? ::dp/files-fetched) stream)) + (rx/of (fetch-page page-id)) + (->> stream + (rx/filter (ptk/type? ::pages-fetched)) (rx/take 1) - (rx/do #(reset! st/loader false)) - (rx/mapcat #(rx/of (initialize-file file-id) - (initialize-page page-id) - #_(initialize-alignment page-id))))) - (rx/of (initialize-file file-id) - (initialize-page page-id))))))) + (rx/merge-map (fn [_] + (rx/of (initialize-file file-id) + (initialize-page page-id))))))))))) (defn- initialize-layout [file-id] @@ -351,9 +365,10 @@ (ptk/reify ::finalize ptk/UpdateEvent (update [_ state] - (dissoc state - :workspace-page - :workspace-data)))) + state + #_(dissoc state + :workspace-page + :workspace-data)))) (def diff-and-commit-changes (ptk/reify ::diff-and-commit-changes @@ -379,17 +394,69 @@ (when-not (empty? changes) (rx/of (commit-changes changes))))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Data Fetching & Uploading +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; --- Specs + +(s/def ::id ::us/uuid) +(s/def ::profile-id ::us/uuid) +(s/def ::name string?) +(s/def ::type keyword?) +(s/def ::file-id ::us/uuid) +(s/def ::created-at ::us/inst) +(s/def ::modified-at ::us/inst) +(s/def ::version ::us/integer) +(s/def ::revn ::us/integer) +(s/def ::ordering ::us/integer) +(s/def ::metadata (s/nilable ::cp/metadata)) +(s/def ::data ::cp/data) + +(s/def ::file ::dp/file) +(s/def ::page + (s/keys :req-un [::id + ::name + ::file-id + ::version + ::revn + ::created-at + ::modified-at + ::ordering + ::data])) + ;; --- Fetch Workspace Users (declare users-fetched) +(declare file-fetched) -(defn fetch-users - [file-id] - (ptk/reify ::fetch-users +(defn fetch-file-with-users + [id] + (us/verify ::us/uuid id) + (ptk/reify ::fetch-file-with-users ptk/WatchEvent (watch [_ state stream] - (->> (rp/query :project-file-users {:file-id file-id}) - (rx/map users-fetched))))) + (->> (rp/query :file-with-users {:id id}) + (rx/merge-map (fn [result] + (rx/of (file-fetched (dissoc result :users)) + (users-fetched (:users result))))))))) +(defn fetch-file + [id] + (us/verify ::us/uuid id) + (ptk/reify ::fetch-file + ptk/WatchEvent + (watch [_ state stream] + (->> (rp/query :file {:id id}) + (rx/map file-fetched))))) + +(defn file-fetched + [{:keys [id] :as file}] + (us/verify ::file file) + (ptk/reify ::file-fetched + ptk/UpdateEvent + (update [_ state] + (update state :files assoc id file)))) (defn users-fetched [users] @@ -402,6 +469,226 @@ users)))) +;; --- Fetch Pages + +(declare pages-fetched) +(declare unpack-page) + +(defn fetch-pages + [file-id] + (us/verify ::us/uuid file-id) + (ptk/reify ::fetch-pages + ptk/WatchEvent + (watch [_ state s] + (->> (rp/query :pages {:file-id file-id}) + (rx/map pages-fetched))))) + +(defn fetch-page + [page-id] + (us/verify ::us/uuid page-id) + (ptk/reify ::fetch-pages + ptk/WatchEvent + (watch [_ state s] + (->> (rp/query :page {:id page-id}) + (rx/map #(pages-fetched [%])))))) + +(defn pages-fetched + [pages] + (us/verify (s/every ::page) pages) + (ptk/reify ::pages-fetched + IDeref + (-deref [_] pages) + + ptk/UpdateEvent + (update [_ state] + (reduce unpack-page state pages)))) + +;; --- Page Crud + +(declare page-created) + +(def create-empty-page + (ptk/reify ::create-empty-page + ptk/WatchEvent + (watch [this state stream] + (let [file-id (get-in state [:workspace-page :file-id]) + name (str "Page " (gensym "p")) + ordering (count (get-in state [:files file-id :pages])) + params {:name name + :file-id file-id + :ordering ordering + :data cp/default-page-data}] + (->> (rp/mutation :create-page params) + (rx/map page-created)))))) + +(defn page-created + [{:keys [id file-id] :as page}] + (us/verify ::page page) + (ptk/reify ::page-created + cljs.core/IDeref + (-deref [_] page) + + ptk/UpdateEvent + (update [_ state] + (-> state + (update-in [:workspace-file :pages] (fnil conj []) id) + (unpack-page page))) + + ptk/WatchEvent + (watch [_ state stream] + (rx/of (fetch-file file-id))))) + +(s/def ::rename-page + (s/keys :req-un [::id ::name])) + +(defn rename-page + [id name] + (us/verify ::us/uuid id) + (us/verify string? name) + (ptk/reify ::rename-page + ptk/UpdateEvent + (update [_ state] + (let [pid (get-in state [:workspac-page :id]) + state (assoc-in state [:pages id :name] name)] + (cond-> state + (= pid id) (assoc-in [:workspace-page :name] name)))) + + ptk/WatchEvent + (watch [_ state stream] + (let [params {:id id :name name}] + (->> (rp/mutation :rename-page params) + (rx/map #(ptk/data-event ::page-renamed params))))))) + +(declare purge-page) +(declare go-to-file) + +(defn delete-page + [id] + {:pre [(uuid? id)]} + (reify + ptk/UpdateEvent + (update [_ state] + (purge-page state id)) + + ptk/WatchEvent + (watch [_ state s] + (let [page (:workspace-page state)] + (rx/merge + (->> (rp/mutation :delete-page {:id id}) + (rx/flat-map (fn [_] + (if (= id (:id page)) + (rx/of (go-to-file (:file-id page))) + (rx/empty)))))))))) + + +;; --- Fetch Workspace Images + +(declare images-fetched) + +(defn fetch-images + [file-id] + (ptk/reify ::fetch-images + ptk/WatchEvent + (watch [_ state stream] + (->> (rp/query :file-images {:file-id file-id}) + (rx/map images-fetched))))) + +(defn images-fetched + [images] + (ptk/reify ::images-fetched + ptk/UpdateEvent + (update [_ state] + (let [images (d/index-by :id images)] + (assoc state :workspace-images images))))) + + +;; --- Upload Image + +(declare image-uploaded) +(def allowed-file-types #{"image/jpeg" "image/png"}) + +(defn upload-image + ([file] (upload-image file identity)) + ([file on-uploaded] + (us/verify fn? on-uploaded) + (ptk/reify ::upload-image + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-local :uploading] true)) + + ptk/WatchEvent + (watch [_ state stream] + (let [allowed-file? #(contains? allowed-file-types (.-type %)) + finalize-upload #(assoc-in % [:workspace-local :uploading] false) + file-id (get-in state [:workspace-page :file-id]) + + on-success #(do (st/emit! finalize-upload) + (on-uploaded %)) + on-error #(do (st/emit! finalize-upload) + (rx/throw %)) + + prepare + (fn [file] + {:name (.-name file) + :file-id file-id + :content file})] + (->> (rx/of file) + (rx/filter allowed-file?) + (rx/map prepare) + (rx/mapcat #(rp/mutation! :upload-file-image %)) + (rx/do on-success) + (rx/map image-uploaded) + (rx/catch on-error))))))) + + +(s/def ::id ::us/uuid) +(s/def ::name ::us/string) +(s/def ::width ::us/number) +(s/def ::height ::us/number) +(s/def ::mtype ::us/string) +(s/def ::uri ::us/string) +(s/def ::thumb-uri ::us/string) + +(s/def ::image + (s/keys :req-un [::id + ::name + ::width + ::height + ::uri + ::thumb-uri])) + +(defn image-uploaded + [item] + (us/verify ::image item) + (ptk/reify ::image-created + ptk/UpdateEvent + (update [_ state] + (update state :workspace-images assoc (:id item) item)))) + + +;; --- Helpers + +(defn unpack-page + [state {:keys [id data] :as page}] + (-> state + (update :pages assoc id (dissoc page :data)) + (update :pages-data assoc id data))) + +(defn purge-page + "Remove page and all related stuff from the state." + [state id] + (if-let [file-id (get-in state [:pages id :file-id])] + (-> state + (update-in [:files file-id :pages] #(filterv (partial not= id) %)) + (update-in [:workspace-file :pages] #(filterv (partial not= id) %)) + (update :pages dissoc id) + (update :pages-data dissoc id)) + state)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Workspace State Manipulation +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + ;; --- Toggle layout flag (defn toggle-layout-flag @@ -1052,23 +1339,23 @@ (watch [_ state stream] (let [page (:workspace-page state) params {:id (:id page) - :version (:version page) + :revn (:revn page) :changes (vec changes)}] - (->> (rp/mutation :update-project-page params) + (->> (rp/mutation :update-page params) (rx/map shapes-changes-commited)))))) (s/def ::shapes-changes-commited - (s/keys :req-un [::page-id ::version ::cp/changes])) + (s/keys :req-un [::page-id ::revn ::cp/changes])) (defn shapes-changes-commited - [{:keys [page-id version changes] :as params}] + [{:keys [page-id revn changes] :as params}] (us/verify ::shapes-changes-commited params) (ptk/reify ::shapes-changes-commited ptk/UpdateEvent (update [_ state] (-> state - (assoc-in [:workspace-page :version] version) - (assoc-in [:pages page-id :version] version) + (assoc-in [:workspace-page :revn] revn) + (assoc-in [:pages page-id :revn] revn) (update-in [:pages-data page-id] cp/process-changes changes) (update :workspace-data cp/process-changes changes))))) @@ -1298,7 +1585,7 @@ (defn go-to-page [page-id] (us/verify ::us/uuid page-id) - (ptk/reify ::go-to + (ptk/reify ::go-to-page ptk/WatchEvent (watch [_ state stream] (let [file-id (get-in state [:workspace-page :file-id]) @@ -1306,93 +1593,16 @@ query-params {:page-id page-id}] (rx/of (rt/nav :workspace path-params query-params)))))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Workspace Images -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -;; --- Fetch Workspace Images - -(declare images-fetched) - -(defn fetch-images +(defn go-to-file [file-id] - (ptk/reify ::fetch-images + (us/verify ::us/uuid file-id) + (ptk/reify ::go-to-file ptk/WatchEvent (watch [_ state stream] - (->> (rp/query :project-file-images {:file-id file-id}) - (rx/map images-fetched))))) - -(defn images-fetched - [images] - (ptk/reify ::images-fetched - ptk/UpdateEvent - (update [_ state] - (let [images (d/index-by :id images)] - (assoc state :workspace-images images))))) - - -;; --- Upload Image - -(declare image-uploaded) -(def allowed-file-types #{"image/jpeg" "image/png"}) - -(defn upload-image - ([file] (upload-image file identity)) - ([file on-uploaded] - (us/verify fn? on-uploaded) - (ptk/reify ::upload-image - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:workspace-local :uploading] true)) - - ptk/WatchEvent - (watch [_ state stream] - (let [allowed-file? #(contains? allowed-file-types (.-type %)) - finalize-upload #(assoc-in % [:workspace-local :uploading] false) - file-id (get-in state [:workspace-page :file-id]) - - on-success #(do (st/emit! finalize-upload) - (on-uploaded %)) - on-error #(do (st/emit! finalize-upload) - (rx/throw %)) - - prepare - (fn [file] - {:name (.-name file) - :file-id file-id - :content file})] - (->> (rx/of file) - (rx/filter allowed-file?) - (rx/map prepare) - (rx/mapcat #(rp/mutation! :upload-project-file-image %)) - (rx/do on-success) - (rx/map image-uploaded) - (rx/catch on-error))))))) - -(s/def ::id ::us/uuid) -(s/def ::name ::us/string) -(s/def ::width ::us/number) -(s/def ::height ::us/number) -(s/def ::mtype ::us/string) -(s/def ::uri ::us/string) -(s/def ::thumb-uri ::us/string) - -(s/def ::image - (s/keys :req-un [::id - ::name - ::width - ::height - ::uri - ::thumb-uri])) - -(defn image-uploaded - [item] - (us/verify ::image item) - (ptk/reify ::image-created - ptk/UpdateEvent - (update [_ state] - (update state :workspace-images assoc (:id item) item)))) + (let [page-ids (get-in state [:files file-id :pages]) + path-params {:file-id file-id} + query-params {:page-id (first page-ids)}] + (rx/of (rt/nav :workspace path-params query-params)))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Page Changes Reactions diff --git a/frontend/src/uxbox/main/repo.cljs b/frontend/src/uxbox/main/repo.cljs index ae28ed36de..35c5fc368f 100644 --- a/frontend/src/uxbox/main/repo.cljs +++ b/frontend/src/uxbox/main/repo.cljs @@ -120,7 +120,7 @@ (seq params)) (send-mutation! id form))) -(defmethod mutation :upload-project-file-image +(defmethod mutation :upload-file-image [id params] (let [form (js/FormData.)] (run! (fn [[key val]] diff --git a/frontend/src/uxbox/main/ui/dashboard.cljs b/frontend/src/uxbox/main/ui/dashboard.cljs index 18403ede46..a890f32f79 100644 --- a/frontend/src/uxbox/main/ui/dashboard.cljs +++ b/frontend/src/uxbox/main/ui/dashboard.cljs @@ -5,7 +5,6 @@ [uxbox.util.data :refer [parse-int uuid-str?]] [uxbox.main.ui.dashboard.header :refer [header]] [uxbox.main.ui.dashboard.projects :as projects] - ;; [uxbox.main.ui.dashboard.elements :as elements] [uxbox.main.ui.dashboard.icons :as icons] [uxbox.main.ui.dashboard.images :as images] [uxbox.main.ui.dashboard.colors :as colors] diff --git a/frontend/src/uxbox/main/ui/dashboard/projects.cljs b/frontend/src/uxbox/main/ui/dashboard/projects.cljs index 8d1dcb14d2..1dbb1c28c8 100644 --- a/frontend/src/uxbox/main/ui/dashboard/projects.cljs +++ b/frontend/src/uxbox/main/ui/dashboard/projects.cljs @@ -2,6 +2,9 @@ ;; 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/. ;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; ;; Copyright (c) 2015-2017 Andrey Antukh ;; Copyright (c) 2015-2017 Juan de la Cruz @@ -14,6 +17,7 @@ [uxbox.builtins.icons :as i] [uxbox.main.constants :as c] [uxbox.main.data.projects :as udp] + [uxbox.main.data.dashboard :as dsh] [uxbox.main.store :as st] [uxbox.main.exports :as exports] [uxbox.main.ui.modal :as modal] @@ -137,13 +141,12 @@ (sort-by order)) on-click #(do (dom/prevent-default %) - (st/emit! (udp/create-file {:project-id id})))] + (st/emit! dsh/create-file))] [:section.dashboard-grid [:div.dashboard-grid-content [:div.dashboard-grid-row - (when id - [:div.grid-item.add-project {:on-click on-click} - [:span (tr "ds.new-file")]]) + [:div.grid-item.add-project {:on-click on-click} + [:span (tr "ds.new-file")]] (for [item files] [:& grid-item {:file item :key (:id item)}])]]])) @@ -195,16 +198,20 @@ :placeholder (tr "ds.search.placeholder")}] [:div.clear-search i/close]] [:ul.library-elements - [:li.recent-projects {:on-click #(st/emit! (udp/go-to-project nil)) - :class-name (when (nil? id) "current")} + [:li.recent-projects #_{:on-click #(st/emit! (udp/go-to-project nil)) + :class-name (when (nil? id) "current")} [:span.element-title "Recent"]] + [:li.recent-projects {:on-click #(st/emit! (udp/go-to-project nil)) + :class-name (when (nil? id) "current")} + [:span.element-title "Drafts"]] + [:div.projects-row - [:span "PROJECTS"] - [:a.add-project {:on-click #(st/emit! udp/create-project)} + [:span "PROJECTS/TEAMS TODO"] + #_[:a.add-project {:on-click #(st/emit! udp/create-project)} i/close]] - (for [item projects] + #_(for [item projects] [:& nav-item {:id (:id item) :key (:id item) :name (:name item) @@ -213,14 +220,9 @@ ;; --- Component: Content (def files-ref - (letfn [(selector [state] - (let [id (get-in state [:dashboard-projects :id]) - ids (get-in state [:dashboard-projects :files id]) - xf (comp (map #(get-in state [:files %])) - (remove nil?))] - (into [] xf ids)))] - (-> (l/lens selector) - (l/derive st/state)))) + (-> (comp (l/key :files) + (l/lens vals)) + (l/derive st/state))) (mf/defc content [{:keys [id] :as props}] @@ -233,8 +235,7 @@ (mf/defc projects-page [{:keys [id] :as props}] - (mf/use-effect #(st/emit! udp/fetch-projects)) - (mf/use-effect {:fn #(st/emit! (udp/initialize id)) + (mf/use-effect {:fn #(st/emit! dsh/initialize-drafts) :deps #js [id]}) [:section.dashboard-content [:& nav {:id id}] diff --git a/frontend/src/uxbox/main/ui/workspace/header.cljs b/frontend/src/uxbox/main/ui/workspace/header.cljs index 65e679c9cb..ab0bf6117e 100644 --- a/frontend/src/uxbox/main/ui/workspace/header.cljs +++ b/frontend/src/uxbox/main/ui/workspace/header.cljs @@ -87,7 +87,7 @@ {:alt (tr "header.sitemap") :class (when (contains? layout :sitemap) "selected") :on-click #(st/emit! (dw/toggle-layout-flag :sitemap))} - [:span (:project-name file) " / " (:name file)]] + [:span (:name file)]] [:& active-users] diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs index e09f924a4d..de3b349dd3 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs @@ -11,7 +11,6 @@ [lentes.core :as l] [rumext.alpha :as mf] [uxbox.builtins.icons :as i] - [uxbox.main.data.projects :as dp] [uxbox.main.data.workspace :as dw] [uxbox.main.store :as st] [uxbox.main.refs :as refs] @@ -45,7 +44,7 @@ ;; parent (.-parentNode parent) name (dom/get-value target)] ;; (set! (.-draggable parent) true) - (st/emit! (dp/rename-page (:id page) name)) + (st/emit! (dw/rename-page (:id page) name)) (swap! local assoc :edition false))) on-key-down (fn [event] @@ -56,7 +55,7 @@ (kbd/esc? event) (swap! local assoc :edition false))) - delete-fn #(st/emit! (dp/delete-page (:id page))) + delete-fn #(st/emit! (dw/delete-page (:id page))) on-delete #(do (dom/prevent-default %) (dom/stop-propagation %) @@ -128,7 +127,7 @@ (mf/defc sitemap-toolbox [{:keys [file page] :as props}] - (let [on-create-click #(st/emit! dp/create-empty-page) + (let [on-create-click #(st/emit! dw/create-empty-page) locale (i18n/use-locale)] [:div.sitemap.tool-window [:div.tool-window-bar