♻️ Big refactor of the default data model.

Introduce teams.
This commit is contained in:
Andrey Antukh 2020-02-17 09:49:04 +01:00
parent 6379c62e37
commit 7a5145fa37
65 changed files with 4529 additions and 3005 deletions

View file

@ -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;

View file

@ -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);

View file

@ -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,95 +112,48 @@ 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
@ -165,7 +161,7 @@ CREATE OR REPLACE FUNCTION handle_page_update()
--- 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();

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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}))

View file

@ -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,
(def sql:create-page
"insert into page (id, file_id, name,
version, ordering, data)
values ($1, $2, $3, $4, $5, $6, $7)
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]
(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!
(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))))
(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)))

View file

@ -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
(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 ""})))))
: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
(p/promise {:status 200
:body {:params (:params req)
:cookies (:cookies req)
:headers (:headers req)}})
:headers (:headers req)}}))

View file

@ -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"])]

View file

@ -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)

View file

@ -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")}
]})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -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))

View file

@ -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 <niwi@niwi.nz>
(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))

View file

@ -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})))

View file

@ -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 <niwi@niwi.nz>
(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))))

View file

@ -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 <niwi@niwi.nz>
(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]))
(sm/defmutation ::update-icons-collection
[{:keys [id user name] :as params}]
(let [sql "update icon_collections
set name = $3
(def ^:private sql:rename-collection
"update icon_collection
set name = $2
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))))
returning *")
;; --- Copy Icon
(s/def ::rename-icon-collection
(s/keys :req-un [::profile-id ::name ::id]))
(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]))
(sm/defmutation ::delete-icons-collection
[{:keys [user id] :as params}]
(let [sql "update icon_collections
(def ^:private sql:mark-collection-deleted
"update icon_collection
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)
returning id")
(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
(s/def ::delete-icon
(s/keys :req-un [::user ::id]))
(sm/defmutation ::delete-icon
[{:keys [id user] :as params}]
(let [sql "update icons
(def ^:private sql:mark-icon-deleted
"update icon
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))))
and profile_id = $2
returning id")
(s/def ::delete-icon
(s/keys :req-un [::profile-id ::id]))
(sm/defmutation ::delete-icon
[{: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))

View file

@ -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 <niwi@niwi.nz>
(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)
(def ^:private sql:create-image-collection
"insert into image_collection (id, profile_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
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)))
(s/def ::delete-image
(s/keys :req-un [::id ::user]))
(sm/defmutation ::delete-image
[{:keys [user id] :as params}]
(let [sql "update images
(def ^:private sql:mark-image-deleted
"update image
set deleted_at = clock_timestamp()
where id = $1
and user_id = $2
returning *"]
(db/query-one db/pool [sql id user])))
and profile_id = $2
returning id")
(s/def ::delete-image
(s/keys :req-un [::id ::profile-id]))
(sm/defmutation ::delete-image
[{: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))

View file

@ -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 <niwi@niwi.nz>
(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)))

View file

@ -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)))

View file

@ -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 <niwi@niwi.nz>
(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))))

View file

@ -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 <niwi@niwi.nz>
(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)))

View file

@ -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 <niwi@niwi.nz>
;; 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 <niwi@niwi.nz>
(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)))

View file

@ -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 <niwi@niwi.nz>
(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)))

View file

@ -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 <niwi@niwi.nz>
(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))))

View file

@ -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 <niwi@niwi.nz>
(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))))))

View file

@ -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 <niwi@niwi.nz>
(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 %)))))

View file

@ -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 <niwi@niwi.nz>
(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)
(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))))))))
(mapv #(images/resolve-urls % :thumb-path :thumb-uri)))))))

View file

@ -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 <niwi@niwi.nz>
(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)))))

View file

@ -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)
[{: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]))))
(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]))

View file

@ -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 <niwi@niwi.nz>
(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))))))

View file

@ -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 <niwi@niwi.nz>
(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)))))

View file

@ -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)))))

View file

@ -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)

View file

@ -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

View file

@ -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 <niwi@niwi.nz>
(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])))

View file

@ -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 <niwi@niwi.nz>
(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)))))

View file

@ -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)

View file

@ -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)

View file

@ -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 <niwi@niwi.nz>
(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])))

View file

@ -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))))

View file

@ -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,34 +70,37 @@
;; --- 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-project-file
[conn user-id project-id i]
(files/create-file conn {:id (mk-uuid "project-file" i)
:user user-id
(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 "sample project file" i)}))
:name (str "file" i)}))
(defn create-project-page
[conn user-id file-id i]
(pages/create-page conn {:id (mk-uuid "page" i)
:user user-id
(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
@ -104,14 +110,26 @@
: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
(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-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,7 +146,8 @@
[expr]
`(try
(let [d# (p/deferred)]
(->> #(p/finally ~expr (fn [v# e#]
(->> #(p/finally (p/do! ~expr)
(fn [v# e#]
(if e#
(p/reject! d# e#)
(p/resolve! d# v#))))
@ -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]}]

View file

@ -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 "<g></g>"
;; :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 "<g></g>"
;; :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 "<g></g>"
;; :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 "<g></g>"
;; :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 "<g></g>"
;; :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))))))))

View file

@ -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))))))
))

View file

@ -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

View file

@ -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 "<rect></rect>"
: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))))))
))

View file

@ -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
(t/deftest image-collections-crud
(let [id (uuid/next)
user @(th/create-user db/pool 2)]
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

View file

@ -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])))))
))

View file

@ -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,10 +23,11 @@
(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"
(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))]
@ -40,48 +42,52 @@
(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"
(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 (= (get-in out [:result :id]) (:id user)))))
(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)]
(t/testing "query profile"
(let [data {::sq/type :profile
:profile-id (:id profile)}
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/deftest mutation-update-profile
(let [user @(th/create-user db/pool 1)
data (assoc user
(let [result (:result out)]
(t/is (= "Profile 1" (:fullname result)))
(t/is (= "profile1.test@nodomain.com" (:email result)))
(t/is (not (contains? result :password))))))
(t/testing "update profile"
(let [data (assoc profile
::sm/type :update-profile
:fullname "Full Name"
:username "user222"
:name "profile222"
: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)))))
(t/deftest mutation-update-profile-photo
(let [user @(th/create-user db/pool 1)
data {::sm/type :update-profile-photo
:user (:id user)
(let [result (:result out)]
(t/is (= (:fullname data) (:fullname result)))
(t/is (= (:email data) (:email result)))
(t/is (not (contains? result :password))))))
(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
@ -90,62 +96,102 @@
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (= (:id user) (get-in out [:result :id])))))
;; (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)))
(let [result (:result out)]
(t/is (= (:id profile) (:id result))))))
))
;; (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)))))))
(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-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 "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))]
;; ;; Initially no tokens exists
;; (t/is (nil? res))
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (nil? (:result 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)))
(t/testing "query profile after delete"
(let [data {::sq/type :profile
:profile-id (:id prof)}
out (th/try-on! (sq/handle data))]
;; (let [res (sc/fetch-one conn sql)]
;; (t/is (not (nil? res)))
;; (t/is (= (:user res) (:id user))))))))
;; (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 (:username user))
;; uri (str th/+base-url+ "/api/auth/recovery")
;; data {:token token :password "mytestpassword"}
;; [status data] (th/http-put user uri {:body data})
(let [result (:result out)]
(t/is (= (:fullname prof) (:fullname result))))))
;; user' (usu/find-full-user-by-id conn (:id user))]
;; (t/is (= status 204))
;; (t/is (hashers/check "mytestpassword" (:password user'))))))))
(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

View file

@ -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]))))))

View file

@ -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)))))

View file

@ -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)
(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/is (nil? (:error out)))
(t/is (= (:name data) (get-in out [:result :name])))))
(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)}
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (= (:name data) (:name result))))))
(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)
(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])))))
(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)))
(t/is (= (:id data) (get-in out [:result :id])))
(t/is (= (:user data) (get-in out [:result :user-id])))
(t/is (= (:name data) (get-in out [:result :name])))))
(t/deftest mutation-delete-project
(let [user @(th/create-user db/pool 1)
proj @(th/create-project db/pool (:id user) 1)
data {::sm/type :delete-project
:id (:id proj)
:user (:id user)}
(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/is (nil? (: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/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)
;; TODO: add permisions related tests
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (= 1 (count result))))))
))

View file

@ -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))))))

View file

@ -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"))

View file

@ -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

View file

@ -5,49 +5,222 @@
;; Copyright (c) 2015-2016 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.main.data.dashboard
(:require [beicon.core :as rx]
[uxbox.util.uuid :as uuid]
(:require
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[potok.core :as ptk]
[uxbox.util.router :as r]
[uxbox.main.store :as st]
[uxbox.common.data :as d]
[uxbox.common.pages :as cp]
[uxbox.common.spec :as us]
[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)]))
[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]
(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)
(s/def ::team
(s/keys :req-un [::id
::name
::created-at
::modified-at]))
(s/def ::project
(s/keys ::req-un [::id
::name
::team-id
::version
::profile-id
::created-at
::modified-at]))
(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]
(update state :dashboard assoc
:section section
:collection-type :builtin
:collection-id 1)))
(let [profile (:profile state)]
(update state :dashboard-local assoc
:team-id (:default-team-id profile)
:project-id (:default-project-id profile))))
(defn initialize
[section]
(InitializeDashboard. section))
ptk/WatchEvent
(watch [_ state stream]
(let [local (:dashboard-local state)]
(rx/of (fetch-files (:project-id local)))))))
(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
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 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]
(as-> state $
(assoc-in $ [:dashboard :collection-type] type)
(select-first $))))))
(let [assoc-project #(update-in %1 [:projects (:id %2)] merge %2)]
(reduce assoc-project state projects)))))
(defn set-collection
;; --- 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]
(reify
(us/verify ::us/uuid id)
(ptk/reify ::delete-project
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:dashboard :collection-id] id))))
(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})))))

View file

@ -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)))))

View file

@ -4,6 +4,11 @@
;;
;; Copyright (c) 2015-2017 Andrey Antukh <niwi@niwi.nz>
;; 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))))))

View file

@ -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 (dp/fetch-file file-id)
(dp/fetch-pages file-id)
(rx/of (fetch-file-with-users file-id)
(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/zip (rx/filter (ptk/type? ::pages-fetched) stream)
(rx/filter (ptk/type? ::file-fetched) stream))
(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/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-page page-id)
#_(initialize-alignment page-id)))))))
(rx/merge
(rx/of (fetch-page page-id))
(->> stream
(rx/filter (ptk/type? ::pages-fetched))
(rx/take 1)
(rx/merge-map (fn [_]
(rx/of (initialize-file file-id)
(initialize-page page-id)))))))))))
(defn- initialize-layout
[file-id]
@ -351,7 +365,8 @@
(ptk/reify ::finalize
ptk/UpdateEvent
(update [_ state]
(dissoc state
state
#_(dissoc state
:workspace-page
:workspace-data))))
@ -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

View file

@ -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]]

View file

@ -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]

View file

@ -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 <niwi@niwi.nz>
;; Copyright (c) 2015-2017 Juan de la Cruz <delacruzgarciajuan@gmail.com>
@ -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")]])
[: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))
[: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}]

View file

@ -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]

View file

@ -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