♻️ 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 "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto"; CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- Modified At
CREATE FUNCTION update_modified_at() CREATE FUNCTION update_modified_at()
RETURNS TRIGGER AS $updt$ RETURNS TRIGGER AS $updt$
BEGIN BEGIN
@ -10,3 +8,19 @@ CREATE FUNCTION update_modified_at()
RETURN NEW; RETURN NEW;
END; END;
$updt$ LANGUAGE plpgsql; $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(), id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(), created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
@ -14,18 +14,26 @@ CREATE TABLE users (
is_demo boolean NOT NULL DEFAULT false is_demo boolean NOT NULL DEFAULT false
); );
CREATE UNIQUE INDEX users__email__idx CREATE UNIQUE INDEX profile__email__idx
ON users (email) ON profile (email)
WHERE deleted_at IS null; WHERE deleted_at IS null;
CREATE INDEX users__is_demo CREATE INDEX profile__is_demo
ON users (is_demo) ON profile (is_demo)
WHERE deleted_at IS null WHERE deleted_at IS null
AND is_demo IS true; AND is_demo IS true;
--- Table used for register all used emails by the user INSERT INTO profile (id, fullname, email, photo, password)
CREATE TABLE user_emails ( VALUES ('00000000-0000-0000-0000-000000000000'::uuid,
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, '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(), created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
verified_at timestamptz NULL DEFAULT NULL, verified_at timestamptz NULL DEFAULT NULL,
@ -36,13 +44,58 @@ CREATE TABLE user_emails (
is_verified boolean NOT NULL DEFAULT false is_verified boolean NOT NULL DEFAULT false
); );
CREATE INDEX user_emails__user_id__idx CREATE INDEX profile_email__profile_id__idx
ON user_emails (user_id); 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(), created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
modified_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, key text NOT NULL,
val bytea 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 ( CREATE TRIGGER profile_attr__modified_at__tgr
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, 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, token text NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(), created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
used_at timestamptz NULL, 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(), id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(), created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
modified_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 user_agent text NULL
); );
CREATE INDEX sessions__user_id__idx CREATE INDEX session__profile_id__idx
ON sessions (user_id); ON session(profile_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();

View File

@ -1,8 +1,53 @@
-- Tables CREATE TABLE project (
CREATE TABLE projects (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 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(), created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
modified_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 name text NOT NULL
); );
CREATE INDEX projects__user_id__idx CREATE TRIGGER file__modified_at__tgr
ON projects(user_id); 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, CREATE TABLE file_profile_rel (
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE, 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(), created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
modified_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, can_edit boolean DEFAULT false,
PRIMARY KEY (user_id, project_id) PRIMARY KEY (file_id, profile_id)
); );
CREATE INDEX project_users__user_id__idx COMMENT ON TABLE file_profile_rel
ON project_users(user_id); IS 'Relation between files and profiles (NM)';
CREATE INDEX project_users__project_id__idx CREATE INDEX file_profile_rel__profile_id__idx
ON project_users(project_id); 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(), 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 file(id) ON DELETE CASCADE,
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
name text NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(), created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), modified_at timestamptz NOT NULL DEFAULT clock_timestamp(),
deleted_at timestamptz DEFAULT NULL 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,
name text NOT NULL, name text NOT NULL,
@ -69,103 +112,56 @@ CREATE TABLE project_file_images (
thumb_mtype text NOT NULL thumb_mtype text NOT NULL
); );
CREATE INDEX project_file_images__file_id__idx CREATE INDEX file_image__file_id__idx
ON project_file_images(file_id); ON file_image(file_id);
CREATE INDEX project_file_images__user_id__idx CREATE TRIGGER file_image__modified_at__tgr
ON project_file_images(user_id); BEFORE UPDATE ON file_image
FOR EACH ROW EXECUTE PROCEDURE update_modified_at();
CREATE TABLE project_file_users ( CREATE TRIGGER file_image__on_delete__tgr
file_id uuid NOT NULL REFERENCES project_files(id) ON DELETE CASCADE, AFTER DELETE ON file_image
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, 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 TABLE page (
);
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 (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
file_id uuid NOT NULL REFERENCES file(id) ON DELETE CASCADE,
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
file_id uuid NOT NULL REFERENCES project_files(id) ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(), created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), modified_at timestamptz NOT NULL DEFAULT clock_timestamp(),
deleted_at timestamptz DEFAULT NULL, 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, ordering smallint NOT NULL,
name text NOT NULL, name text NOT NULL,
data bytea NOT NULL data bytea NOT NULL
); );
CREATE INDEX project_pages__user_id__idx CREATE INDEX page__file_id__idx
ON project_pages(user_id); ON page(file_id);
CREATE INDEX project_pages__file_id__idx CREATE FUNCTION handle_page_update()
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()
RETURNS TRIGGER AS $pagechange$ RETURNS TRIGGER AS $pagechange$
DECLARE DECLARE
current_dt timestamptz := clock_timestamp(); current_dt timestamptz := clock_timestamp();
proj_id uuid; proj_id uuid;
BEGIN BEGIN
UPDATE project_files NEW.modified_at := current_dt;
UPDATE file
SET modified_at = current_dt SET modified_at = current_dt
WHERE id = OLD.file_id WHERE id = OLD.file_id
RETURNING project_id RETURNING project_id
INTO STRICT proj_id; INTO STRICT proj_id;
--- Update projects modified_at attribute when a --- Update projects modified_at attribute when a
--- page of that project is modified. --- page of that project is modified.
UPDATE projects UPDATE project
SET modified_at = current_dt SET modified_at = current_dt
WHERE id = proj_id; WHERE id = proj_id;
@ -173,27 +169,60 @@ CREATE OR REPLACE FUNCTION handle_page_update()
END; END;
$pagechange$ LANGUAGE plpgsql; $pagechange$ LANGUAGE plpgsql;
CREATE TRIGGER projects_on_insert_tgr CREATE TRIGGER page__on_update__tgr
AFTER INSERT ON projects BEFORE UPDATE ON page
FOR EACH ROW EXECUTE PROCEDURE handle_project_insert();
CREATE TRIGGER pages__on_update__tgr
BEFORE UPDATE ON project_pages
FOR EACH ROW EXECUTE PROCEDURE handle_page_update(); 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(); 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 CREATE TABLE page_change (
BEFORE UPDATE ON project_page_snapshots 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(); FOR EACH ROW EXECUTE PROCEDURE update_modified_at();

View File

@ -1,6 +1,4 @@
--- Tables CREATE TABLE task (
CREATE TABLE tasks (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(), created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
@ -14,16 +12,21 @@ CREATE TABLE tasks (
props bytea NOT NULL, props bytea NOT NULL,
error text NULL DEFAULT NULL, error text NULL DEFAULT NULL,
result bytea NULL DEFAULT NULL,
retry_num smallint NOT NULL DEFAULT 0, retry_num smallint NOT NULL DEFAULT 0,
status text NOT NULL DEFAULT 'new' status text NOT NULL DEFAULT 'new'
); );
CREATE INDEX tasks__scheduled_at__queue__idx CREATE INDEX task__scheduled_at__queue__idx
ON tasks (scheduled_at, queue); 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, id text PRIMARY KEY,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(), created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
@ -33,9 +36,6 @@ CREATE TABLE scheduled_tasks (
cron_expr text NOT NULL cron_expr text NOT NULL
); );
--- Triggers CREATE TRIGGER scheduled_task__modified_at__tgr
BEFORE UPDATE ON scheduled_task
CREATE TRIGGER scheduled_tasks__modified_at__tgr
BEFORE UPDATE ON scheduled_tasks
FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); 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(), 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(), created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
modified_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 name text NOT NULL
); );
CREATE INDEX image_collections__user_id__idx CREATE INDEX image_collection__profile_id__idx
ON image_collections(user_id); 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(), 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,
collection_id uuid NOT NULL REFERENCES image_collections(id) ON DELETE CASCADE, collection_id uuid NOT NULL REFERENCES image_collection(id) ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(), created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
modified_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 thumb_mtype text NOT NULL
); );
CREATE INDEX images__user_id__idx CREATE INDEX image__profile_id__idx
ON images(user_id); ON image(profile_id);
CREATE INDEX images__collection_id__idx CREATE INDEX image__collection_id__idx
ON images(collection_id); ON image(collection_id);
CREATE TRIGGER image_collections__modified_at__tgr CREATE TRIGGER image__modified_at__tgr
BEFORE UPDATE ON image_collections BEFORE UPDATE ON image
FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); FOR EACH ROW EXECUTE PROCEDURE update_modified_at();
CREATE TRIGGER images__modified_at__tgr CREATE TRIGGER image__on_delete__tgr
BEFORE UPDATE ON images AFTER DELETE ON image
FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); FOR EACH ROW EXECUTE PROCEDURE handle_delete();

View File

@ -1,8 +1,6 @@
-- Tables CREATE TABLE icon_collection (
CREATE TABLE icon_collections (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 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(), created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
modified_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 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(), 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(), created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), modified_at timestamptz NOT NULL DEFAULT clock_timestamp(),
deleted_at timestamptz DEFAULT NULL, deleted_at timestamptz DEFAULT NULL,
collection_id uuid REFERENCES icon_collection(id)
ON DELETE CASCADE,
name text NOT NULL, name text NOT NULL,
content text NOT NULL, content text NOT NULL,
metadata bytea NOT NULL, metadata bytea NOT NULL
collection_id uuid REFERENCES icon_collections(id)
ON DELETE SET NULL
DEFAULT 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 TRIGGER icon__modified_at__tgr
CREATE INDEX icons__user_id__idx ON icons(user_id); BEFORE UPDATE ON icon
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
FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); 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] [cuerdas.core :as str]
[environ.core :refer [env]] [environ.core :refer [env]]
[mount.core :refer [defstate]] [mount.core :refer [defstate]]
[uxbox.common.exceptions :as ex])) [uxbox.common.exceptions :as ex]
[uxbox.util.time :as tm]))
(def defaults (def defaults
{:http-server-port 6060 {:http-server-port 6060
@ -106,3 +107,6 @@
(defstate config (defstate config
:start (read-config env)) :start (read-config env))
(def default-deletion-delay
(tm/duration {:hours 48}))

View File

@ -12,173 +12,255 @@
[mount.core :as mount] [mount.core :as mount]
[promesa.core :as p] [promesa.core :as p]
[uxbox.config :as cfg] [uxbox.config :as cfg]
[uxbox.common.data :as d]
[uxbox.core] [uxbox.core]
[uxbox.db :as db] [uxbox.db :as db]
[uxbox.media :as media] [uxbox.media :as media]
[uxbox.migrations] [uxbox.migrations]
[uxbox.services.mutations.profile :as mt.profile]
[uxbox.util.blob :as blob] [uxbox.util.blob :as blob]
[uxbox.util.uuid :as uuid])) [uxbox.util.uuid :as uuid]
[vertx.util :as vu]))
(defn- mk-uuid (defn- mk-uuid
[prefix & args] [prefix & args]
(uuid/namespaced uuid/oid (apply str prefix (interpose "-" args)))) (uuid/namespaced uuid/oid (apply str prefix (interpose "-" args))))
;; --- Users creation ;; --- Profiles creation
(def create-user-sql
"insert into users (id, fullname, username, email, password, photo)
values ($1, $2, $3, $4, $5, $6)
returning *;")
(def password (pwhash/derive "123123")) (def password (pwhash/derive "123123"))
(defn create-user (def sql:create-team
[conn user-index] "insert into team (id, name, photo)
(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)
values ($1, $2, $3) values ($1, $2, $3)
returning *;") returning *;")
(defn create-project (def sql:create-team-profile
[conn [project-index user-index]] "insert into team_profile_rel (team_id, profile_id, is_owner, is_admin, can_edit)
(log/info "create project" user-index project-index) values ($1, $2, $3, $4, $5)
(let [sql create-project-sql returning *;")
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])))))
;; --- Create Page Files (def sql:create-project
"insert into project (id, team_id, name)
values ($1, $2, $3)
returning *;")
(def create-file-sql (def sql:create-project-profile
"insert into project_files (id, user_id, project_id, name) "insert into project_profile_rel (project_id, profile_id, is_owner, is_admin, can_edit)
values ($1, $2, $3, $4) returning id") values ($1, $2, $3, $4, $5)
returning *")
(defn create-file (def sql:create-file-profile
[conn [file-index project-index user-index]] "insert into file_profile_rel (file_id, profile_id, is_owner, is_admin, can_edit)
(log/info "create page file" user-index project-index file-index) values ($1, $2, $3, $4, $5)
(let [sql create-file-sql returning *")
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])))
;; --- Create Pages (def sql:create-file
"insert into file (id, project_id, name)
values ($1, $2, $3 ) returning *")
(def create-page-sql (def sql:create-page
"insert into project_pages (id, user_id, file_id, name, "insert into page (id, file_id, name,
version, ordering, data) version, ordering, data)
values ($1, $2, $3, $4, $5, $6, $7) values ($1, $2, $3, $4, $5, $6)
returning id;") 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 (def preset-small
{:users 50 {:num-teams 50
:projects 5 :num-profiles 50
:files 5 :num-profiles-per-team 5
:pages 3}) :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 (defn rng-ids
{:users 500 [rng n max]
:projects 20 (let [stream (->> (.longs rng 0 max)
:files 5 (.iterator)
:pages 3}) (iterator-seq))]
(reduce (fn [acc item]
(if (= (count acc) n)
(reduced acc)
(conj acc item)))
#{}
stream)))
(def preset-big (defn rng-vec
{:users 5000 [rng vdata n]
:projects 50 (let [ids (rng-ids rng n (count vdata))]
:files 5 (mapv #(nth vdata %) ids)))
:pages 4})
(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 (defn run
[opts] [opts]
(db/with-atomic [conn db/pool] (let [rng (java.util.Random. 1)
(p/do!
(p/run! #(create-user conn %) (range (:users opts))) create-profile
(p/run! #(create-project conn %) (fn [conn index]
(for [user-index (range (:users opts)) (let [id (mk-uuid "profile" index)]
project-index (range (:projects opts))] (log/info "create profile" id)
[project-index user-index])) (mt.profile/register-profile conn
(p/run! #(create-file conn %) {:id id
(for [user-index (range (:users opts)) :fullname (str "Profile " index)
project-index (range (:projects opts)) :password "123123"
file-index (range (:files opts))] :demo? true
[file-index project-index user-index])) :email (str "profile" index ".test@uxbox.io")})))
(p/run! #(create-page conn %)
(for [user-index (range (:users opts)) create-profiles
project-index (range (:projects opts)) (fn [conn]
file-index (range (:files opts)) (log/info "create profiles")
page-index (range (:pages opts))] (collect (partial create-profile conn)
[page-index file-index project-index user-index])) (range (:num-profiles opts))))
(p/promise nil))))
create-team
(fn [conn index]
(let [sql sql:create-team
id (mk-uuid "team" index)
name (str "Team" index)]
(log/info "create team" id)
(-> (db/query-one conn [sql id name ""])
(p/then (constantly id)))))
create-teams
(fn [conn]
(log/info "create teams")
(collect (partial create-team conn)
(range (:num-teams opts))))
create-page
(fn [conn owner-id project-id file-id index]
(p/let [id (mk-uuid "page" project-id file-id index)
data {:version 1
:shapes []
:canvas []
:options {}
:shapes-by-id {}}
name (str "page " index)
version 0
ordering index
data (blob/encode data)]
(log/info "create page" id)
(db/query-one conn [sql:create-page
id file-id name version ordering data])))
create-pages
(fn [conn owner-id project-id file-id]
(log/info "create pages")
(p/run! (partial create-page conn owner-id project-id file-id)
(range (:num-pages-per-file opts))))
create-file
(fn [conn owner-id project-id index]
(p/let [id (mk-uuid "file" project-id index)
name (str "file" index)]
(log/info "create file" id)
(db/query-one conn [sql:create-file id project-id name])
(db/query-one conn [sql:create-file-profile
id owner-id true true true])
id))
create-files
(fn [conn owner-id project-id]
(log/info "create files")
(p/let [file-ids (collect (partial create-file conn owner-id project-id)
(range (:num-files-per-project opts)))]
(p/run! (partial create-pages conn owner-id project-id) file-ids)))
create-project
(fn [conn team-id owner-id index]
(p/let [id (mk-uuid "project" team-id index)
name (str "project " index)]
(log/info "create project" id)
(db/query-one conn [sql:create-project id team-id name])
(db/query-one conn [sql:create-project-profile
id owner-id true true true])
id))
create-projects
(fn [conn team-id profile-ids]
(log/info "create projects")
(p/let [owner-id (rng-nth rng profile-ids)
project-ids (collect (partial create-project conn team-id owner-id)
(range (:num-projects-per-team opts)))]
(p/run! (partial create-files conn owner-id) project-ids)))
assign-profile-to-team
(fn [conn team-id owner? profile-id]
(let [sql sql:create-team-profile]
(db/query-one conn [sql team-id profile-id owner? true true])))
setup-team
(fn [conn team-id profile-ids]
(log/info "setup team" team-id profile-ids)
(p/do!
(assign-profile-to-team conn team-id true (first profile-ids))
(p/run! (partial assign-profile-to-team conn team-id false)
(rest profile-ids))
(create-projects conn team-id profile-ids)))
assign-teams-and-profiles
(fn [conn teams profiles]
(log/info "assign teams and profiles")
(vu/loop [team-id (first teams)
teams (rest teams)]
(when-not (nil? team-id)
(p/let [n-profiles-team (:num-profiles-per-team opts)
selected-profiles (rng-vec rng profiles n-profiles-team)]
(setup-team conn team-id selected-profiles)
(p/recur (first teams)
(rest teams))))))
create-draft-pages
(fn [conn owner-id file-id]
(log/info "create draft pages")
(p/run! (partial create-page conn owner-id nil file-id)
(range (:num-draft-pages-per-file opts))))
create-draft-file
(fn [conn owner index]
(p/let [owner-id (:id owner)
id (mk-uuid "file" "draft" owner-id index)
name (str "file" index)
project-id (:id (:default-project owner))]
(log/info "create draft file" id)
(db/query-one conn [sql:create-file id project-id name])
(db/query-one conn [sql:create-file-profile
id owner-id true true true])
id))
create-draft-files
(fn [conn profile]
(p/let [file-ids (collect (partial create-draft-file conn profile)
(range (:num-draft-files-per-profile opts)))]
(p/run! (partial create-draft-pages conn (:id profile)) file-ids)))
]
(db/with-atomic [conn db/pool]
(p/let [profiles (create-profiles conn)
teams (create-teams conn)]
(assign-teams-and-profiles conn teams (map :id profiles))
(p/run! (partial create-draft-files conn) profiles)))))
(defn -main (defn -main
[& args] [& args]
@ -190,8 +272,8 @@
(mount/start)) (mount/start))
(let [preset (case (first args) (let [preset (case (first args)
(nil "small") preset-small (nil "small") preset-small
"medium" preset-medium ;; "medium" preset-medium
"big" preset-big ;; "big" preset-big
preset-small)] preset-small)]
(log/info "Using preset:" (pr-str preset)) (log/info "Using preset:" (pr-str preset))
(deref (run preset))) (deref (run preset)))

View File

@ -34,8 +34,8 @@
(let [type (keyword (get-in req [:path-params :type])) (let [type (keyword (get-in req [:path-params :type]))
data (merge (:params req) data (merge (:params req)
{::sq/type type {::sq/type type
:user (:user req)})] :profile-id (:profile-id req)})]
(if (or (:user req) (if (or (:profile-id req)
(isa? query-types-hierarchy type ::unauthenticated)) (isa? query-types-hierarchy type ::unauthenticated))
(-> (sq/handle (with-meta data {:req req})) (-> (sq/handle (with-meta data {:req req}))
(p/then' (fn [result] (p/then' (fn [result]
@ -52,8 +52,8 @@
(:body-params req) (:body-params req)
(:uploads req) (:uploads req)
{::sm/type type {::sm/type type
:user (:user req)})] :profile-id (:profile-id req)})]
(if (or (:user req) (if (or (:profile-id req)
(isa? mutation-types-hierarchy type ::unauthenticated)) (isa? mutation-types-hierarchy type ::unauthenticated))
(-> (sm/handle (with-meta data {:req req})) (-> (sm/handle (with-meta data {:req req}))
(p/then' (fn [result] (p/then' (fn [result]
@ -66,12 +66,11 @@
[req] [req]
(let [data (:body-params req) (let [data (:body-params req)
user-agent (get-in req [:headers "user-agent"])] user-agent (get-in req [:headers "user-agent"])]
(-> (sm/handle (assoc data ::sm/type :login)) (p/let [profile (sm/handle (assoc data ::sm/type :login))
(p/then #(session/create (:id %) user-agent)) token (session/create (:id profile) user-agent)]
(p/then' (fn [token] {:status 200
{:status 204 :cookies {"auth-token" {:value token :path "/"}}
:cookies {"auth-token" {:value token :path "/"}} :body profile})))
:body ""})))))
(defn logout-handler (defn logout-handler
[req] [req]
@ -83,22 +82,10 @@
:cookies {"auth-token" nil} :cookies {"auth-token" nil}
:body ""}))))) :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 (defn echo-handler
[req] [req]
{:status 200 (p/promise {:status 200
:body {:params (:params req) :body {:params (:params req)
:cookies (:cookies 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." "Retrieves a user id associated with the provided auth token."
[token] [token]
(when 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]) (-> (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 (defn create
[user-id user-agent] [user-id user-agent]
(let [id (uuid/random) (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]) (-> (db/query-one db/pool [sql id user-id user-agent])
(p/then (constantly (str id)))))) (p/then (constantly (str id))))))
(defn delete (defn delete
[token] [token]
(let [sql "delete from sessions where id = $1"] (let [sql "delete from session where id = $1"]
(-> (db/query-one db/pool [sql token]) (-> (db/query-one db/pool [sql token])
(p/then' (constantly nil))))) (p/then' (constantly nil)))))
;; --- Interceptor ;; --- Interceptor
(defn parse-token (defn- parse-token
[request] [request]
(try (try
(when-let [token (get-in request [:cookies "auth-token"])] (when-let [token (get-in request [:cookies "auth-token"])]

View File

@ -37,8 +37,7 @@
;; --- State Management ;; --- State Management
(defonce state (def state (atom {}))
(atom {}))
(defn send! (defn send!
[{:keys [output] :as ws} message] [{:keys [output] :as ws} message]
@ -50,15 +49,15 @@
(fn [ws message] (:type message))) (fn [ws message] (:type message)))
(defmethod handle-message :connect (defmethod handle-message :connect
[{:keys [file-id user-id] :as ws} message] [{:keys [file-id profile-id] :as ws} message]
(let [local (swap! state assoc-in [file-id user-id] ws) (let [local (swap! state assoc-in [file-id profile-id] ws)
sessions (get local file-id) sessions (get local file-id)
message {:type :who :users (set (keys sessions))}] message {:type :who :users (set (keys sessions))}]
(p/run! #(send! % message) (vals sessions)))) (p/run! #(send! % message) (vals sessions))))
(defmethod handle-message :disconnect (defmethod handle-message :disconnect
[{:keys [user-id] :as ws} {:keys [file-id] :as message}] [{:keys [profile-id] :as ws} {:keys [file-id] :as message}]
(let [local (swap! state update file-id dissoc user-id) (let [local (swap! state update file-id dissoc profile-id)
sessions (get local file-id) sessions (get local file-id)
message {:type :who :users (set (keys sessions))}] message {:type :who :users (set (keys sessions))}]
(p/run! #(send! % message) (vals sessions)))) (p/run! #(send! % message) (vals sessions))))
@ -69,14 +68,14 @@
(send! ws {:type :who :users (set users)}))) (send! ws {:type :who :users (set users)})))
(defmethod handle-message :pointer-update (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)) (let [sessions (->> (vals (get @state file-id))
(remove #(= user-id (:user-id %)))) (remove #(= profile-id (:profile-id %))))
message (assoc message :user-id user-id)] message (assoc message :profile-id profile-id)]
(p/run! #(send! % message) sessions))) (p/run! #(send! % message) sessions)))
(defn- on-eventbus-message (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)) (send! ws body))
(defn- start-eventbus-consumer! (defn- start-eventbus-consumer!
@ -90,9 +89,9 @@
[ws req] [ws req]
(let [ctx (vu/current-context) (let [ctx (vu/current-context)
file-id (get-in req [:path-params :file-id]) file-id (get-in req [:path-params :file-id])
user-id (:user req) profile-id (:profile-id req)
ws (assoc ws ws (assoc ws
:user-id user-id :profile-id profile-id
:file-id file-id) :file-id file-id)
send-ping #(send! ws {:type :ping}) send-ping #(send! ws {:type :ping})
sem1 (start-eventbus-consumer! ctx ws file-id) sem1 (start-eventbus-consumer! ctx ws file-id)

View File

@ -35,6 +35,9 @@
{:desc "Initial icons tables" {:desc "Initial icons tables"
:name "0006-icons" :name "0006-icons"
:fn (mg/resource "migrations/0006.icons.sql")} :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.icons)
(require 'uxbox.services.queries.images) (require 'uxbox.services.queries.images)
(require 'uxbox.services.queries.colors)
(require 'uxbox.services.queries.projects) (require 'uxbox.services.queries.projects)
(require 'uxbox.services.queries.project-files) (require 'uxbox.services.queries.files)
(require 'uxbox.services.queries.project-pages) (require 'uxbox.services.queries.pages)
(require 'uxbox.services.queries.profile) (require 'uxbox.services.queries.profile)
(require 'uxbox.services.queries.user-attrs)) ;; (require 'uxbox.services.queries.user-attrs)
)
(defn- load-mutation-services (defn- load-mutation-services
[] []
(require 'uxbox.services.mutations.demo) (require 'uxbox.services.mutations.demo)
(require 'uxbox.services.mutations.icons) (require 'uxbox.services.mutations.icons)
(require 'uxbox.services.mutations.images) (require 'uxbox.services.mutations.images)
(require 'uxbox.services.mutations.colors)
(require 'uxbox.services.mutations.projects) (require 'uxbox.services.mutations.projects)
(require 'uxbox.services.mutations.project-files) (require 'uxbox.services.mutations.files)
(require 'uxbox.services.mutations.project-pages) (require 'uxbox.services.mutations.pages)
(require 'uxbox.services.mutations.profile) (require 'uxbox.services.mutations.profile)
(require 'uxbox.services.mutations.user-attrs)) ;; (require 'uxbox.services.mutations.user-attrs)
)
(defstate query-services (defstate query-services
:start (load-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." "A demo specific mutations."
(:require (:require
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[datoteka.core :as fs]
[datoteka.storages :as ds]
[promesa.core :as p]
[promesa.exec :as px]
[sodi.prng] [sodi.prng]
[sodi.pwhash]
[sodi.util] [sodi.util]
[uxbox.common.exceptions :as ex] [uxbox.common.exceptions :as ex]
[uxbox.common.spec :as us]
[uxbox.config :as cfg] [uxbox.config :as cfg]
[uxbox.db :as db] [uxbox.db :as db]
[uxbox.emails :as emails]
[uxbox.images :as images]
[uxbox.media :as media]
[uxbox.services.mutations :as sm] [uxbox.services.mutations :as sm]
[uxbox.services.util :as su]
[uxbox.services.mutations.profile :as profile] [uxbox.services.mutations.profile :as profile]
[uxbox.tasks :as tasks] [uxbox.tasks :as tasks]
[uxbox.util.blob :as blob]
[uxbox.util.uuid :as uuid] [uxbox.util.uuid :as uuid]
[uxbox.util.time :as tm] [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)")
(sm/defmutation ::create-demo-profile (sm/defmutation ::create-demo-profile
[_] [_]
@ -49,15 +29,17 @@
email (str "demo-" sem ".demo@nodomain.com") email (str "demo-" sem ".demo@nodomain.com")
fullname (str "Demo User " sem) fullname (str "Demo User " sem)
password (-> (sodi.prng/random-bytes 12) password (-> (sodi.prng/random-bytes 12)
(sodi.util/bytes->b64s)) (sodi.util/bytes->b64s))]
password' (sodi.pwhash/derive password)]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(db/query-one conn [sql:insert-user id fullname email password']) (#'profile/register-profile conn {:id id
(db/query-one conn [sql:insert-email id email]) :email email
:fullname fullname
:demo? true
:password password})
;; Schedule deletion of the demo profile ;; Schedule deletion of the demo profile
(tasks/schedule! conn {:name "remove-demo-profile" (tasks/schedule! conn {:name "delete-profile"
:delay (tm/duration {:hours 48}) :delay cfg/default-deletion-delay
:props {:id id}}) :props {:profile-id id}})
{:email email {:email email
:password password}))) :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 ;; 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/. ;; 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> ;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.services.mutations.icons (ns uxbox.services.mutations.icons
(:require (:require
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[datoteka.core :as fs]
[datoteka.storages :as ds]
[promesa.core :as p] [promesa.core :as p]
[uxbox.db :as db] [promesa.exec :as px]
[uxbox.common.exceptions :as ex]
[uxbox.common.spec :as us] [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.mutations :as sm]
[uxbox.services.util :as su] [uxbox.services.util :as su]
[uxbox.services.queries.icons :refer [decode-icon-row]]
[uxbox.util.blob :as blob] [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 ;; --- Helpers & Specs
(s/def ::id ::us/uuid) (s/def ::id ::us/uuid)
(s/def ::name ::us/string) (s/def ::name ::us/string)
(s/def ::user ::us/uuid) (s/def ::profile-id ::us/uuid)
(s/def ::collection-id (s/nilable ::us/uuid)) (s/def ::collection-id ::us/uuid)
(s/def ::width ::us/integer) (s/def ::width ::us/integer)
(s/def ::height ::us/integer) (s/def ::height ::us/integer)
@ -36,128 +50,188 @@
(s/def ::metadata (s/def ::metadata
(s/keys :opt-un [::width ::height ::view-box ::mimetype])) (s/keys :opt-un [::width ::height ::view-box ::mimetype]))
;; --- Mutation: Create Collection ;; --- Mutation: Create Collection
(s/def ::create-icons-collection (declare create-icon-collection)
(s/keys :req-un [::user ::name]
(s/def ::create-icon-collection
(s/keys :req-un [::profile-id ::name]
:opt-un [::id])) :opt-un [::id]))
(sm/defmutation ::create-icons-collection (sm/defmutation ::create-icon-collection
[{:keys [id user name] :as params}] [{:keys [id profile-id name] :as params}]
(let [id (or id (uuid/next)) (db/with-atomic [conn db/pool]
sql "insert into icon_collections (id, user_id, name) (create-icon-collection conn params)))
values ($1, $2, $3) returning *"]
(db/query-one db/pool [sql id user name]))) (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 ;; --- Mutation: Update Collection
(s/def ::update-icons-collection (def ^:private sql:rename-collection
(s/keys :req-un [::user ::name ::id])) "update icon_collection
set name = $2
where id = $1
returning *")
(sm/defmutation ::update-icons-collection (s/def ::rename-icon-collection
[{:keys [id user name] :as params}] (s/keys :req-un [::profile-id ::name ::id]))
(let [sql "update icon_collections
set name = $3
where id = $1
and user_id = $2
returning *"]
(-> (db/query-one db/pool [sql id user name])
(p/then' su/raise-not-found-if-nil))))
;; --- Copy Icon (sm/defmutation ::rename-icon-collection
[{:keys [id profile-id name] :as params}]
(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}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(-> (retrieve-icon conn {:user user :id id}) (check-collection-edition-permissions! conn profile-id id)
(p/then (fn [icon] (db/query-one conn [sql:rename-collection id name])))
(let [icon (-> (dissoc icon :id)
(assoc :collection-id collection-id))]
(create-icon conn icon)))))))
;; ;; --- 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 ;; --- Delete Collection
(s/def ::delete-icons-collection (def ^:private sql:mark-collection-deleted
(s/keys :req-un [::user ::id])) "update icon_collection
set deleted_at = clock_timestamp()
where id = $1
returning id")
(sm/defmutation ::delete-icons-collection (s/def ::delete-icon-collection
[{:keys [user id] :as params}] (s/keys :req-un [::profile-id ::id]))
(let [sql "update icon_collections
set deleted_at = clock_timestamp() (sm/defmutation ::delete-icon-collection
where id = $1 [{:keys [profile-id id] :as params}]
and user_id = $2 (db/with-atomic [conn db/pool]
returning id"] (check-collection-edition-permissions! conn profile-id id)
(-> (db/query-one db/pool [sql id user]) (-> (db/query-one conn [sql:mark-collection-deleted id])
(p/then' su/raise-not-found-if-nil)
(p/then' su/constantly-nil)))) (p/then' su/constantly-nil))))
;; --- Mutation: Create Icon (Upload) ;; --- Mutation: Create Icon (Upload)
(def ^:private create-icon-sql (declare create-icon)
"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))))
(s/def ::create-icon (s/def ::create-icon
(s/keys :req-un [::user ::name ::metadata ::content] (s/keys :req-un [::profile-id ::name ::metadata ::content ::collection-id]
:opt-un [::id ::collection-id])) :opt-un [::id]))
(sm/defmutation ::create-icon (sm/defmutation ::create-icon
[params] [{:keys [profile-id collection-id] :as params}]
(create-icon db/pool 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 ;; --- 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/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 (sm/defmutation ::update-icon
[{:keys [id name user collection-id] :as params}] [{:keys [id name profile-id collection-id] :as params}]
(let [sql "update icons (db/with-atomic [conn db/pool]
set name = $1, (check-collection-edition-permissions! conn profile-id collection-id)
collection_id = $2 (-> (db/query-one db/pool [sql:update-icon id profile-id name collection-id])
where id = $3
and user_id = $4
returning *"]
(-> (db/query-one db/pool [sql name collection-id id user])
(p/then' su/raise-not-found-if-nil)))) (p/then' su/raise-not-found-if-nil))))
;; --- Mutation: Delete Icon ;; --- Mutation: Delete Icon
(def ^:private sql:mark-icon-deleted
"update icon
set deleted_at = clock_timestamp()
where id = $1
and profile_id = $2
returning id")
(s/def ::delete-icon (s/def ::delete-icon
(s/keys :req-un [::user ::id])) (s/keys :req-un [::profile-id ::id]))
(sm/defmutation ::delete-icon (sm/defmutation ::delete-icon
[{:keys [id user] :as params}] [{:keys [id profile-id] :as params}]
(let [sql "update icons (db/with-atomic [conn db/pool]
set deleted_at = clock_timestamp() (-> (db/query-one conn [sql:mark-icon-deleted id profile-id])
where id = $1 (p/then' su/raise-not-found-if-nil))
and user_id = $2
returning id"] ;; Schedule object deletion
(-> (db/query-one db/pool [sql id user]) (tasks/schedule! conn {:name "delete-object"
(p/then' su/raise-not-found-if-nil) :delay cfg/default-deletion-delay
(p/then' su/constantly-nil)))) :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 ;; 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/. ;; 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> ;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.services.mutations.images (ns uxbox.services.mutations.images
@ -13,9 +16,11 @@
[promesa.exec :as px] [promesa.exec :as px]
[uxbox.common.exceptions :as ex] [uxbox.common.exceptions :as ex]
[uxbox.common.spec :as us] [uxbox.common.spec :as us]
[uxbox.config :as cfg]
[uxbox.db :as db] [uxbox.db :as db]
[uxbox.media :as media] [uxbox.media :as media]
[uxbox.images :as images] [uxbox.images :as images]
[uxbox.tasks :as tasks]
[uxbox.services.mutations :as sm] [uxbox.services.mutations :as sm]
[uxbox.services.util :as su] [uxbox.services.util :as su]
[uxbox.util.blob :as blob] [uxbox.util.blob :as blob]
@ -32,69 +37,102 @@
(s/def ::id ::us/uuid) (s/def ::id ::us/uuid)
(s/def ::name ::us/string) (s/def ::name ::us/string)
(s/def ::user ::us/uuid) (s/def ::profile-id ::us/uuid)
(s/def ::collection-id ::us/uuid)
;; --- Create Collection ;; --- Create Collection
(declare create-images-collection) (declare create-image-collection)
(s/def ::create-images-collection (s/def ::create-image-collection
(s/keys :req-un [::user ::us/name] (s/keys :req-un [::profile-id ::name]
:opt-un [::id])) :opt-un [::id]))
(sm/defmutation ::create-images-collection (sm/defmutation ::create-image-collection
[{:keys [id user name] :as params}] [{:keys [id profile-id name] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(create-images-collection conn params))) (create-image-collection conn params)))
(defn create-images-collection (def ^:private sql:create-image-collection
[conn {:keys [id user name] :as params}] "insert into image_collection (id, profile_id, name)
(let [id (or id (uuid/next)) values ($1, $2, $3)
sql "insert into image_collections (id, user_id, name)
values ($1, $2, $3)
on conflict (id) do nothing
returning *;"]
(db/query-one db/pool [sql id user name])))
;; --- Update Collection
(def ^:private
sql:rename-images-collection
"update image_collections
set name = $3
where id = $1
and user_id = $2
returning *;") returning *;")
(s/def ::rename-images-collection (defn- create-image-collection
(s/keys :req-un [::id ::user ::us/name])) [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/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 ;; --- Delete Collection
(s/def ::delete-images-collection (s/def ::delete-image-collection
(s/keys :req-un [::user ::id])) (s/keys :req-un [::profile-id ::id]))
(def ^:private (def ^:private sql:mark-image-collection-as-deleted
sql:delete-images-collection "update image_collection
"update image_collections
set deleted_at = clock_timestamp() set deleted_at = clock_timestamp()
where id = $1 where id = $1
and user_id = $2
returning id") returning id")
(sm/defmutation ::delete-images-collection (sm/defmutation ::delete-image-collection
[{:keys [id user] :as params}] [{:keys [id profile-id] :as params}]
(-> (db/query-one db/pool [sql:delete-images-collection id user]) (db/with-atomic [conn db/pool]
(p/then' su/raise-not-found-if-nil))) (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) ;; --- Create Image (Upload)
(declare select-collection-for-update)
(declare create-image) (declare create-image)
(declare persist-image-on-fs) (declare persist-image-on-fs)
(declare persist-image-thumbnail-on-fs) (declare persist-image-thumbnail-on-fs)
@ -113,31 +151,27 @@
:uxbox$upload/path :uxbox$upload/path
:uxbox$upload/mtype])) :uxbox$upload/mtype]))
(s/def ::collection-id ::us/uuid)
(s/def ::content ::upload) (s/def ::content ::upload)
(s/def ::upload-image (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])) :opt-un [::id]))
(sm/defmutation ::upload-image (sm/defmutation ::upload-image
[{:keys [collection-id user] :as params}] [{:keys [collection-id profile-id] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [coll (select-collection-for-update conn collection-id)] (check-collection-edition-permissions! conn profile-id collection-id)
(when (not= (:user-id coll) user) (create-image conn params)))
(ex/raise :type :validation
:code :not-authorized))
(create-image conn params))))
(def ^:private sql:insert-image (def ^:private sql:insert-image
"insert into images "insert into image
(id, collection_id, user_id, name, path, width, height, mtype, (id, collection_id, profile_id, name, path, width, height, mtype,
thumb_path, thumb_width, thumb_height, thumb_quality, thumb_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) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
returning *") returning *")
(defn create-image (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)) (when-not (valid-image-types? (:mtype content))
(ex/raise :type :validation (ex/raise :type :validation
:code :image-type-not-allowed :code :image-type-not-allowed
@ -151,7 +185,7 @@
sqlv [sql:insert-image sqlv [sql:insert-image
id id
collection-id collection-id
user profile-id
name name
(str image-path) (str image-path)
(:width image-opts) (:width image-opts)
@ -167,16 +201,6 @@
(p/then' #(images/resolve-urls % :path :uri)) (p/then' #(images/resolve-urls % :path :uri))
(p/then' #(images/resolve-urls % :thumb-path :thumb-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 (defn persist-image-on-fs
[{:keys [name path] :as upload}] [{:keys [name path] :as upload}]
(vu/blocking (vu/blocking
@ -193,32 +217,37 @@
(str "thumbnail-" filename))] (str "thumbnail-" filename))]
(ust/save! media/media-storage thumb-name thumb-data)))) (ust/save! media/media-storage thumb-name thumb-data))))
;; --- Update Image ;; --- Update Image
(s/def ::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 (def ^:private sql:update-image
"update images "update image
set name = $3, set name = $3,
collection_id = $2 collection_id = $2
where id = $1 where id = $1
and user_id = $4 and profile_id = $4
returning *;") returning *;")
(sm/defmutation ::update-image (sm/defmutation ::update-image
[{:keys [id name user collection-id] :as params}] [{:keys [id name profile-id collection-id] :as params}]
(db/query-one db/pool [sql:update-image id collection-id name user])) (-> (db/query-one db/pool [sql:update-image id
collection-id name profile-id])
(p/then' su/raise-not-found-if-nil)))
;; --- Copy Image ;; --- Copy Image
(declare retrieve-image) ;; (declare retrieve-image)
;; (s/def ::copy-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 ;; (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}] ;; (letfn [(copy-image [conn {:keys [path] :as image}]
;; (-> (ds/lookup media/images-storage (:path image)) ;; (-> (ds/lookup media/images-storage (:path image))
;; (p/then (fn [path] (ds/save media/images-storage (fs/name path) path))) ;; (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))))] ;; (p/then (partial store-image-in-db conn))))]
;; (db/with-atomic [conn db/pool] ;; (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 su/raise-not-found-if-nil)
;; (p/then (partial copy-image conn)))))) ;; (p/then (partial copy-image conn))))))
;; --- Delete Image ;; --- Delete Image
;; TODO: this need to be performed in the GC process (def ^:private sql:mark-image-deleted
;; (defn- delete-image-from-storage "update image
;; [{:keys [path] :as image}] set deleted_at = clock_timestamp()
;; (when @(ds/exists? media/images-storage path) where id = $1
;; @(ds/delete media/images-storage path)) and profile_id = $2
;; (when @(ds/exists? media/thumbnails-storage path) returning id")
;; @(ds/delete media/thumbnails-storage path)))
(s/def ::delete-image (s/def ::delete-image
(s/keys :req-un [::id ::user])) (s/keys :req-un [::id ::profile-id]))
(sm/defmutation ::delete-image (sm/defmutation ::delete-image
[{:keys [user id] :as params}] [{:keys [profile-id id] :as params}]
(let [sql "update images (db/with-atomic [conn db/pool]
set deleted_at = clock_timestamp() (-> (db/query-one conn [sql:mark-image-deleted id profile-id])
where id = $1 (p/then' su/raise-not-found-if-nil))
and user_id = $2
returning *"] ;; Schedule object deletion
(db/query-one db/pool [sql id user]))) (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.media :as media]
[uxbox.services.mutations :as sm] [uxbox.services.mutations :as sm]
[uxbox.services.mutations.images :as imgs] [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.queries.profile :as profile]
[uxbox.services.util :as su] [uxbox.services.util :as su]
[uxbox.util.blob :as blob] [uxbox.util.blob :as blob]
@ -40,14 +42,14 @@
(s/def ::fullname ::us/string) (s/def ::fullname ::us/string)
(s/def ::lang ::us/string) (s/def ::lang ::us/string)
(s/def ::path ::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 ::password ::us/string)
(s/def ::old-password ::us/string) (s/def ::old-password ::us/string)
;; --- Mutation: Login ;; --- Mutation: Login
(declare retrieve-user-by-email) (declare retrieve-profile-by-email)
(s/def ::email ::us/email) (s/def ::email ::us/email)
(s/def ::scope ::us/string) (s/def ::scope ::us/string)
@ -58,31 +60,34 @@
(sm/defmutation ::login (sm/defmutation ::login
[{:keys [email password scope] :as params}] [{:keys [email password scope] :as params}]
(letfn [(check-password [user password] (letfn [(check-password [profile password]
(let [result (sodi.pwhash/verify password (:password user))] (let [result (sodi.pwhash/verify password (:password profile))]
(:valid result))) (:valid result)))
(check-user [user] (check-profile [profile]
(when-not user (when-not profile
(ex/raise :type :validation (ex/raise :type :validation
:code ::wrong-credentials)) :code ::wrong-credentials))
(when-not (check-password user password) (when-not (check-password profile password)
(ex/raise :type :validation (ex/raise :type :validation
:code ::wrong-credentials)) :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)})] (def sql:profile-by-email
(-> (retrieve-user-by-email db/pool email)
(p/then' check-user))))
(def sql:user-by-email
"select u.* "select u.*
from users as u from profile as u
where u.email=$1 where u.email=$1
and u.deleted_at is null") and u.deleted_at is null")
(defn- retrieve-user-by-email (defn- retrieve-profile-by-email
[conn 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 ;; --- Mutation: Add additional email
@ -97,7 +102,7 @@
;; --- Mutation: Update Profile (own) ;; --- Mutation: Update Profile (own)
(def ^:private sql:update-profile (def ^:private sql:update-profile
"update users "update profile
set fullname = $2, set fullname = $2,
lang = $3 lang = $3
where id = $1 where id = $1
@ -123,27 +128,27 @@
;; --- Mutation: Update Password ;; --- Mutation: Update Password
(defn- validate-password! (defn- validate-password!
[conn {:keys [user old-password] :as params}] [conn {:keys [profile-id old-password] :as params}]
(p/let [profile (profile/retrieve-profile conn user) (p/let [profile (profile/retrieve-profile conn profile-id)
result (sodi.pwhash/verify old-password (:password profile))] result (sodi.pwhash/verify old-password (:password profile))]
(when-not (:valid result) (when-not (:valid result)
(ex/raise :type :validation (ex/raise :type :validation
:code ::old-password-not-match)))) :code ::old-password-not-match))))
(defn update-password (defn update-password
[conn {:keys [user password]}] [conn {:keys [profile-id password]}]
(let [sql "update users (let [sql "update profile
set password = $2 set password = $2
where id = $1 where id = $1
and deleted_at is null and deleted_at is null
returning id" returning id"
password (sodi.pwhash/derive password)] 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/raise-not-found-if-nil)
(p/then' su/constantly-nil)))) (p/then' su/constantly-nil))))
(s/def ::update-profile-password (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 (sm/defmutation ::update-profile-password
[params] [params]
@ -159,22 +164,22 @@
(s/def ::file ::imgs/upload) (s/def ::file ::imgs/upload)
(s/def ::update-profile-photo (s/def ::update-profile-photo
(s/keys :req-un [::user ::file])) (s/keys :req-un [::profile-id ::file]))
(sm/defmutation ::update-profile-photo (sm/defmutation ::update-profile-photo
[{:keys [user file] :as params}] [{:keys [profile-id file] :as params}]
(db/with-atomic [conn db/pool] (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)] photo (upload-photo conn params)]
;; Schedule deletion of old photo ;; Schedule deletion of old photo
(tasks/schedule! conn {:name "remove-media" (tasks/schedule! conn {:name "remove-media"
:props {:path (:photo profile)}}) :props {:path (:photo profile)}})
;; Save new photo ;; Save new photo
(update-profile-photo conn user photo)))) (update-profile-photo conn profile-id photo))))
(defn- upload-photo (defn- upload-photo
[conn {:keys [file user]}] [conn {:keys [file profile-id]}]
(when-not (imgs/valid-image-types? (:mtype file)) (when-not (imgs/valid-image-types? (:mtype file))
(ex/raise :type :validation (ex/raise :type :validation
:code :image-type-not-allowed :code :image-type-not-allowed
@ -191,12 +196,16 @@
(ust/save! media/media-storage name photo)))) (ust/save! media/media-storage name photo))))
(defn- update-profile-photo (defn- update-profile-photo
[conn user path] [conn profile-id path]
(let [sql "update users set photo=$1 where id=$2 and deleted_at is null returning id"] (let [sql "update profile set photo=$1
(-> (db/query-one conn [sql (str path) user]) 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)))) (p/then' su/raise-not-found-if-nil))))
;; --- Mutation: Register Profile ;; --- Mutation: Register Profile
(declare check-profile-existence!) (declare check-profile-existence!)
@ -212,18 +221,26 @@
:code :registration-disabled)) :code :registration-disabled))
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(check-profile-existence! conn params) (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) (def ^:private sql:insert-profile
values ($1, $2, $3, $4, '') returning *") "insert into profile (id, fullname, email, password, photo, is_demo)
values ($1, $2, $3, $4, '', $5) returning *")
(def ^:private sql:insert-email (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)") values ($1, $2, true)")
(def ^:private sql:profile-existence (def ^:private sql:profile-existence
"select exists (select * from users "select exists (select * from profile
where email = $1 where email = $1
and deleted_at is null) as val") and deleted_at is null) as val")
@ -236,33 +253,40 @@
:code ::email-already-exists)) :code ::email-already-exists))
params)))) params))))
(defn create-profile (defn- create-profile
"Create the user entry on the database with limited input "Create the profile entry on the database with limited input
filling all the other fields with defaults." 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)) (let [id (or id (uuid/next))
password (sodi.pwhash/derive password) demo? (if (boolean? demo?) demo? false)
sqlv1 [sql:insert-user password (sodi.pwhash/derive password)]
id (db/query-one conn [sql:insert-profile id fullname email password demo?])))
fullname
email (defn- create-profile-email
password] [conn {:keys [id email] :as profile}]
sqlv2 [sql:insert-email id email]] (-> (db/query-one conn [sql:insert-email id email])
(p/let [profile (db/query-one conn sqlv1)] (p/then' su/constantly-nil)))
(db/query-one conn sqlv2)
profile)))
(defn register-profile (defn register-profile
[conn params] [conn params]
(-> (create-profile conn params) (p/let [prof (create-profile conn params)
(p/then' profile/strip-private-attrs) _ (create-profile-email conn prof)
(p/then (fn [profile]
;; TODO: send a correct link for email verification team (mt.teams/create-team conn {:profile-id (:id prof)
(let [data {:to (:email params) :name "Default"
:name (:fullname params)}] :default? true})
(p/do! _ (mt.teams/create-team-profile conn {:team-id (:id team)
(emails/send! conn emails/register data) :profile-id (:id prof)})
profile))))))
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 ;; --- Mutation: Request Profile Recovery
@ -270,24 +294,24 @@
(s/keys :req-un [::email])) (s/keys :req-un [::email]))
(def sql:insert-recovery-token (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 (sm/defmutation ::request-profile-recovery
[{:keys [email] :as params}] [{: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) (let [token (-> (sodi.prng/random-bytes 32)
(sodi.util/bytes->b64s)) (sodi.util/bytes->b64s))
sql sql:insert-recovery-token] sql sql:insert-recovery-token]
(-> (db/query-one conn [sql id token]) (-> (db/query-one conn [sql id token])
(p/then (constantly (assoc user :token token)))))) (p/then (constantly (assoc profile :token token))))))
(send-email-notification [conn user] (send-email-notification [conn profile]
(emails/send! conn (emails/send! conn
emails/password-recovery emails/password-recovery
{:to (:email user) {:to (:email profile)
:token (:token user) :token (:token profile)
:name (:fullname user)}))] :name (:fullname profile)}))]
(db/with-atomic [conn db/pool] (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' su/raise-not-found-if-nil)
(p/then #(create-recovery-token conn %)) (p/then #(create-recovery-token conn %))
(p/then #(send-email-notification conn %)) (p/then #(send-email-notification conn %))
@ -300,23 +324,77 @@
(s/keys :req-un [::token ::password])) (s/keys :req-un [::token ::password]))
(def sql:remove-recovery-token (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 (sm/defmutation ::recover-profile
[{:keys [token password]}] [{:keys [token password]}]
(letfn [(validate-token [conn token] (letfn [(validate-token [conn token]
(let [sql "delete from tokens where token=$1 returning *" (let [sql "delete from password_recovery_token
sql "select * from tokens where token=$1"] where token=$1 returning *"
sql "select * from password_recovery_token
where token=$1"]
(-> (db/query-one conn [sql token]) (-> (db/query-one conn [sql token])
(p/then' :user-id) (p/then' :profile-id)
(p/then' su/raise-not-found-if-nil)))) (p/then' su/raise-not-found-if-nil))))
(update-password [conn user-id] (update-password [conn profile-id]
(let [sql "update users set password=$2 where id=$1" (let [sql "update profile set password=$2 where id=$1"
pwd (sodi.pwhash/derive password)] 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)))))] (p/then' (constantly nil)))))]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(-> (validate-token conn token) (-> (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 ;; 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/. ;; 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 (ns uxbox.services.mutations.projects
(:require (:require
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[promesa.core :as p] [promesa.core :as p]
[uxbox.config :as cfg]
[uxbox.db :as db] [uxbox.db :as db]
[uxbox.common.exceptions :as ex] [uxbox.common.exceptions :as ex]
[uxbox.common.spec :as us] [uxbox.common.spec :as us]
[uxbox.tasks :as tasks]
[uxbox.services.mutations :as sm] [uxbox.services.mutations :as sm]
[uxbox.services.util :as su] [uxbox.services.util :as su]
[uxbox.util.blob :as blob] [uxbox.util.blob :as blob]
@ -20,65 +25,95 @@
(s/def ::id ::us/uuid) (s/def ::id ::us/uuid)
(s/def ::name ::us/string) (s/def ::name ::us/string)
(s/def ::token ::us/string) (s/def ::profile-id ::us/uuid)
(s/def ::user ::us/uuid)
;; --- Permissions Checks ;; --- Permissions Checks
(def ^:private sql:project-permissions (def ^:private sql:project-permissions
"select p.id, "select tpr.is_owner,
pu.can_edit as can_edit tpr.is_admin,
from projects as p tpr.can_edit
inner join project_users as pu from team_profile_rel as tpr
on (pu.project_id = p.id) inner join project as p on (p.team_id = tpr.team_id)
where pu.user_id = $1 where p.id = $1
and p.id = $2 and tpr.profile_id = $2
for update of p;") 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! (defn check-edition-permissions!
[conn user project-id] [conn profile-id project-id]
(-> (db/query-one conn [sql:project-permissions user 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' su/raise-not-found-if-nil)
(p/then' (fn [{:keys [id can-edit] :as proj}] (p/then' (fn [rows]
(when-not can-edit (when-not (or (some :can-edit rows)
(some :is-admin rows)
(some :is-owner rows))
(ex/raise :type :validation (ex/raise :type :validation
:code :not-authorized)))))) :code :not-authorized))))))
;; --- Mutation: Create Project ;; --- Mutation: Create Project
(declare create-project) (declare create-project)
(declare create-project-profile)
(s/def ::team-id ::us/uuid)
(s/def ::create-project (s/def ::create-project
(s/keys :req-un [::user ::name] (s/keys :req-un [::profile-id ::team-id ::name]
:opt-un [::id])) :opt-un [::id]))
(sm/defmutation ::create-project (sm/defmutation ::create-project
[params] [params]
(db/with-atomic [conn db/pool] (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 (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)) (let [id (or id (uuid/next))
sql "insert into projects (id, user_id, name) default? (if (boolean? default?) default? false)]
values ($1, $2, $3) returning *"] (db/query-one conn [sql:insert-project id team-id name default?])))
(db/query-one conn [sql id user name])))
;; --- 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) (declare rename-project)
(s/def ::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 (sm/defmutation ::rename-project
[{:keys [id user] :as params}] [{:keys [id profile-id] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(check-edition-permissions! conn user id) (check-edition-permissions! conn profile-id id)
(rename-project conn params))) (rename-project conn params)))
(def sql:rename-project (def ^:private sql:rename-project
"update projects "update project
set name = $2 set name = $2
where id = $1 where id = $1
and deleted_at is null and deleted_at is null
@ -86,31 +121,36 @@
(defn rename-project (defn rename-project
[conn {:keys [id name] :as params}] [conn {:keys [id name] :as params}]
(let [sql sql:rename-project] (db/query-one conn [sql:rename-project id name]))
(db/query-one conn [sql id name])))
;; --- Mutation: Delete Project ;; --- Mutation: Delete Project
(declare delete-project) (declare mark-project-deleted)
(s/def ::delete-project (s/def ::delete-project
(s/keys :req-un [::id ::user])) (s/keys :req-un [::id ::profile-id]))
(sm/defmutation ::delete-project (sm/defmutation ::delete-project
[{:keys [id user] :as params}] [{:keys [id profile-id] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(check-edition-permissions! conn user id) (check-edition-permissions! conn profile-id id)
(delete-project conn params)))
(def ^:private sql:delete-project ;; Schedule object deletion
"update projects (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() set deleted_at = clock_timestamp()
where id = $1 where id = $1
and deleted_at is null
returning id") returning id")
(defn delete-project (defn mark-project-deleted
[conn {:keys [id user] :as params}] [conn {:keys [id profile-id] :as params}]
(let [sql sql:delete-project] (-> (db/query-one conn [sql:mark-project-deleted id])
(-> (db/query-one conn [sql id]) (p/then' su/constantly-nil)))
(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 ;; 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/. ;; 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> ;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.services.queries.icons (ns uxbox.services.queries.icons
(:require (:require
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[promesa.core :as p] [promesa.core :as p]
[promesa.exec :as px]
[uxbox.common.exceptions :as ex] [uxbox.common.exceptions :as ex]
[uxbox.common.spec :as us] [uxbox.common.spec :as us]
[uxbox.db :as db] [uxbox.db :as db]
[uxbox.media :as media]
[uxbox.images :as images]
[uxbox.services.queries :as sq] [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 ;; --- Helpers & Specs
(s/def ::id ::us/uuid) (s/def ::id ::us/uuid)
(s/def ::user ::us/uuid) (s/def ::profile-id ::us/uuid)
(s/def ::collection-id (s/nilable ::us/uuid)) (s/def ::collection-id (s/nilable ::us/uuid))
(defn decode-icon-row (defn decode-row
[{:keys [metadata] :as row}] [{:keys [metadata] :as row}]
(when row (when row
(cond-> row (cond-> row
metadata (assoc :metadata (blob/decode metadata))))) metadata (assoc :metadata (blob/decode metadata)))))
;; --- Query: Collections ;; --- Query: Collections
(def sql:icons-collections (def ^:private sql:collections
"select *, "select *,
(select count(*) from icons where collection_id = ic.id) as num_icons (select count(*) from icon where collection_id = ic.id) as num_icons
from icon_collections as ic from icon_collection as ic
where (ic.user_id = $1 or where (ic.profile_id = $1 or
ic.user_id = '00000000-0000-0000-0000-000000000000'::uuid) ic.profile_id = '00000000-0000-0000-0000-000000000000'::uuid)
and ic.deleted_at is null and ic.deleted_at is null
order by ic.created_at desc") order by ic.created_at desc")
(s/def ::icons-collections (s/def ::icon-collections
(s/keys :req-un [::user])) (s/keys :req-un [::profile-id]))
(sq/defquery ::icons-collections (sq/defquery ::icon-collections
[{:keys [user] :as params}] [{:keys [profile-id] :as params}]
(let [sqlv [sql:icons-collections user]] (let [sqlv [sql:collections profile-id]]
(db/query db/pool sqlv))) (db/query db/pool sqlv)))
;; --- Icons By Collection ID ;; --- Icons By Collection ID
(def ^:private icons-by-collection-sql (def ^:private sql:icons
"select * "select *
from icons as i from icon as i
where (i.user_id = $1 or where (i.profile_id = $1 or
i.user_id = '00000000-0000-0000-0000-000000000000'::uuid) i.profile_id = '00000000-0000-0000-0000-000000000000'::uuid)
and i.deleted_at is null and i.deleted_at is null
and case when $2::uuid is null then i.collection_id is null and i.collection_id = $2
else i.collection_id = $2::uuid
end
order by i.created_at desc") order by i.created_at desc")
(s/def ::icons-by-collection (s/def ::icons
(s/keys :req-un [::user] (s/keys :req-un [::profile-id ::collection-id]))
:opt-un [::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 ;; 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/. ;; 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> ;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.services.queries.images (ns uxbox.services.queries.images
@ -23,73 +26,74 @@
(s/def ::id ::us/uuid) (s/def ::id ::us/uuid)
(s/def ::name ::us/string) (s/def ::name ::us/string)
(s/def ::user ::us/uuid) (s/def ::profile-id ::us/uuid)
(s/def ::collection-id (s/nilable ::us/uuid)) (s/def ::collection-id (s/nilable ::us/uuid))
;; --- Query: Images Collections ;; --- Query: Image Collections
(def ^:private sql:collections (def ^:private sql:collections
"select *, "select *,
(select count(*) from images where collection_id = ic.id) as num_images (select count(*) from image where collection_id = ic.id) as num_images
from image_collections as ic from image_collection as ic
where (ic.user_id = $1 or where (ic.profile_id = $1 or
ic.user_id = '00000000-0000-0000-0000-000000000000'::uuid) ic.profile_id = '00000000-0000-0000-0000-000000000000'::uuid)
and ic.deleted_at is null and ic.deleted_at is null
order by ic.created_at desc;") order by ic.created_at desc;")
(s/def ::images-collections (s/def ::image-collections
(s/keys :req-un [::user])) (s/keys :req-un [::profile-id]))
(sq/defquery ::images-collections (sq/defquery ::image-collections
[{:keys [user] :as params}] [{:keys [profile-id] :as params}]
(db/query db/pool [sql:collections user])) (db/query db/pool [sql:collections profile-id]))
;; --- Query: Image by ID
(defn retrieve-image ;; --- Query: Image (by ID)
[conn id]
(let [sql "select * from images (declare retrieve-image)
where id = $1
and deleted_at is null;"]
(db/query-one conn [sql id])))
(s/def ::id ::us/uuid) (s/def ::id ::us/uuid)
(s/def ::image-by-id (s/def ::image
(s/keys :req-un [::user ::id])) (s/keys :req-un [::profile-id ::id]))
(sq/defquery ::image-by-id (sq/defquery ::image
[params] [{:keys [id] :as params}]
(-> (retrieve-image db/pool (:id params)) (-> (retrieve-image db/pool id)
(p/then' #(images/resolve-urls % :path :uri)) (p/then' #(images/resolve-urls % :path :uri))
(p/then' #(images/resolve-urls % :thumb-path :thumb-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 ;; --- Query: Images (by collection)
user_id = '00000000-0000-0000-0000-000000000000'::uuid)
(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 deleted_at is null
and collection_id = $2
order by created_at desc") order by created_at desc")
(def sql:images-by-collection (s/def ::images
(str "with images as (" sql:images-by-collection ") (s/keys :req-un [::profile-id ::collection-id]))
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]))
;; TODO: check if we can resolve url with transducer for reduce ;; TODO: check if we can resolve url with transducer for reduce
;; garbage generation for each request ;; garbage generation for each request
(sq/defquery ::images-by-collection (sq/defquery ::images
[{:keys [user collection-id] :as params}] [{:keys [profile-id collection-id] :as params}]
(let [sqlv [sql:images-by-collection user collection-id]] (-> (db/query db/pool [sql:images profile-id collection-id])
(-> (db/query db/pool sqlv) (p/then' (fn [rows]
(p/then' (fn [rows] (->> rows
(->> rows (mapv #(images/resolve-urls % :path :uri))
(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 ::password ::us/string)
(s/def ::path ::us/string) (s/def ::path ::us/string)
(s/def ::user ::us/uuid) (s/def ::user ::us/uuid)
(s/def ::username ::us/string) (s/def ::profile-id ::us/uuid)
;; --- Query: Profile (own) ;; --- Query: Profile (own)
(defn retrieve-profile (defn retrieve-profile
[conn id] [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]))) (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/def ::profile
(s/keys :req-un [::user])) (s/keys :req-un [::profile-id]))
(sq/defquery ::profile (sq/defquery ::profile
[{:keys [user] :as params}] [{:keys [profile-id] :as params}]
(-> (retrieve-profile db/pool user) (db/with-atomic [conn db/pool]
(p/then' strip-private-attrs) (p/let [prof (-> (retrieve-profile conn profile-id)
(p/then' #(images/resolve-media-uris % [:photo :photo-uri])))) (p/then' su/raise-not-found-if-nil)
(p/then' strip-private-attrs)
(p/then' #(images/resolve-media-uris % [:photo :photo-uri])))
addt (retrieve-additional-data conn profile-id)]
(merge prof addt))))
;; --- Attrs Helpers ;; --- Attrs Helpers
(defn strip-private-attrs (defn strip-private-attrs
"Only selects a publicy visible user attrs." "Only selects a publicy visible profile attrs."
[profile] [profile]
(select-keys profile [:id :fullname :lang :email :created-at :photo])) (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) (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 ;; --- Query: Projects
(def sql:projects (def ^:private sql:projects
"select p.* "with projects as (
from project_users as pu select p.*
inner join projects as p on (p.id = pu.project_id) from project as p
where pu.can_edit = true inner join team_profile_rel as tpr on (tpr.team_id = p.team_id)
and pu.user_id = $1 where tpr.profile_id = $1
order by p.created_at asc") 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/def ::team-id ::us/uuid)
(s/keys :req-un [::user])) (s/def ::profile-id ::us/uuid)
(sq/defquery ::projects (s/def ::projects-by-team
[{:keys [user] :as params}] (s/keys :req-un [::profile-id ::team-id]))
(-> (db/query db/pool [sql:projects user])
(p/then' (partial mapv decode-row))))
(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.uuid :as uuid]
[uxbox.util.dispatcher :as uds])) [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 (defn raise-not-found-if-nil
[v] [v]
(if (nil? v) (if (nil? v)

View File

@ -20,7 +20,8 @@
[uxbox.db :as db] [uxbox.db :as db]
[uxbox.tasks.sendmail] [uxbox.tasks.sendmail]
[uxbox.tasks.remove-media] [uxbox.tasks.remove-media]
[uxbox.tasks.remove-demo-profile] [uxbox.tasks.delete-profile]
[uxbox.tasks.delete-object]
[uxbox.tasks.impl :as impl] [uxbox.tasks.impl :as impl]
[uxbox.util.time :as dt] [uxbox.util.time :as dt]
[vertx.core :as vc] [vertx.core :as vc]
@ -42,7 +43,8 @@
;; need to perform a maintenance and delete some old tasks. ;; need to perform a maintenance and delete some old tasks.
(def ^:private 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 "remove-media" #'uxbox.tasks.remove-media/handler
"sendmail" #'uxbox.tasks.sendmail/handler}) "sendmail" #'uxbox.tasks.sendmail/handler})
@ -54,7 +56,7 @@
;; (def ^:private schedule ;; (def ^:private schedule
;; [{:id "every 1 hour" ;; [{:id "every 1 hour"
;; :cron (dt/cron "1 1 */1 * * ? *") ;; :cron (dt/cron "1 1 */1 * * ? *")
;; :fn #'uxbox.tasks.demo-gc/handler ;; :fn #'uxbox.tasks.gc/handler
;; :props {:foo 1}}]) ;; :props {:foo 1}}])
;; (defstate scheduler ;; (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.db :as db]
[uxbox.util.blob :as blob])) [uxbox.util.blob :as blob]))
;; TODO: add images-gc with proper resource removal ;; TODO: delete media referenced in pendint_to_delete table
;; TODO: add icons-gc
;; TODO: add pages-gc
;; TODO: test this
;; --- 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 ;; (defn- remove-items
"delete from projects ;; []
where id = $1 ;; (vu/loop []
and deleted_at is not null;") ;; (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) ;; (defn- remove-media
(s/def ::delete-project ;; [{:keys
(s/keys :req-un [::id])) ;; (doseq [item files]
;; (ust/delete! media/media-storage (:path item))
(defn- delete-project ;; (ust/delete! media/media-storage (:thumb-path item)))
"Clean deleted projects." ;; files)
[{: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))

View File

@ -38,7 +38,7 @@
(.printStackTrace err (java.io.PrintWriter. *out*)))) (.printStackTrace err (java.io.PrintWriter. *out*))))
(def ^:private sql:mark-as-retry (def ^:private sql:mark-as-retry
"update tasks "update task
set scheduled_at = clock_timestamp() + '5 seconds'::interval, set scheduled_at = clock_timestamp() + '5 seconds'::interval,
error = $1, error = $1,
status = 'retry', status = 'retry',
@ -53,7 +53,7 @@
(p/then' (constantly nil))))) (p/then' (constantly nil)))))
(def ^:private sql:mark-as-failed (def ^:private sql:mark-as-failed
"update tasks "update task
set scheduled_at = clock_timestamp() + '5 seconds'::interval, set scheduled_at = clock_timestamp() + '5 seconds'::interval,
error = $1, error = $1,
status = 'failed' status = 'failed'
@ -67,7 +67,7 @@
(p/then' (constantly nil))))) (p/then' (constantly nil)))))
(def ^:private sql:mark-as-completed (def ^:private sql:mark-as-completed
"update tasks "update task
set completed_at = clock_timestamp(), set completed_at = clock_timestamp(),
status = 'completed' status = 'completed'
where id = $1") where id = $1")
@ -87,7 +87,7 @@
nil)))) nil))))
(def ^:private sql:select-next-task (def ^:private sql:select-next-task
"select * from tasks as t "select * from task as t
where t.scheduled_at <= now() where t.scheduled_at <= now()
and t.queue = $1 and t.queue = $1
and (t.status = 'new' or (t.status = 'retry' and t.retry_num <= $2)) 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))))))))) (event-loop-handler (assoc options ::counter (inc counter)))))))))
(def ^:private sql:insert-new-task (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)) values ($1, $2, $3, clock_timestamp()+cast($4::text as interval))
returning id") returning id")
@ -162,7 +162,7 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:privatr sql:upsert-scheduled-task (def ^:privatr sql:upsert-scheduled-task
"insert into scheduled_tasks (id, cron_expr) "insert into scheduled_task (id, cron_expr)
values ($1, $2) values ($1, $2)
on conflict (id) on conflict (id)
do update set cron_expr=$2") do update set cron_expr=$2")
@ -178,7 +178,7 @@
(p/run! (partial synchronize-schedule-item conn) schedule))) (p/run! (partial synchronize-schedule-item conn) schedule)))
(def ^:private sql:lock-scheduled-task (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) (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] [clojure.spec.alpha :as s]
[promesa.core :as p] [promesa.core :as p]
[expound.alpha :as expound] [expound.alpha :as expound]
[sieppari.core :as sp]
[sieppari.context :as spx]
[uxbox.common.exceptions :as ex]) [uxbox.common.exceptions :as ex])
(:import (:import
clojure.lang.IDeref clojure.lang.IDeref
@ -45,8 +43,7 @@
(let [key (get params attr) (let [key (get params attr)
f (.get ^Map reg key)] f (.get ^Map reg key)]
(when (nil? f) (when (nil? f)
(ex/raise :type :not-found (ex/raise :type :method-not-found
:code :method-not-found
:hint "No method found for the current request.")) :hint "No method found for the current request."))
(f params)))) (f params))))

View File

@ -7,9 +7,12 @@
[environ.core :refer [env]] [environ.core :refer [env]]
[uxbox.services.mutations.profile :as profile] [uxbox.services.mutations.profile :as profile]
[uxbox.services.mutations.projects :as projects] [uxbox.services.mutations.projects :as projects]
[uxbox.services.mutations.project-files :as files] [uxbox.services.mutations.teams :as teams]
[uxbox.services.mutations.project-pages :as pages] [uxbox.services.mutations.files :as files]
[uxbox.services.mutations.pages :as pages]
[uxbox.services.mutations.images :as images] [uxbox.services.mutations.images :as images]
[uxbox.services.mutations.icons :as icons]
[uxbox.services.mutations.colors :as colors]
[uxbox.fixtures :as fixtures] [uxbox.fixtures :as fixtures]
[uxbox.migrations] [uxbox.migrations]
[uxbox.media] [uxbox.media]
@ -67,51 +70,66 @@
;; --- Profile creation ;; --- Profile creation
(defn create-user (defn create-profile
[conn i] [conn i]
(profile/create-profile conn {:id (mk-uuid "user" i) (#'profile/register-profile conn {:id (mk-uuid "profile" i)
:fullname (str "User " i) :fullname (str "Profile " i)
:email (str "user" i ".test@nodomain.com") :email (str "profile" i ".test@nodomain.com")
:password "123123" :password "123123"}))
:metadata {}}))
(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 (defn create-project
[conn user-id i] [conn profile-id team-id i]
(projects/create-project conn {:id (mk-uuid "project" i) (#'projects/create-project conn {:id (mk-uuid "project" i)
:user user-id :profile-id profile-id
:version 1 :team-id team-id
:name (str "sample project " i)})) :name (str "project" i)}))
(defn create-file
[conn profile-id project-id i]
(#'files/create-file conn {:id (mk-uuid "file" i)
:profile-id profile-id
:project-id project-id
:name (str "file" i)}))
(defn create-page
[conn profile-id file-id i]
(#'pages/create-page conn {:id (mk-uuid "page" i)
:profile-id profile-id
:file-id file-id
:name (str "page" i)
:ordering i
:data {:version 1
:shapes []
:options {}
:canvas []
:shapes-by-id {}}}))
(defn create-project-file (defn create-image-collection
[conn user-id project-id i] [conn profile-id i]
(files/create-file conn {:id (mk-uuid "project-file" i) (#'images/create-image-collection conn {:id (mk-uuid "imgcoll" i)
:user user-id :profile-id profile-id
:project-id project-id :name (str "image collection " i)}))
:name (str "sample project file" i)}))
(defn create-icon-collection
(defn create-project-page [conn profile-id i]
[conn user-id file-id i] (#'icons/create-icon-collection conn {:id (mk-uuid "imgcoll" i)
(pages/create-page conn {:id (mk-uuid "page" i) :profile-id profile-id
:user user-id :name (str "icon collection " i)}))
:file-id file-id (defn create-color-collection
:name (str "page" i) [conn profile-id i]
:ordering i (#'colors/create-color-collection conn {:id (mk-uuid "imgcoll" i)
:data {:version 1 :profile-id profile-id
:shapes [] :name (str "color collection " i)}))
:options {}
:canvas []
:shapes-by-id {}}}))
(defn create-images-collection
[conn user-id i]
(images/create-images-collection conn {:id (mk-uuid "imgcoll" i)
:user user-id
:name (str "image collection " i)}))
(defn handle-error (defn handle-error
[err] [^Throwable err]
(if (instance? java.util.concurrent.ExecutionException err) (if (instance? java.util.concurrent.ExecutionException err)
(handle-error (.getCause err)) (handle-error (.getCause err))
err)) err))
@ -128,10 +146,11 @@
[expr] [expr]
`(try `(try
(let [d# (p/deferred)] (let [d# (p/deferred)]
(->> #(p/finally ~expr (fn [v# e#] (->> #(p/finally (p/do! ~expr)
(if e# (fn [v# e#]
(p/reject! d# e#) (if e#
(p/resolve! d# v#)))) (p/reject! d# e#)
(p/resolve! d# v#))))
(vu/run-on-context! *context*)) (vu/run-on-context! *context*))
(array-map :error nil (array-map :error nil
:result (deref d#))) :result (deref d#)))
@ -155,8 +174,11 @@
(= :spec-validation (:code data)) (= :spec-validation (:code data))
(println (:explain data)) (println (:explain data))
(= :service-error (:type data))
(print-error! (.getCause ^Throwable error))
:else :else
(.printStackTrace error)))) (.printStackTrace ^Throwable error))))
(defn print-result! (defn print-result!
[{:keys [error 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 (:require
[clojure.test :as t] [clojure.test :as t]
[promesa.core :as p] [promesa.core :as p]
@ -16,26 +16,28 @@
(t/use-fixtures :once th/state-init) (t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset) (t/use-fixtures :each th/database-reset)
(t/deftest images-collections-crud (t/deftest image-collections-crud
(let [id (uuid/next) (let [id (uuid/next)
user @(th/create-user db/pool 2)] profile @(th/create-profile db/pool 2)]
(t/testing "create collection" (t/testing "create collection"
(let [data {::sm/type :create-images-collection (let [data {::sm/type :create-image-collection
:name "sample collection" :name "sample collection"
:user (:id user) :profile-id (:id profile)
:id id} :id id}
out (th/try-on! (sm/handle data))] out (th/try-on! (sm/handle data))]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error 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" (t/testing "update collection"
(let [data {::sm/type :rename-images-collection (let [data {::sm/type :rename-image-collection
:name "sample collection renamed" :name "sample collection renamed"
:user (:id user) :profile-id (:id profile)
:id id} :id id}
out (th/try-on! (sm/handle data))] out (th/try-on! (sm/handle data))]
@ -43,35 +45,35 @@
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(t/is (= id (get-in out [:result :id]))) (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/is (= (:name data) (get-in out [:result :name])))))
(t/testing "query collections" (t/testing "query collections"
(let [data {::sq/type :images-collections (let [data {::sq/type :image-collections
:user (:id user)} :profile-id (:id profile)}
out (th/try-on! (sq/handle data))] out (th/try-on! (sq/handle data))]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(t/is (= 1 (count (:result 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/is (= id (get-in out [:result 0 :id])))))
(t/testing "delete collection" (t/testing "delete collection"
(let [data {::sm/type :delete-images-collection (let [data {::sm/type :delete-image-collection
:user (:id user) :profile-id (:id profile)
:id id} :id id}
out (th/try-on! (sm/handle data))] out (th/try-on! (sm/handle data))]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error 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" (t/testing "query collections after delete"
(let [data {::sq/type :images-collections (let [data {::sq/type :image-collections
:user (:id user)} :profile-id (:id profile)}
out (th/try-on! (sq/handle data))] out (th/try-on! (sq/handle data))]
;; (th/print-result! out) ;; (th/print-result! out)
@ -80,8 +82,8 @@
)) ))
(t/deftest images-crud (t/deftest images-crud
(let [user @(th/create-user db/pool 1) (let [profile @(th/create-profile db/pool 1)
coll @(th/create-images-collection db/pool (:id user) 1) coll @(th/create-image-collection db/pool (:id profile) 1)
image-id (uuid/next)] image-id (uuid/next)]
(t/testing "upload image to collection" (t/testing "upload image to collection"
@ -91,13 +93,11 @@
:size 312043} :size 312043}
data {::sm/type :upload-image data {::sm/type :upload-image
:id image-id :id image-id
:user (:id user) :profile-id (:id profile)
:collection-id (:id coll) :collection-id (:id coll)
:name "testfile" :name "testfile"
:content content} :content content}
out (th/try-on! (sm/handle data))] 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) ;; (th/print-result! out)
(t/is (nil? (:error 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 :uri])))
(t/is (string? (get-in out [:result :thumb-uri]))))) (t/is (string? (get-in out [:result :thumb-uri])))))
(t/testing "list images by collection" (t/testing "list images by collection"
(let [data {::sq/type :images-by-collection (let [data {::sq/type :images
:user (:id user) :profile-id (:id profile)
:collection-id (:id coll)} :collection-id (:id coll)}
out (th/try-on! (sq/handle data))] out (th/try-on! (sq/handle data))]
;; (th/print-result! out) ;; (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 :uri])))
(t/is (string? (get-in out [:result 0 :thumb-uri]))))) (t/is (string? (get-in out [:result 0 :thumb-uri])))))
(t/testing "get image by id" (t/testing "single image"
(let [data {::sq/type :image-by-id (let [data {::sq/type :image
:user (:id user) :profile-id (:id profile)
:id image-id} :id image-id}
out (th/try-on! (sq/handle data))] out (th/try-on! (sq/handle data))]
;; (th/print-result! out) ;; (th/print-result! out)
@ -152,7 +151,38 @@
(t/is (string? (get-in out [:result :thumb-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 :uri])))
(t/is (string? (get-in out [:result :thumb-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 (:require
[clojure.test :as t] [clojure.test :as t]
[clojure.java.io :as io] [clojure.java.io :as io]
[mockery.core :refer [with-mocks]]
[promesa.core :as p] [promesa.core :as p]
[cuerdas.core :as str] [cuerdas.core :as str]
[datoteka.core :as fs] [datoteka.core :as fs]
@ -22,130 +23,175 @@
(t/use-fixtures :once th/state-init) (t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset) (t/use-fixtures :each th/database-reset)
(t/deftest login-with-failed-auth (t/deftest profile-login
(let [user @(th/create-user db/pool 1) (let [profile @(th/create-profile db/pool 1)]
event {::sm/type :login (t/testing "failed"
:email "user1.test@nodomain.com" (let [event {::sm/type :login
:password "foobar" :email "profile1.test@nodomain.com"
:scope "foobar"} :password "foobar"
out (th/try-on! (sm/handle event))] :scope "foobar"}
out (th/try-on! (sm/handle event))]
;; (th/print-result! out) ;; (th/print-result! out)
(let [error (:error out)] (let [error (:error out)]
(t/is (th/ex-info? error)) (t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :service-error))) (t/is (th/ex-of-type? error :service-error)))
(let [error (ex-cause (:error out))] (let [error (ex-cause (:error out))]
(t/is (th/ex-info? error)) (t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :validation)) (t/is (th/ex-of-type? error :validation))
(t/is (th/ex-of-code? error :uxbox.services.mutations.profile/wrong-credentials))))) (t/is (th/ex-of-code? error :uxbox.services.mutations.profile/wrong-credentials)))))
(t/deftest login-with-success-auth (t/testing "success"
(let [user @(th/create-user db/pool 1) (let [event {::sm/type :login
event {::sm/type :login :email "profile1.test@nodomain.com"
:email "user1.test@nodomain.com" :password "123123"
:password "123123" :scope "foobar"}
:scope "foobar"} out (th/try-on! (sm/handle event))]
out (th/try-on! (sm/handle event))] ;; (th/print-result! out)
;; (th/print-result! out) (t/is (nil? (:error out)))
(t/is (nil? (:error out))) (t/is (= (:id profile) (get-in out [:result :id])))))
(t/is (= (get-in out [:result :id]) (:id user))))) ))
(t/deftest query-profile (t/deftest profile-query-and-manipulation
(let [user @(th/create-user db/pool 1) (let [profile @(th/create-profile db/pool 1)]
data {::sq/type :profile
:user (:id user)}
out (th/try-on! (sq/handle data))] (t/testing "query profile"
;; (th/print-result! out) (let [data {::sq/type :profile
(t/is (nil? (:error out))) :profile-id (:id profile)}
(t/is (= "User 1" (get-in out [:result :fullname]))) out (th/try-on! (sq/handle data))]
(t/is (= "user1.test@nodomain.com" (get-in out [:result :email])))
(t/is (not (contains? (:result out) :password)))))
(t/deftest mutation-update-profile ;; (th/print-result! out)
(let [user @(th/create-user db/pool 1) (t/is (nil? (:error out)))
data (assoc user
::sm/type :update-profile
:fullname "Full Name"
:username "user222"
:lang "en")
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (= (:fullname data) (get-in out [:result :fullname])))
(t/is (= (:email data) (get-in out [:result :email])))
(t/is (= (:metadata data) (get-in out [:result :metadata])))
(t/is (not (contains? (:result out) :password)))))
(t/deftest mutation-update-profile-photo (let [result (:result out)]
(let [user @(th/create-user db/pool 1) (t/is (= "Profile 1" (:fullname result)))
data {::sm/type :update-profile-photo (t/is (= "profile1.test@nodomain.com" (:email result)))
:user (:id user) (t/is (not (contains? result :password))))))
:file {:name "sample.jpg"
:path "tests/uxbox/tests/_files/sample.jpg"
:size 123123
:mtype "image/jpeg"}}
out (th/try-on! (sm/handle data))]
;; (th/print-result! out) (t/testing "update profile"
(t/is (nil? (:error out))) (let [data (assoc profile
(t/is (= (:id user) (get-in out [:result :id]))))) ::sm/type :update-profile
:fullname "Full Name"
:name "profile222"
:lang "en")
out (th/try-on! (sm/handle data))]
;; (t/deftest test-mutation-register-profile ;; (th/print-result! out)
;; (let[data {:fullname "Full Name" (t/is (nil? (:error out)))
;; :username "user222"
;; :email "user222@uxbox.io"
;; :password "user222"
;; ::sv/type :register-profile}
;; [err rsp] (th/try-on (sm/handle data))]
;; (println "RESPONSE:" err rsp)))
;; (t/deftest test-http-validate-recovery-token (let [result (:result out)]
;; (with-open [conn (db/connection)] (t/is (= (:fullname data) (:fullname result)))
;; (let [user (th/create-user conn 1)] (t/is (= (:email data) (:email result)))
;; (with-server {:handler (uft/routes)} (t/is (not (contains? result :password))))))
;; (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 test-http-request-password-recovery (t/testing "update photo"
;; (with-open [conn (db/connection)] (let [data {::sm/type :update-profile-photo
;; (let [user (th/create-user conn 1) :profile-id (:id profile)
;; sql "select * from user_pswd_recovery" :file {:name "sample.jpg"
;; res (sc/fetch-one conn sql)] :path "tests/uxbox/tests/_files/sample.jpg"
:size 123123
:mtype "image/jpeg"}}
out (th/try-on! (sm/handle data))]
;; ;; Initially no tokens exists ;; (th/print-result! out)
;; (t/is (nil? res)) (t/is (nil? (:error out)))
;; (with-server {:handler (uft/routes)} (let [result (:result out)]
;; (let [uri (str th/+base-url+ "/api/auth/recovery") (t/is (= (:id profile) (:id result))))))
;; data {:username "user1"} ))
;; [status data] (th/http-post user uri {:body data})]
;; ;; (println "RESPONSE:" status data)
;; (t/is (= 204 status)))
;; (let [res (sc/fetch-one conn sql)] (t/deftest profile-deletion
;; (t/is (not (nil? res))) (let [prof @(th/create-profile db/pool 1)
;; (t/is (= (:user res) (:id user)))))))) team (:default-team prof)
proj (:default-project prof)
file @(th/create-file db/pool (:id prof) (:id proj) 1)
page @(th/create-page db/pool (:id prof) (:id file) 1)]
;; (t/deftest test-http-validate-recovery-token (t/testing "try to delete profile not marked for deletion"
;; (with-open [conn (db/connection)] (let [params {:props {:profile-id (:id prof)}}
;; (let [user (th/create-user conn 1)] out (th/try-on! (uxbox.tasks.delete-profile/handler params))]
;; (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})
;; user' (usu/find-full-user-by-id conn (:id user))] ;; (th/print-result! out)
;; (t/is (= status 204)) (t/is (nil? (:error out)))
;; (t/is (hashers/check "mytestpassword" (:password user')))))))) (t/is (nil? (:result out)))))
(t/testing "query profile after delete"
(let [data {::sq/type :profile
:profile-id (:id prof)}
out (th/try-on! (sq/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (= (:fullname prof) (:fullname result))))))
(t/testing "mark profile for deletion"
(with-mocks
[mock {:target 'uxbox.tasks/schedule! :return nil}]
(let [data {::sm/type :delete-profile
:profile-id (:id prof)}
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (nil? (:result out))))
;; check the mock
(let [mock (deref mock)
mock-params (second (:call-args mock))]
(t/is (true? (:called? mock)))
(t/is (= 1 (:call-count mock)))
(t/is (= "delete-profile" (:name mock-params)))
(t/is (= (:id prof) (get-in mock-params [:props :profile-id]))))))
(t/testing "query files after profile soft deletion"
(let [data {::sq/type :files
:project-id (:id proj)
:profile-id (:id prof)}
out (th/try-on! (sq/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (= 1 (count (:result out))))))
(t/testing "try to delete profile marked for deletion"
(let [params {:props {:profile-id (:id prof)}}
out (th/try-on! (uxbox.tasks.delete-profile/handler params))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (= (:id prof) (:result out)))))
(t/testing "query profile after delete"
(let [data {::sq/type :profile
:profile-id (:id prof)}
out (th/try-on! (sq/handle data))]
;; (th/print-result! out)
(let [error (:error out)
error-data (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type error-data) :service-error))
(t/is (= (:name error-data) :uxbox.services.queries.profile/profile)))
(let [error (ex-cause (:error out))
error-data (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type error-data) :not-found)))))
(t/testing "query files after profile permanent deletion"
(let [data {::sq/type :files
:project-id (:id proj)
:profile-id (:id prof)}
out (th/try-on! (sq/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (= 0 (count (:result out))))))
))
;; TODO: profile deletion with teams
;; TODO: profile deletion with owner teams
;; TODO: profile registration
;; TODO: profile password recovery

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.http :as http]
[uxbox.services.mutations :as sm] [uxbox.services.mutations :as sm]
[uxbox.services.queries :as sq] [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 :once th/state-init)
(t/use-fixtures :each th/database-reset) (t/use-fixtures :each th/database-reset)
(t/deftest query-projects (t/deftest projects-crud
(let [user @(th/create-user db/pool 1) (let [prof @(th/create-profile db/pool 1)
proj @(th/create-project db/pool (:id user) 1) team @(th/create-team db/pool (:id prof) 1)
data {::sq/type :projects project-id (uuid/next)]
: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 mutation-create-project (t/testing "create a project"
(let [user @(th/create-user db/pool 1) (let [data {::sm/type :create-project
data {::sm/type :create-project :id project-id
:user (:id user) :profile-id (:id prof)
:name "test project"} :team-id (:id team)
out (th/try-on! (sm/handle data))] :name "test project"}
;; (th/print-result! out) out (th/try-on! (sm/handle data))]
(t/is (nil? (:error out))) ;; (th/print-result! out)
(t/is (= (:name data) (get-in out [:result :name])))))
(t/deftest mutation-rename-project (t/is (nil? (:error out)))
(let [user @(th/create-user db/pool 1) (let [result (:result out)]
proj @(th/create-project db/pool (:id user) 1) (t/is (= (:name data) (:name result))))))
data {::sm/type :rename-project
:id (:id proj)
:name "test project mod"
:user (:id user)}
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (= (:id data) (get-in out [:result :id])))
(t/is (= (:user data) (get-in out [:result :user-id])))
(t/is (= (:name data) (get-in out [:result :name])))))
(t/deftest mutation-delete-project (t/testing "query a list of projects"
(let [user @(th/create-user db/pool 1) (let [data {::sq/type :projects-by-team
proj @(th/create-project db/pool (:id user) 1) :team-id (:id team)
data {::sm/type :delete-project :profile-id (:id prof)}
:id (:id proj) out (th/try-on! (sq/handle data))]
:user (:id user)} ;; (th/print-result! out)
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (nil? (:result out)))
(let [sql "select * from projects where user_id=$1 and deleted_at is null" (t/is (nil? (:error out)))
res @(db/query db/pool [sql (:id user)])] (let [result (:result out)]
(t/is (empty? res))))) (t/is (= 1 (count result)))
(t/is project-id (get-in result [0 :id]))
(t/is "test project" (get-in result [0 :name])))))
;; TODO: add permisions related tests (t/testing "rename project"
(let [data {::sm/type :rename-project
:id project-id
:name "renamed project"
:profile-id (:id prof)}
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (= (:id data) (:id result)))
(t/is (= (:name data) (:name result)))
(t/is (= (:profile-id data) (:id prof))))))
(t/testing "delete project"
(let [data {::sm/type :delete-project
:id project-id
:profile-id (:id prof)}
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (nil? (:result out)))))
(t/testing "query a list of projects after delete"
(let [data {::sq/type :projects-by-team
:team-id (:id team)
:profile-id (:id prof)}
out (th/try-on! (sq/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (= 1 (count result))))))
))

View File

@ -9,57 +9,57 @@
[uxbox.services.queries :as sq] [uxbox.services.queries :as sq]
[uxbox.tests.helpers :as th])) [uxbox.tests.helpers :as th]))
(t/use-fixtures :once th/state-init) ;; (t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset) ;; (t/use-fixtures :each th/database-reset)
(t/deftest test-user-attrs ;; (t/deftest test-user-attrs
(let [{:keys [id] :as user} @(th/create-user db/pool 1)] ;; (let [{:keys [id] :as user} @(th/create-user db/pool 1)]
(let [out (th/try-on! (sq/handle {::sq/type :user-attr ;; (let [out (th/try-on! (sq/handle {::sq/type :user-attr
:key "foobar" ;; :key "foobar"
:user id}))] ;; :user id}))]
(t/is (nil? (:result out))) ;; (t/is (nil? (:result out)))
(let [error (:error out)] ;; (let [error (:error out)]
(t/is (th/ex-info? error)) ;; (t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :service-error))) ;; (t/is (th/ex-of-type? error :service-error)))
(let [error (ex-cause (:error out))] ;; (let [error (ex-cause (:error out))]
(t/is (th/ex-info? error)) ;; (t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :not-found)))) ;; (t/is (th/ex-of-type? error :not-found))))
(let [out (th/try-on! (sm/handle {::sm/type :upsert-user-attr ;; (let [out (th/try-on! (sm/handle {::sm/type :upsert-user-attr
:user id ;; :user id
:key "foobar" ;; :key "foobar"
:val {:some #{:value}}}))] ;; :val {:some #{:value}}}))]
;; (th/print-result! out) ;; ;; (th/print-result! out)
(t/is (nil? (:error out))) ;; (t/is (nil? (:error out)))
(t/is (nil? (:result out)))) ;; (t/is (nil? (:result out))))
(let [out (th/try-on! (sq/handle {::sq/type :user-attr ;; (let [out (th/try-on! (sq/handle {::sq/type :user-attr
:key "foobar" ;; :key "foobar"
:user id}))] ;; :user id}))]
;; (th/print-result! out) ;; ;; (th/print-result! out)
(t/is (nil? (:error out))) ;; (t/is (nil? (:error out)))
(t/is (= {:some #{:value}} (get-in out [:result :val]))) ;; (t/is (= {:some #{:value}} (get-in out [:result :val])))
(t/is (= "foobar" (get-in out [:result :key])))) ;; (t/is (= "foobar" (get-in out [:result :key]))))
(let [out (th/try-on! (sm/handle {::sm/type :delete-user-attr ;; (let [out (th/try-on! (sm/handle {::sm/type :delete-user-attr
:user id ;; :user id
:key "foobar"}))] ;; :key "foobar"}))]
;; (th/print-result! out) ;; ;; (th/print-result! out)
(t/is (nil? (:error out))) ;; (t/is (nil? (:error out)))
(t/is (nil? (:result out)))) ;; (t/is (nil? (:result out))))
(let [out (th/try-on! (sq/handle {::sq/type :user-attr ;; (let [out (th/try-on! (sq/handle {::sq/type :user-attr
:key "foobar" ;; :key "foobar"
:user id}))] ;; :user id}))]
;; (th/print-result! out) ;; ;; (th/print-result! out)
(t/is (nil? (:result out))) ;; (t/is (nil? (:result out)))
(let [error (:error out)] ;; (let [error (:error out)]
(t/is (th/ex-info? error)) ;; (t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :service-error))) ;; (t/is (th/ex-of-type? error :service-error)))
(let [error (ex-cause (:error out))] ;; (let [error (ex-cause (:error out))]
(t/is (th/ex-info? error)) ;; (t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :not-found)))))) ;; (t/is (th/ex-of-type? error :not-found))))))

View File

@ -58,7 +58,7 @@
(st/emit! #(assoc % :router router)) (st/emit! #(assoc % :router router))
(add-watch html-history/path ::main #(on-navigate router %4)) (add-watch html-history/path ::main #(on-navigate router %4))
(when (:auth storage) (when (:profile storage)
(st/emit! udu/fetch-profile)) (st/emit! udu/fetch-profile))
(mf/mount (mf/element ui/app) (dom/get-element "app")) (mf/mount (mf/element ui/app) (dom/get-element "app"))

View File

@ -30,14 +30,9 @@
(defn logged-in (defn logged-in
[data] [data]
(ptk/reify ::logged-in (ptk/reify ::logged-in
ptk/UpdateEvent
(update [this state]
(assoc state :auth data))
ptk/WatchEvent ptk/WatchEvent
(watch [this state s] (watch [this state stream]
(swap! storage assoc :auth data) (rx/of (du/profile-fetched data)
(rx/of du/fetch-profile
(rt/navigate :dashboard-projects))))) (rt/navigate :dashboard-projects)))))
;; --- Login ;; --- Login

View File

@ -5,49 +5,222 @@
;; Copyright (c) 2015-2016 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2015-2016 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.main.data.dashboard (ns uxbox.main.data.dashboard
(:require [beicon.core :as rx] (:require
[uxbox.util.uuid :as uuid] [beicon.core :as rx]
[potok.core :as ptk] [cljs.spec.alpha :as s]
[uxbox.util.router :as r] [cuerdas.core :as str]
[uxbox.main.store :as st] [potok.core :as ptk]
[uxbox.main.repo :as rp] [uxbox.common.data :as d]
[uxbox.main.data.projects :as dp] [uxbox.common.pages :as cp]
[uxbox.main.data.colors :as dc] [uxbox.common.spec :as us]
[uxbox.main.data.images :as di] [uxbox.main.repo :as rp]
[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)
ptk/UpdateEvent (s/def ::name string?)
(update [_ state] (s/def ::team-id ::us/uuid)
(update state :dashboard assoc (s/def ::profile-id ::us/uuid)
:section section (s/def ::project-id ::us/uuid)
:collection-type :builtin (s/def ::created-at ::us/inst)
:collection-id 1))) (s/def ::modified-at ::us/inst)
(defn initialize (s/def ::team
[section] (s/keys :req-un [::id
(InitializeDashboard. section)) ::name
::created-at
::modified-at]))
(defn set-collection-type (s/def ::project
[type] (s/keys ::req-un [::id
{:pre [(contains? #{:builtin :own} type)]} ::name
(letfn [(select-first [state] ::team-id
(if (= type :builtin) ::version
(assoc-in state [:dashboard :collection-id] 1) ::profile-id
(let [colls (sort-by :id (vals (:colors-by-id state)))] ::created-at
(assoc-in state [:dashboard :collection-id] (:id (first colls))))))] ::modified-at]))
(reify
ptk/UpdateEvent
(update [_ state]
(as-> state $
(assoc-in $ [:dashboard :collection-type] type)
(select-first $))))))
(defn set-collection (s/def ::file
[id] (s/keys :req-un [::id
(reify ::name
::created-at
::modified-at
::project-id]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Initialization
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare fetch-files)
(def initialize-drafts
(ptk/reify ::initialize
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(assoc-in state [:dashboard :collection-id] id)))) (let [profile (:profile state)]
(update state :dashboard-local assoc
:team-id (:default-team-id profile)
:project-id (:default-project-id profile))))
ptk/WatchEvent
(watch [_ state stream]
(let [local (:dashboard-local state)]
(rx/of (fetch-files (:project-id local)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Fetching
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- Fetch Projects
(declare projects-fetched)
(def fetch-projects
(ptk/reify ::fetch-projects
ptk/WatchEvent
(watch [_ state stream]
(->> (rp/query :projects)
(rx/map projects-fetched)))))
(defn projects-fetched
[projects]
(us/verify (s/every ::project) projects)
(ptk/reify ::projects-fetched
ptk/UpdateEvent
(update [_ state]
(let [assoc-project #(update-in %1 [:projects (:id %2)] merge %2)]
(reduce assoc-project state projects)))))
;; --- Fetch Files
(declare files-fetched)
(defn fetch-files
[project-id]
(ptk/reify ::fetch-files
ptk/WatchEvent
(watch [_ state stream]
(let [params {:project-id project-id}]
(->> (rp/query :files params)
(rx/map files-fetched))))))
(defn files-fetched
[files]
(us/verify (s/every ::file) files)
(ptk/reify ::files-fetched
ptk/UpdateEvent
(update [_ state]
(let [state (dissoc state :files)
files (d/index-by :id files)]
(assoc state :files files)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Modification
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- Rename Project
(defn rename-project
[id name]
{:pre [(uuid? id) (string? name)]}
(ptk/reify ::rename-project
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:projects id :name] name))
ptk/WatchEvent
(watch [_ state stream]
(let [params {:id id :name name}]
(->> (rp/mutation :rename-project params)
(rx/ignore))))))
;; --- Delete Project (by id)
(defn delete-project
[id]
(us/verify ::us/uuid id)
(ptk/reify ::delete-project
ptk/UpdateEvent
(update [_ state]
(update state :projects dissoc id))
ptk/WatchEvent
(watch [_ state s]
(->> (rp/mutation :delete-project {:id id})
(rx/ignore)))))
;; --- Delete File (by id)
(defn delete-file
[id]
(us/verify ::us/uuid id)
(ptk/reify ::delete-file
ptk/UpdateEvent
(update [_ state]
(update state :files dissoc id))
ptk/WatchEvent
(watch [_ state s]
(->> (rp/mutation :delete-file {:id id})
(rx/ignore)))))
;; --- Rename Project
(defn rename-file
[id name]
{:pre [(uuid? id) (string? name)]}
(ptk/reify ::rename-file
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:files id :name] name))
ptk/WatchEvent
(watch [_ state stream]
(let [params {:id id :name name}]
(->> (rp/mutation :rename-file params)
(rx/ignore))))))
;; --- Create File
(declare file-created)
(def create-file
(ptk/reify ::create-draft-file
ptk/WatchEvent
(watch [_ state stream]
(let [name (str "New File " (gensym "p"))
project-id (get-in state [:dashboard-local :project-id])
params {:name name :project-id project-id}]
(->> (rp/mutation! :create-file params)
(rx/map file-created))))))
(defn file-created
[data]
(us/verify ::file data)
(ptk/reify ::create-draft-file
ptk/UpdateEvent
(update [this state]
(update state :files assoc (:id data) data))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; UI State Handling
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- Update Opts (Filtering & Ordering)
(defn update-opts
[& {:keys [order filter] :as opts}]
(ptk/reify ::update-opts
ptk/UpdateEvent
(update [_ state]
(update state :dashboard-local merge
(when order {:order order})
(when filter {:filter filter})))))

View File

@ -79,7 +79,7 @@
(ptk/reify ::fetch-collections (ptk/reify ::fetch-collections
ptk/WatchEvent ptk/WatchEvent
(watch [_ state s] (watch [_ state s]
(->> (rp/query! :images-collections) (->> (rp/query! :image-collections)
(rx/map collections-fetched))))) (rx/map collections-fetched)))))
@ -108,7 +108,7 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state s] (watch [_ state s]
(let [data {:name (tr "ds.default-library-title" (gensym "c"))}] (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)))))) (rx/map collection-created))))))
;; --- Collection Created ;; --- Collection Created
@ -134,7 +134,7 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state s] (watch [_ state s]
(let [params {:id id :name name}] (let [params {:id id :name name}]
(->> (rp/mutation! :rename-images-collection params) (->> (rp/mutation! :rename-image-collection params)
(rx/ignore)))))) (rx/ignore))))))
;; --- Delete Collection ;; --- Delete Collection
@ -148,7 +148,7 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state s] (watch [_ state s]
(->> (rp/mutation! :delete-images-collection {:id id}) (->> (rp/mutation! :delete-image-collection {:id id})
(rx/tap on-success) (rx/tap on-success)
(rx/ignore))))) (rx/ignore)))))

View File

@ -4,6 +4,11 @@
;; ;;
;; Copyright (c) 2015-2017 Andrey Antukh <niwi@niwi.nz> ;; 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 (ns uxbox.main.data.projects
(:require (:require
[beicon.core :as rx] [beicon.core :as rx]
@ -22,22 +27,16 @@
(s/def ::id ::us/uuid) (s/def ::id ::us/uuid)
(s/def ::name string?) (s/def ::name string?)
(s/def ::user ::us/uuid) (s/def ::profile-id ::us/uuid)
(s/def ::type keyword?) (s/def ::project-id (s/nilable ::us/uuid))
(s/def ::file-id ::us/uuid)
(s/def ::project-id ::us/uuid)
(s/def ::created-at ::us/inst) (s/def ::created-at ::us/inst)
(s/def ::modified-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/def ::project
(s/keys ::req-un [::id (s/keys ::req-un [::id
::name ::name
::version ::version
::user-id ::profile-id
::created-at ::created-at
::modified-at])) ::modified-at]))
@ -48,43 +47,17 @@
::modified-at ::modified-at
::project-id])) ::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 ;; --- Initialize Dashboard
(declare fetch-projects) (declare fetch-projects)
(declare fetch-files) (declare fetch-files)
(declare fetch-draft-files)
(declare initialized) (declare initialized)
;; NOTE/WARN: this need to be refactored completly when new UI is
;; prototyped.
(defn initialize (defn initialize
[id] [id]
(ptk/reify ::initialize (ptk/reify ::initialize
@ -95,7 +68,9 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(rx/merge (rx/merge
(rx/of (fetch-files id)) (if (nil? id)
(rx/of fetch-draft-files)
(rx/of (fetch-files id)))
(->> stream (->> stream
(rx/filter (ptk/type? ::files-fetched)) (rx/filter (ptk/type? ::files-fetched))
(rx/take 1) (rx/take 1)
@ -121,6 +96,7 @@
(when filter {:filter filter}))))) (when filter {:filter filter})))))
;; --- Fetch Projects ;; --- Fetch Projects
(declare projects-fetched) (declare projects-fetched)
(def fetch-projects (def fetch-projects
@ -151,9 +127,16 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(let [params (if (nil? project-id) {} {:project-id project-id})] (let [params (if (nil? project-id) {} {:project-id project-id})]
(->> (rp/query :project-files params) (->> (rp/query :files params)
(rx/map files-fetched)))))) (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) ;; --- Fetch File (by ID)
(defn fetch-file (defn fetch-file
@ -162,23 +145,11 @@
(ptk/reify ::fetch-file (ptk/reify ::fetch-file
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(->> (rp/query :project-file {:id id}) (->> (rp/query :file {:id id})
(rx/map #(files-fetched [%])))))) (rx/map #(files-fetched [%]))))))
;; --- 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 ;; --- Create Project
(declare project-created) (declare project-created)
@ -201,12 +172,31 @@
(watch [this state stream] (watch [this state stream]
(let [name (str "New File " (gensym "p")) (let [name (str "New File " (gensym "p"))
params {:name name :project-id project-id}] params {:name name :project-id project-id}]
(->> (rp/mutation! :create-project-file params) (->> (rp/mutation! :create-file params)
(rx/mapcat (rx/mapcat
(fn [data] (fn [data]
(rx/of (files-fetched [data]) (rx/of (files-fetched [data])
#(update-in % [:dashboard-projects :files project-id] conj (:id 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 ;; --- Rename Project
(defn rename-project (defn rename-project
@ -250,7 +240,7 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state s] (watch [_ state s]
(->> (rp/mutation :delete-project-file {:id id}) (->> (rp/mutation :delete-file {:id id})
(rx/ignore))))) (rx/ignore)))))
;; --- Rename Project ;; --- Rename Project
@ -266,7 +256,7 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(let [params {:id id :name name}] (let [params {:id id :name name}]
(->> (rp/mutation :rename-project-file params) (->> (rp/mutation :rename-file params)
(rx/ignore)))))) (rx/ignore))))))
;; --- Go To Project ;; --- Go To Project
@ -291,178 +281,3 @@
(if (nil? id) (if (nil? id)
(rx/of (rt/nav :dashboard-projects {} {})) (rx/of (rt/nav :dashboard-projects {} {}))
(rx/of (rt/nav :dashboard-projects {} {:project-id (str id)})))))) (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-who)
(declare handle-pointer-update) (declare handle-pointer-update)
(declare handle-pointer-send) (declare handle-pointer-send)
(declare handle-page-snapshot) (declare handle-page-change)
(declare shapes-changes-commited) (declare shapes-changes-commited)
(declare commit-changes) (declare commit-changes)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Websockets Events ;; Workspace WebSocket
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- Initialize WebSocket ;; --- Initialize WebSocket
@ -93,7 +93,7 @@
(case type (case type
:who (handle-who msg) :who (handle-who msg)
:pointer-update (handle-pointer-update msg) :pointer-update (handle-pointer-update msg)
:page-snapshot (handle-page-snapshot msg) :page-change (handle-page-change msg)
::unknown)))) ::unknown))))
(->> stream (->> stream
@ -160,11 +160,12 @@
:y (:y point)}] :y (:y point)}]
(ws/-send ws (t/encode msg)))))) (ws/-send ws (t/encode msg))))))
(defn handle-page-snapshot (defn handle-page-change
[{:keys [user-id page-id version operations] :as msg}] [{:keys [profile-id page-id revn operations] :as msg}]
(ptk/reify ::handle-page-snapshot (ptk/reify ::handle-page-change
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(prn "handle-page-change")
(let [page-id' (get-in state [:workspace-page :id])] (let [page-id' (get-in state [:workspace-page :id])]
(when (= page-id page-id') (when (= page-id page-id')
(rx/of (shapes-changes-commited msg))))))) (rx/of (shapes-changes-commited msg)))))))
@ -254,9 +255,9 @@
;; (update [_ state] ;; (update [_ state]
;; (update :workspace-local dissoc :undo-index)))) ;; (update :workspace-local dissoc :undo-index))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; General workspace events ;; Workspace Initialization
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- Initialize Workspace ;; --- Initialize Workspace
@ -275,6 +276,9 @@
(declare initialize-layout) (declare initialize-layout)
(declare initialize-page) (declare initialize-page)
(declare initialize-file) (declare initialize-file)
(declare fetch-file-with-users)
(declare fetch-pages)
(declare fetch-page)
(defn initialize (defn initialize
"Initialize the workspace state." "Initialize the workspace state."
@ -286,21 +290,31 @@
(watch [_ state stream] (watch [_ state stream]
(let [file (:workspace-file state)] (let [file (:workspace-file state)]
(if (not= (:id file) file-id) (if (not= (:id file) file-id)
(do
;; (reset! st/loader true)
(rx/merge
(rx/of (fetch-file-with-users file-id)
(fetch-pages file-id)
(initialize-layout file-id)
(fetch-images file-id))
(->> (rx/zip (rx/filter (ptk/type? ::pages-fetched) stream)
(rx/filter (ptk/type? ::file-fetched) stream))
(rx/take 1)
(rx/do (fn [_]
(uxbox.util.timers/schedule 500 #(reset! st/loader false))))
(rx/mapcat (fn [_]
(rx/of (initialize-file file-id)
(initialize-page page-id)
#_(initialize-alignment page-id)))))))
(rx/merge (rx/merge
(rx/of (dp/fetch-file file-id) (rx/of (fetch-page page-id))
(dp/fetch-pages file-id) (->> stream
(initialize-layout file-id) (rx/filter (ptk/type? ::pages-fetched))
(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/take 1) (rx/take 1)
(rx/do #(reset! st/loader false)) (rx/merge-map (fn [_]
(rx/mapcat #(rx/of (initialize-file file-id) (rx/of (initialize-file file-id)
(initialize-page page-id) (initialize-page page-id)))))))))))
#_(initialize-alignment page-id)))))
(rx/of (initialize-file file-id)
(initialize-page page-id)))))))
(defn- initialize-layout (defn- initialize-layout
[file-id] [file-id]
@ -351,9 +365,10 @@
(ptk/reify ::finalize (ptk/reify ::finalize
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(dissoc state state
:workspace-page #_(dissoc state
:workspace-data)))) :workspace-page
:workspace-data))))
(def diff-and-commit-changes (def diff-and-commit-changes
(ptk/reify ::diff-and-commit-changes (ptk/reify ::diff-and-commit-changes
@ -379,17 +394,69 @@
(when-not (empty? changes) (when-not (empty? changes)
(rx/of (commit-changes 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 ;; --- Fetch Workspace Users
(declare users-fetched) (declare users-fetched)
(declare file-fetched)
(defn fetch-users (defn fetch-file-with-users
[file-id] [id]
(ptk/reify ::fetch-users (us/verify ::us/uuid id)
(ptk/reify ::fetch-file-with-users
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(->> (rp/query :project-file-users {:file-id file-id}) (->> (rp/query :file-with-users {:id id})
(rx/map users-fetched))))) (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 (defn users-fetched
[users] [users]
@ -402,6 +469,226 @@
users)))) 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 ;; --- Toggle layout flag
(defn toggle-layout-flag (defn toggle-layout-flag
@ -1052,23 +1339,23 @@
(watch [_ state stream] (watch [_ state stream]
(let [page (:workspace-page state) (let [page (:workspace-page state)
params {:id (:id page) params {:id (:id page)
:version (:version page) :revn (:revn page)
:changes (vec changes)}] :changes (vec changes)}]
(->> (rp/mutation :update-project-page params) (->> (rp/mutation :update-page params)
(rx/map shapes-changes-commited)))))) (rx/map shapes-changes-commited))))))
(s/def ::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 (defn shapes-changes-commited
[{:keys [page-id version changes] :as params}] [{:keys [page-id revn changes] :as params}]
(us/verify ::shapes-changes-commited params) (us/verify ::shapes-changes-commited params)
(ptk/reify ::shapes-changes-commited (ptk/reify ::shapes-changes-commited
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(-> state (-> state
(assoc-in [:workspace-page :version] version) (assoc-in [:workspace-page :revn] revn)
(assoc-in [:pages page-id :version] version) (assoc-in [:pages page-id :revn] revn)
(update-in [:pages-data page-id] cp/process-changes changes) (update-in [:pages-data page-id] cp/process-changes changes)
(update :workspace-data cp/process-changes changes))))) (update :workspace-data cp/process-changes changes)))))
@ -1298,7 +1585,7 @@
(defn go-to-page (defn go-to-page
[page-id] [page-id]
(us/verify ::us/uuid page-id) (us/verify ::us/uuid page-id)
(ptk/reify ::go-to (ptk/reify ::go-to-page
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(let [file-id (get-in state [:workspace-page :file-id]) (let [file-id (get-in state [:workspace-page :file-id])
@ -1306,93 +1593,16 @@
query-params {:page-id page-id}] query-params {:page-id page-id}]
(rx/of (rt/nav :workspace path-params query-params)))))) (rx/of (rt/nav :workspace path-params query-params))))))
(defn go-to-file
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Workspace Images
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- Fetch Workspace Images
(declare images-fetched)
(defn fetch-images
[file-id] [file-id]
(ptk/reify ::fetch-images (us/verify ::us/uuid file-id)
(ptk/reify ::go-to-file
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(->> (rp/query :project-file-images {:file-id file-id}) (let [page-ids (get-in state [:files file-id :pages])
(rx/map images-fetched))))) path-params {:file-id file-id}
query-params {:page-id (first page-ids)}]
(defn images-fetched (rx/of (rt/nav :workspace path-params query-params))))))
[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))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Page Changes Reactions ;; Page Changes Reactions

View File

@ -120,7 +120,7 @@
(seq params)) (seq params))
(send-mutation! id form))) (send-mutation! id form)))
(defmethod mutation :upload-project-file-image (defmethod mutation :upload-file-image
[id params] [id params]
(let [form (js/FormData.)] (let [form (js/FormData.)]
(run! (fn [[key val]] (run! (fn [[key val]]

View File

@ -5,7 +5,6 @@
[uxbox.util.data :refer [parse-int uuid-str?]] [uxbox.util.data :refer [parse-int uuid-str?]]
[uxbox.main.ui.dashboard.header :refer [header]] [uxbox.main.ui.dashboard.header :refer [header]]
[uxbox.main.ui.dashboard.projects :as projects] [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.icons :as icons]
[uxbox.main.ui.dashboard.images :as images] [uxbox.main.ui.dashboard.images :as images]
[uxbox.main.ui.dashboard.colors :as colors] [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 ;; 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/. ;; 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 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2017 Juan de la Cruz <delacruzgarciajuan@gmail.com> ;; Copyright (c) 2015-2017 Juan de la Cruz <delacruzgarciajuan@gmail.com>
@ -14,6 +17,7 @@
[uxbox.builtins.icons :as i] [uxbox.builtins.icons :as i]
[uxbox.main.constants :as c] [uxbox.main.constants :as c]
[uxbox.main.data.projects :as udp] [uxbox.main.data.projects :as udp]
[uxbox.main.data.dashboard :as dsh]
[uxbox.main.store :as st] [uxbox.main.store :as st]
[uxbox.main.exports :as exports] [uxbox.main.exports :as exports]
[uxbox.main.ui.modal :as modal] [uxbox.main.ui.modal :as modal]
@ -137,13 +141,12 @@
(sort-by order)) (sort-by order))
on-click #(do on-click #(do
(dom/prevent-default %) (dom/prevent-default %)
(st/emit! (udp/create-file {:project-id id})))] (st/emit! dsh/create-file))]
[:section.dashboard-grid [:section.dashboard-grid
[:div.dashboard-grid-content [:div.dashboard-grid-content
[:div.dashboard-grid-row [:div.dashboard-grid-row
(when id [:div.grid-item.add-project {:on-click on-click}
[:div.grid-item.add-project {:on-click on-click} [:span (tr "ds.new-file")]]
[:span (tr "ds.new-file")]])
(for [item files] (for [item files]
[:& grid-item {:file item :key (:id item)}])]]])) [:& grid-item {:file item :key (:id item)}])]]]))
@ -195,16 +198,20 @@
:placeholder (tr "ds.search.placeholder")}] :placeholder (tr "ds.search.placeholder")}]
[:div.clear-search i/close]] [:div.clear-search i/close]]
[:ul.library-elements [: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")} :class-name (when (nil? id) "current")}
[:span.element-title "Recent"]] [: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 [:div.projects-row
[:span "PROJECTS"] [:span "PROJECTS/TEAMS TODO"]
[:a.add-project {:on-click #(st/emit! udp/create-project)} #_[:a.add-project {:on-click #(st/emit! udp/create-project)}
i/close]] i/close]]
(for [item projects] #_(for [item projects]
[:& nav-item {:id (:id item) [:& nav-item {:id (:id item)
:key (:id item) :key (:id item)
:name (:name item) :name (:name item)
@ -213,14 +220,9 @@
;; --- Component: Content ;; --- Component: Content
(def files-ref (def files-ref
(letfn [(selector [state] (-> (comp (l/key :files)
(let [id (get-in state [:dashboard-projects :id]) (l/lens vals))
ids (get-in state [:dashboard-projects :files id]) (l/derive st/state)))
xf (comp (map #(get-in state [:files %]))
(remove nil?))]
(into [] xf ids)))]
(-> (l/lens selector)
(l/derive st/state))))
(mf/defc content (mf/defc content
[{:keys [id] :as props}] [{:keys [id] :as props}]
@ -233,8 +235,7 @@
(mf/defc projects-page (mf/defc projects-page
[{:keys [id] :as props}] [{:keys [id] :as props}]
(mf/use-effect #(st/emit! udp/fetch-projects)) (mf/use-effect {:fn #(st/emit! dsh/initialize-drafts)
(mf/use-effect {:fn #(st/emit! (udp/initialize id))
:deps #js [id]}) :deps #js [id]})
[:section.dashboard-content [:section.dashboard-content
[:& nav {:id id}] [:& nav {:id id}]

View File

@ -87,7 +87,7 @@
{:alt (tr "header.sitemap") {:alt (tr "header.sitemap")
:class (when (contains? layout :sitemap) "selected") :class (when (contains? layout :sitemap) "selected")
:on-click #(st/emit! (dw/toggle-layout-flag :sitemap))} :on-click #(st/emit! (dw/toggle-layout-flag :sitemap))}
[:span (:project-name file) " / " (:name file)]] [:span (:name file)]]
[:& active-users] [:& active-users]

View File

@ -11,7 +11,6 @@
[lentes.core :as l] [lentes.core :as l]
[rumext.alpha :as mf] [rumext.alpha :as mf]
[uxbox.builtins.icons :as i] [uxbox.builtins.icons :as i]
[uxbox.main.data.projects :as dp]
[uxbox.main.data.workspace :as dw] [uxbox.main.data.workspace :as dw]
[uxbox.main.store :as st] [uxbox.main.store :as st]
[uxbox.main.refs :as refs] [uxbox.main.refs :as refs]
@ -45,7 +44,7 @@
;; parent (.-parentNode parent) ;; parent (.-parentNode parent)
name (dom/get-value target)] name (dom/get-value target)]
;; (set! (.-draggable parent) true) ;; (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))) (swap! local assoc :edition false)))
on-key-down (fn [event] on-key-down (fn [event]
@ -56,7 +55,7 @@
(kbd/esc? event) (kbd/esc? event)
(swap! local assoc :edition false))) (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 on-delete #(do
(dom/prevent-default %) (dom/prevent-default %)
(dom/stop-propagation %) (dom/stop-propagation %)
@ -128,7 +127,7 @@
(mf/defc sitemap-toolbox (mf/defc sitemap-toolbox
[{:keys [file page] :as props}] [{: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)] locale (i18n/use-locale)]
[:div.sitemap.tool-window [:div.sitemap.tool-window
[:div.tool-window-bar [:div.tool-window-bar