0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-28 15:41:25 -05:00

♻️ Refactor services (for add the project-file concept.

And fix many tests.
This commit is contained in:
Andrey Antukh 2019-12-09 16:27:01 +01:00
parent af62d949d8
commit 183f0a5400
40 changed files with 1279 additions and 1006 deletions

View file

@ -10,10 +10,11 @@ CREATE TABLE users (
email text NOT NULL,
photo text NOT NULL,
password text NOT NULL,
metadata bytea NOT NULL
metadata bytea NULL DEFAULT NULL
);
CREATE TABLE IF NOT EXISTS user_storage (
CREATE TABLE IF NOT EXISTS user_attrs (
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
@ -25,7 +26,7 @@ CREATE TABLE IF NOT EXISTS user_storage (
PRIMARY KEY (key, user_id)
);
CREATE TABLE user_tokens (
CREATE TABLE IF NOT EXISTS tokens (
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token text NOT NULL,
@ -35,7 +36,7 @@ CREATE TABLE user_tokens (
PRIMARY KEY (token, user_id)
);
CREATE TABLE sessions (
CREATE TABLE IF NOT EXISTS sessions (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
@ -56,17 +57,19 @@ VALUES ('00000000-0000-0000-0000-000000000000'::uuid,
'!',
'{}');
CREATE UNIQUE INDEX users_username_idx
ON users USING btree (username)
CREATE UNIQUE INDEX users__username__idx
ON users (username)
WHERE deleted_at is null;
CREATE UNIQUE INDEX users_email_idx
ON users USING btree (email)
CREATE UNIQUE INDEX users__email__idx
ON users (email)
WHERE deleted_at is null;
CREATE TRIGGER users_modified_at_tgr BEFORE UPDATE ON users
CREATE TRIGGER users__modified_at__tgr
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE PROCEDURE update_modified_at();
CREATE TRIGGER user_storage_modified_at_tgr BEFORE UPDATE ON user_storage
CREATE TRIGGER user_attrs__modified_at__tgr
BEFORE UPDATE ON user_attrs
FOR EACH ROW EXECUTE PROCEDURE update_modified_at();

View file

@ -8,43 +8,150 @@ CREATE TABLE IF NOT EXISTS projects (
modified_at timestamptz NOT NULL DEFAULT clock_timestamp(),
deleted_at timestamptz DEFAULT NULL,
name text NOT NULL
name text NOT NULL,
metadata bytea NULL DEFAULT NULL
);
CREATE TABLE IF NOT EXISTS projects_users (
CREATE TABLE IF NOT EXISTS project_users (
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
modified_at timestamptz NOT NULL DEFAULT clock_timestamp(),
role text NOT NULL,
can_edit boolean DEFAULT false,
PRIMARY KEY (user_id, project_id)
);
CREATE TABLE IF NOT EXISTS project_files (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
name text NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
modified_at timestamptz NOT NULL DEFAULT clock_timestamp(),
deleted_at timestamptz DEFAULT NULL,
metadata bytea NULL DEFAULT NULL
);
CREATE TABLE IF NOT EXISTS project_file_users (
file_id uuid NOT NULL REFERENCES project_files(id) ON DELETE CASCADE,
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
modified_at timestamptz NOT NULL DEFAULT clock_timestamp(),
can_edit boolean DEFAULT false,
PRIMARY KEY (user_id, file_id)
);
CREATE TABLE IF NOT EXISTS project_pages (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
file_id uuid NOT NULL REFERENCES project_files(id) ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
modified_at timestamptz NOT NULL DEFAULT clock_timestamp(),
deleted_at timestamptz DEFAULT NULL,
version bigint NOT NULL,
ordering smallint NOT NULL,
name text NOT NULL,
data bytea NOT NULL,
metadata bytea NULL DEFAULT NULL
);
CREATE TABLE IF NOT EXISTS project_page_history (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id uuid NULL REFERENCES users(id) ON DELETE SET NULL,
page_id uuid NOT NULL REFERENCES project_pages(id) ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
modified_at timestamptz NOT NULL DEFAULT clock_timestamp(),
version bigint NOT NULL DEFAULT 0,
pinned bool NOT NULL DEFAULT false,
label text NOT NULL DEFAULT '',
data bytea NOT NULL
);
-- Indexes
CREATE INDEX projects_user_idx ON projects(user_id);
CREATE INDEX projects_users_user_id_idx ON projects_users(project_id);
CREATE INDEX projects_users_project_id_idx ON projects_users(user_id);
CREATE INDEX projects__user_id__idx ON projects(user_id);
CREATE INDEX project_files__user_id__idx ON project_files(user_id);
CREATE INDEX project_files__project_id__idx ON project_files(project_id);
CREATE INDEX project_pages__user_id__idx ON project_pages(user_id);
CREATE INDEX project_pages__file_id__idx ON project_pages(file_id);
CREATE INDEX project_page_history__page_id__idx ON project_page_history(page_id);
CREATE INDEX project_page_history__user_id__idx ON project_page_history(user_id);
-- Triggers
CREATE OR REPLACE FUNCTION handle_project_insert()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO projects_users (user_id, project_id, role)
VALUES (NEW.user_id, NEW.id, 'owner');
INSERT INTO project_users (user_id, project_id, can_edit)
VALUES (NEW.user_id, NEW.id, true);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION handle_page_update()
RETURNS TRIGGER AS $pagechange$
DECLARE
current_dt timestamptz := clock_timestamp();
proj_id uuid;
BEGIN
UPDATE project_files
SET modified_at = current_dt
WHERE id = OLD.file_id
RETURNING project_id
INTO STRICT proj_id;
--- Update projects modified_at attribute when a
--- page of that project is modified.
UPDATE projects
SET modified_at = current_dt
WHERE id = proj_id;
RETURN NEW;
END;
$pagechange$ LANGUAGE plpgsql;
CREATE TRIGGER projects_on_insert_tgr
AFTER INSERT ON projects
FOR EACH ROW EXECUTE PROCEDURE handle_project_insert();
CREATE TRIGGER projects_modified_at_tgr
CREATE TRIGGER pages__on_update__tgr
BEFORE UPDATE ON project_pages
FOR EACH ROW EXECUTE PROCEDURE handle_page_update();
CREATE TRIGGER projects__modified_at__tgr
BEFORE UPDATE ON projects
FOR EACH ROW EXECUTE PROCEDURE update_modified_at();
CREATE TRIGGER project_files__modified_at__tgr
BEFORE UPDATE ON project_files
FOR EACH ROW EXECUTE PROCEDURE update_modified_at();
CREATE TRIGGER project_pages__modified_at__tgr
BEFORE UPDATE ON project_pages
FOR EACH ROW EXECUTE PROCEDURE update_modified_at();
CREATE TRIGGER project_page_history__modified_at__tgr
BEFORE UPDATE ON project_page_history
FOR EACH ROW EXECUTE PROCEDURE update_modified_at();

View file

@ -1,65 +0,0 @@
-- Tables
CREATE TABLE IF NOT EXISTS pages (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
modified_at timestamptz NOT NULL DEFAULT clock_timestamp(),
deleted_at timestamptz DEFAULT NULL,
version bigint NOT NULL,
ordering smallint NOT NULL,
name text NOT NULL,
data bytea NOT NULL,
metadata bytea NOT NULL
);
CREATE TABLE IF NOT EXISTS pages_history (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
page_id uuid NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
modified_at timestamptz NOT NULL DEFAULT clock_timestamp(),
version bigint NOT NULL DEFAULT 0,
pinned bool NOT NULL DEFAULT false,
label text NOT NULL DEFAULT '',
data bytea NOT NULL,
metadata bytea NOT NULL
);
-- Indexes
CREATE INDEX pages_project_idx ON pages(project_id);
CREATE INDEX pages_user_idx ON pages(user_id);
CREATE INDEX pages_history_page_idx ON pages_history(page_id);
CREATE INDEX pages_history_user_idx ON pages_history(user_id);
-- Triggers
CREATE OR REPLACE FUNCTION handle_page_update()
RETURNS TRIGGER AS $pagechange$
BEGIN
--- Update projects modified_at attribute when a
--- page of that project is modified.
UPDATE projects SET modified_at = clock_timestamp()
WHERE id = OLD.project_id;
RETURN NEW;
END;
$pagechange$ LANGUAGE plpgsql;
CREATE TRIGGER page_on_update_tgr BEFORE UPDATE ON pages
FOR EACH ROW EXECUTE PROCEDURE handle_page_update();
CREATE TRIGGER pages_modified_at_tgr BEFORE UPDATE ON pages
FOR EACH ROW EXECUTE PROCEDURE update_modified_at();
CREATE TRIGGER pages_history_modified_at_tgr BEFORE UPDATE ON pages
FOR EACH ROW EXECUTE PROCEDURE update_modified_at();

View file

@ -1,6 +1,6 @@
-- Tables
CREATE TABLE IF NOT EXISTS images_collections (
CREATE TABLE IF NOT EXISTS image_collections (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS images (
width int NOT NULL,
height int NOT NULL,
mimetype text NOT NULL,
collection_id uuid REFERENCES images_collections(id)
collection_id uuid REFERENCES image_collections(id)
ON DELETE SET NULL
DEFAULT NULL,
name text NOT NULL,
@ -31,20 +31,17 @@ CREATE TABLE IF NOT EXISTS images (
-- Indexes
CREATE INDEX images_collections_user_idx
ON images_collections (user_id);
CREATE INDEX images_collection_idx
ON images (collection_id);
CREATE INDEX images_user_idx
ON images (user_id);
CREATE INDEX image_collections__user_id__idx ON image_collections (user_id);
CREATE INDEX images__collection_id__idx ON images (collection_id);
CREATE INDEX images__user_id__idx ON images (user_id);
-- Triggers
CREATE TRIGGER images_collections_modified_at_tgr BEFORE UPDATE ON images_collections
CREATE TRIGGER image_collections__modified_at__tgr
BEFORE UPDATE ON image_collections
FOR EACH ROW EXECUTE PROCEDURE update_modified_at();
CREATE TRIGGER images_modified_at_tgr BEFORE UPDATE ON images
FOR EACH ROW EXECUTE PROCEDURE update_modified_at();
CREATE TRIGGER images__modified_at__tgr
BEFORE UPDATE ON images
FOR EACH ROW EXECUTE PROCEDURE update_modified_at();

View file

@ -1,6 +1,6 @@
-- Tables
CREATE TABLE IF NOT EXISTS icons_collections (
CREATE TABLE IF NOT EXISTS icon_collections (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
@ -23,27 +23,23 @@ CREATE TABLE IF NOT EXISTS icons (
content text NOT NULL,
metadata bytea NOT NULL,
collection_id uuid REFERENCES icons_collections(id)
collection_id uuid REFERENCES icon_collections(id)
ON DELETE SET NULL
DEFAULT NULL
);
-- Indexes
CREATE INDEX icon_colections_user_idx
ON icons_collections (user_id);
CREATE INDEX icons_user_idx
ON icons (user_id);
CREATE INDEX icons_collection_idx
ON icons (collection_id);
CREATE INDEX icon_colections__user_id__idx ON icon_collections (user_id);
CREATE INDEX icons__user_id__idx ON icons(user_id);
CREATE INDEX icons__collection_id__idx ON icons(collection_id);
-- Triggers
CREATE TRIGGER icons_collections_modified_at_tgr BEFORE UPDATE ON icons_collections
FOR EACH ROW EXECUTE PROCEDURE update_modified_at();
CREATE TRIGGER icons_modified_at_tgr BEFORE UPDATE ON icons
FOR EACH ROW EXECUTE PROCEDURE update_modified_at();
CREATE TRIGGER icon_collections__modified_at__tgr
BEFORE UPDATE ON icon_collections
FOR EACH ROW EXECUTE PROCEDURE update_modified_at();
CREATE TRIGGER icons__modified_at__tgr
BEFORE UPDATE ON icons
FOR EACH ROW EXECUTE PROCEDURE update_modified_at();

View file

@ -50,7 +50,7 @@
:email-reply-to (lookup-env env :uxbox-email-reply-to "no-reply@uxbox.io")
:email-from (lookup-env env :uxbox-email-from "no-reply@uxbox.io")
:smtp-host (lookup-env env :uxbox-smtp-host "smtp")
:smtp-host (lookup-env env :uxbox-smtp-host "localhost")
:smtp-port (lookup-env env :uxbox-smtp-port 25)
:smtp-user (lookup-env env :uxbox-smtp-user nil)
:smtp-password (lookup-env env :uxbox-smtp-password nil)

View file

@ -13,6 +13,7 @@
[uxbox.config :as cfg]
[uxbox.core :refer [system]]
[uxbox.util.data :as data]
[uxbox.util.exceptions :as ex]
[uxbox.util.pgsql :as pg]
[vertx.core :as vx])
(:import io.vertx.core.buffer.Buffer))
@ -33,17 +34,22 @@
:start (create-pool cfg/config system))
(defmacro with-atomic
[& args]
`(pg/with-atomic ~@args))
[bindings & args]
`(pg/with-atomic ~bindings (p/do! ~@args)))
(def row-xfm
(comp (map pg/row->map)
(map data/normalize-attrs)))
(defmacro query
[& args]
`(pg/query ~@args {:xfm row-xfm}))
[conn sql]
`(-> (pg/query ~conn ~sql {:xfm row-xfm})
(p/catch' (fn [err#]
(ex/raise :type :database-error
:cause err#)))))
(defmacro query-one
[& args]
`(pg/query-one ~@args {:xfm row-xfm}))
[conn sql]
`(-> (pg/query-one ~conn ~sql {:xfm row-xfm})
(p/catch' (fn [err#]
(ex/raise :type :database-error
:cause err#)))))

View file

@ -7,6 +7,7 @@
(ns uxbox.fixtures
"A initial fixtures."
(:require
[clojure.tools.logging :as log]
[buddy.hashers :as hashers]
[mount.core :as mount]
[promesa.core :as p]
@ -20,26 +21,27 @@
(defn- mk-uuid
[prefix & args]
(uuid/namespaced uuid/oid (apply str prefix args)))
(uuid/namespaced uuid/oid (apply str prefix (interpose "-" args))))
;; --- Users creation
(def create-user-sql
"insert into users (id, fullname, username, email, password, metadata, photo)
values ($1, $2, $3, $4, $5, $6, $7)
"insert into users (id, fullname, username, email, password, photo)
values ($1, $2, $3, $4, $5, $6)
returning *;")
(def password (hashers/encrypt "123123"))
(defn create-user
[conn i]
(println "create user" i)
(db/query-one conn [create-user-sql
(mk-uuid "user" i)
(str "User " i)
(str "user" i)
(str "user" i ".test@uxbox.io")
(hashers/encrypt "123123")
(blob/encode {})
""]))
[conn user-index]
(log/info "create user" user-index)
(let [sql create-user-sql
id (mk-uuid "user" user-index)
fullname (str "User " user-index)
username (str "user" user-index)
email (str "user" user-index ".test@uxbox.io")
photo ""]
(db/query-one conn [sql id fullname username email password photo])))
;; --- Projects creation
@ -49,29 +51,46 @@
returning *;")
(defn create-project
[conn [pjid uid]]
(println "create project" pjid "(for user=" uid ")")
(db/query-one conn [create-project-sql
(mk-uuid "project" pjid uid)
(mk-uuid "user" uid)
(str "sample project " pjid)]))
[conn [project-index user-index]]
(log/info "create project" user-index project-index)
(let [sql create-project-sql
id (mk-uuid "project" project-index user-index)
user-id (mk-uuid "user" user-index)
name (str "sample project " project-index)]
(db/query-one conn [sql id user-id name])))
;; --- Pages creation
;; --- Create Page Files
(def create-file-sql
"insert into project_files (id, user_id, project_id, name)
values ($1, $2, $3, $4) returning id")
(defn create-file
[conn [file-index project-index user-index]]
(log/info "create page file" user-index project-index file-index)
(let [sql create-file-sql
id (mk-uuid "page-file" file-index project-index user-index)
user-id (mk-uuid "user" user-index)
project-id (mk-uuid "project" project-index user-index)
name (str "Sample file " file-index)]
(db/query-one conn [sql id user-id project-id name])))
;; --- Create Pages
(def create-page-sql
"insert into pages (id, user_id, project_id, name,
"insert into project_pages (id, user_id, file_id, name,
version, ordering, data, metadata)
values ($1, $2, $3, $4, $5, $6, $7, $8)
returning id;")
(def create-page-history-sql
"insert into pages_history (page_id, user_id, version, data, metadata)
values ($1, $2, $3, $4, $5)
"insert into project_page_history (page_id, user_id, version, data)
values ($1, $2, $3, $4)
returning id;")
(defn create-page
[conn [pjid paid uid]]
(println "create page" paid "(for project=" pjid ", user=" uid ")")
[conn [page-index file-index project-index user-index]]
(log/info "create page" user-index project-index file-index page-index)
(let [canvas {:id (mk-uuid "canvas" 1)
:name "Canvas-1"
:type :canvas
@ -82,29 +101,61 @@
data {:shapes []
:canvas [(:id canvas)]
:shapes-by-id {(:id canvas) canvas}}
sql1 create-page-sql
sql2 create-page-history-sql
id (mk-uuid "page" page-index file-index project-index user-index)
user-id (mk-uuid "user" user-index)
file-id (mk-uuid "page-file" file-index project-index user-index)
name (str "page " page-index)
version 0
ordering page-index
data (blob/encode data)
mdata (blob/encode {})]
(p/do!
(db/query-one conn [create-page-sql
(mk-uuid "page" pjid paid uid)
(mk-uuid "user" uid)
(mk-uuid "project" pjid uid)
(str "page " paid)
0
paid
data
mdata])
(db/query-one conn [create-page-history-sql
(mk-uuid "page" pjid paid uid)
(mk-uuid "user" uid)
0
data
mdata]))))
(db/query-one conn [sql1 id user-id file-id name version ordering data mdata])
#_(db/query-one conn [sql2 id user-id version data]))))
(def preset-small
{:users 50
:projects 5
:files 5
:pages 3})
(def num-users 5)
(def num-projects 5)
(def num-pages 5)
(def preset-medium
{:users 500
:projects 20
:files 5
:pages 3})
(def preset-big
{:users 5000
:projects 50
:files 5
:pages 4})
(defn run
[opts]
(db/with-atomic [conn db/pool]
(p/do!
(p/run! #(create-user conn %) (range (:users opts)))
(p/run! #(create-project conn %)
(for [user-index (range (:users opts))
project-index (range (:projects opts))]
[project-index user-index]))
(p/run! #(create-file conn %)
(for [user-index (range (:users opts))
project-index (range (:projects opts))
file-index (range (:files opts))]
[file-index project-index user-index]))
(p/run! #(create-page conn %)
(for [user-index (range (:users opts))
project-index (range (:projects opts))
file-index (range (:files opts))
page-index (range (:pages opts))]
[page-index file-index project-index user-index]))
(p/promise nil))))
(defn -main
[& args]
@ -115,18 +166,12 @@
#'uxbox.db/pool
#'uxbox.migrations/migrations})
(mount/start))
@(db/with-atomic [conn db/pool]
(p/do!
(p/run! #(create-user conn %) (range num-users))
(p/run! #(create-project conn %)
(for [uid (range num-users)
pjid (range num-projects)]
[pjid uid]))
(p/run! #(create-page conn %)
(for [pjid(range num-projects)
paid (range num-pages)
uid (range num-users)]
[pjid paid uid]))
(p/promise 1)))
(let [preset (case (first args)
(nil "small") preset-small
"medium" preset-medium
"big" preset-big
preset-small)]
(log/info "Using preset:" (pr-str preset))
(deref (run preset)))
(finally
(mount/stop))))

View file

@ -8,6 +8,7 @@
"A errors handling for the http server."
(:require
[clojure.tools.logging :as log]
[cuerdas.core :as str]
[io.aviso.exception :as e]))
(defmulti handle-exception
@ -16,9 +17,18 @@
(defmethod handle-exception :validation
[err req]
(let [response (ex-data err)]
{:status 400
:body response}))
(let [header (get-in req [:headers "accept"])
response (ex-data err)]
(cond
(and (str/starts-with? header "text/html")
(= :spec-validation (:code response)))
{:status 400
:headers {"content-type" "text/html"}
:body (str "<pre style='font-size:16px'>" (:explain response) "</pre>\n")}
:else
{:status 400
:body response})))
(defmethod handle-exception :not-found
[err req]
@ -26,6 +36,10 @@
{:status 404
:body response}))
(defmethod handle-exception :service-error
[err req]
(handle-exception (.getCause err) req))
(defmethod handle-exception :parse
[err req]
{:status 400

View file

@ -26,18 +26,15 @@
{:desc "Initial projects tables"
:name "0003-projects"
:fn (mg/resource "migrations/0003.projects.sql")}
{:desc "Initial pages tables"
:name "0004-pages"
:fn (mg/resource "migrations/0004.pages.sql")}
{:desc "Initial emails related tables"
:name "0005-emails"
:fn (mg/resource "migrations/0005.emails.sql")}
:name "0004-emails"
:fn (mg/resource "migrations/0004.emails.sql")}
{:desc "Initial images tables"
:name "0006-images"
:fn (mg/resource "migrations/0006.images.sql")}
:name "0005-images"
:fn (mg/resource "migrations/0005.images.sql")}
{:desc "Initial icons tables"
:name "0007-icons"
:fn (mg/resource "migrations/0007.icons.sql")}
:name "0006-icons"
:fn (mg/resource "migrations/0006.icons.sql")}
]})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -13,20 +13,22 @@
[]
(require 'uxbox.services.queries.icons)
(require 'uxbox.services.queries.images)
(require 'uxbox.services.queries.pages)
(require 'uxbox.services.queries.profiles)
(require 'uxbox.services.queries.projects)
(require 'uxbox.services.queries.user-storage))
(require 'uxbox.services.queries.project-files)
(require 'uxbox.services.queries.project-pages)
(require 'uxbox.services.queries.users)
(require 'uxbox.services.queries.user-attrs))
(defn- load-mutation-services
[]
(require 'uxbox.services.mutations.auth)
(require 'uxbox.services.mutations.icons)
(require 'uxbox.services.mutations.images)
(require 'uxbox.services.mutations.projects)
(require 'uxbox.services.mutations.pages)
(require 'uxbox.services.mutations.profiles)
(require 'uxbox.services.mutations.user-storage))
(require 'uxbox.services.mutations.project-files)
(require 'uxbox.services.mutations.project-pages)
(require 'uxbox.services.mutations.auth)
(require 'uxbox.services.mutations.users)
(require 'uxbox.services.mutations.user-attrs))
(defstate query-services
:start (load-query-services))

View file

@ -6,11 +6,13 @@
(ns uxbox.services.mutations
(:require
[uxbox.util.dispatcher :as uds]))
[uxbox.util.dispatcher :as uds]
[uxbox.util.exceptions :as ex]))
(uds/defservice handle
{:dispatch-by ::type
:interceptors [uds/spec-interceptor
uds/wrap-errors
#_logging-interceptor
#_context-interceptor]})

View file

@ -1,174 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.services.mutations.pages
(:require
[clojure.spec.alpha :as s]
[promesa.core :as p]
[uxbox.db :as db]
[uxbox.util.spec :as us]
[uxbox.services.mutations :as sm]
[uxbox.services.util :as su]
[uxbox.services.queries.pages :refer [decode-row]]
[uxbox.util.sql :as sql]
[uxbox.util.blob :as blob]
[uxbox.util.uuid :as uuid]))
;; --- Helpers & Specs
;; TODO: validate `:data` and `:metadata`
(s/def ::id ::us/uuid)
(s/def ::name ::us/string)
(s/def ::data any?)
(s/def ::user ::us/uuid)
(s/def ::project-id ::us/uuid)
(s/def ::metadata any?)
(s/def ::ordering ::us/number)
;; --- Mutation: Create Page
(declare create-page)
(s/def ::create-page
(s/keys :req-un [::data ::user ::project-id ::name ::metadata]
:opt-un [::id]))
(sm/defmutation ::create-page
[params]
(create-page db/pool params))
(defn create-page
[conn {:keys [id user project-id name ordering data metadata] :as params}]
(let [sql "insert into pages (id, user_id, project_id, name,
ordering, data, metadata, version)
values ($1, $2, $3, $4, $5, $6, $7, 0)
returning *"
id (or id (uuid/next))
data (blob/encode data)
mdata (blob/encode metadata)]
(-> (db/query-one db/pool [sql id user project-id name ordering data mdata])
(p/then' decode-row))))
;; --- Mutation: Update Page
(s/def ::update-page
(s/keys :req-un [::data ::user ::project-id ::name ::data ::metadata ::id]))
(letfn [(select-for-update [conn id]
(let [sql "select p.id, p.version
from pages as p
where p.id = $1
and deleted_at is null
for update;"]
(-> (db/query-one conn [sql id])
(p/then' su/raise-not-found-if-nil))))
(update-page [conn {:keys [id name version data metadata user]}]
(let [sql "update pages
set name = $1,
version = $2,
data = $3,
metadata = $4
where id = $5
and user_id = $6"]
(-> (db/query-one conn [sql name version data metadata id user])
(p/then' su/constantly-nil))))
(update-history [conn {:keys [user id version data metadata]}]
(let [sql "insert into pages_history (user_id, page_id, version, data, metadata)
values ($1, $2, $3, $4, $5)"]
(-> (db/query-one conn [sql user id version data metadata])
(p/then' su/constantly-nil))))]
(sm/defmutation ::update-page
[{:keys [id data metadata] :as params}]
(db/with-atomic [conn db/pool]
(-> (select-for-update conn id)
(p/then (fn [{:keys [id version]}]
(let [data (blob/encode data)
mdata (blob/encode metadata)
version (inc version)
params (assoc params
:id id
:version version
:data data
:metadata mdata)]
(p/do! (update-page conn params)
(update-history conn params)
(select-keys params [:id :version])))))))))
;; --- Mutation: Rename Page
(s/def ::rename-page
(s/keys :req-un [::id ::name ::user]))
(sm/defmutation ::rename-page
[{:keys [id name user]}]
(let [sql "update pages
set name = $3
where id = $1
and user_id = $2
and deleted_at is null"]
(-> (db/query-one db/pool [sql id user name])
(p/then su/constantly-nil))))
;; --- Mutation: Update Page Metadata
(s/def ::update-page-metadata
(s/keys :req-un [::user ::project-id ::name ::metadata ::id]))
(sm/defmutation ::update-page-metadata
[{:keys [id user project-id name metadata]}]
(let [sql "update pages
set name = $3,
metadata = $4
where id = $1
and user_id = $2
and deleted_at is null
returning *"
mdata (blob/encode metadata)]
(-> (db/query-one db/pool [sql id user name mdata])
(p/then' decode-row))))
;; --- Mutation: Delete Page
(s/def ::delete-page
(s/keys :req-un [::user ::id]))
(sm/defmutation ::delete-page
[{:keys [id user]}]
(let [sql "update pages
set deleted_at = clock_timestamp()
where id = $1
and user_id = $2
and deleted_at is null
returning id"]
(-> (db/query-one db/pool [sql id user])
(p/then su/raise-not-found-if-nil)
(p/then su/constantly-nil))))
;; ;; --- Update Page History
;; (defn update-page-history
;; [conn {:keys [user id label pinned]}]
;; (let [sqlv (sql/update-page-history {:user user
;; :id id
;; :label label
;; :pinned pinned})]
;; (some-> (db/fetch-one conn sqlv)
;; (decode-row))))
;; (s/def ::label ::us/string)
;; (s/def ::update-page-history
;; (s/keys :req-un [::user ::id ::pinned ::label]))
;; (sm/defmutation :update-page-history
;; {:doc "Update page history"
;; :spec ::update-page-history}
;; [params]
;; (with-open [conn (db/connection)]
;; (update-page-history conn params)))

View file

@ -0,0 +1,142 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.services.mutations.project-files
(:require
[clojure.spec.alpha :as s]
[promesa.core :as p]
[uxbox.db :as db]
[uxbox.util.spec :as us]
[uxbox.services.mutations :as sm]
[uxbox.services.mutations.projects :as proj]
[uxbox.services.util :as su]
[uxbox.util.exceptions :as ex]
[uxbox.util.blob :as blob]
[uxbox.util.uuid :as uuid]))
;; --- Helpers & Specs
(s/def ::id ::us/uuid)
(s/def ::name ::us/string)
(s/def ::user ::us/uuid)
(s/def ::project-id ::us/uuid)
;; --- Permissions Checks
;; A query that returns all (not-equal) user assignations for a
;; requested file (project level and file level).
;; Is important having the condition of user_id in the join and not in
;; where clause because we need all results independently if value is
;; true, false or null; with that, the empty result means there are no
;; file found.
(def ^:private sql:file-permissions
"select pf.id,
pfu.can_edit as can_edit
from project_files as pf
left join project_file_users as pfu
on (pfu.file_id = pf.id and pfu.user_id = $1)
where pf.id = $2
union all
select pf.id,
pu.can_edit as can_edit
from project_files as pf
left join project_users as pu
on (pf.project_id = pu.project_id and pu.user_id = $1)
where pf.id = $2")
(defn check-edition-permissions!
[conn user file-id]
(-> (db/query conn [sql:file-permissions user file-id])
(p/then' seq)
(p/then' su/raise-not-found-if-nil)
(p/then' (fn [rows]
(when-not (some :can-edit rows)
(ex/raise :type :validation
:code :not-authorized))))))
;; --- Mutation: Create Project
(declare create-file)
(declare create-page)
(s/def ::create-project-file
(s/keys :req-un [::user ::name ::project-id]
:opt-un [::id]))
(sm/defmutation ::create-project-file
[{:keys [user project-id] :as params}]
(db/with-atomic [conn db/pool]
(proj/check-edition-permissions! conn user project-id)
(p/let [file (create-file conn params)]
(create-page conn (assoc params :file-id (:id file)))
file)))
(defn create-file
[conn {:keys [id user name project-id] :as params}]
(let [id (or id (uuid/next))
sql "insert into project_files (id, user_id, project_id, name)
values ($1, $2, $3, $4) returning *"]
(db/query-one conn [sql id user project-id name])))
(defn- create-page
"Creates an initial page for the file."
[conn {:keys [user file-id] :as params}]
(let [id (uuid/next)
name "Page 1"
data (blob/encode {})
sql "insert into project_pages (id, user_id, file_id, name, version,
ordering, data)
values ($1, $2, $3, $4, 0, 1, $5) returning id"]
(db/query-one conn [sql id user file-id name data])))
;; --- Mutation: Update Project
(declare update-file)
(s/def ::update-project-file
(s/keys :req-un [::user ::name ::id]))
(sm/defmutation ::update-project-file
[{:keys [id user] :as params}]
(db/with-atomic [conn db/pool]
(check-edition-permissions! conn user id)
(update-file conn params)))
(defn- update-file
[conn {:keys [id name user] :as params}]
(let [sql "update project_files
set name = $2
where id = $1
and deleted_at is null"]
(-> (db/query-one conn [sql id name])
(p/then' su/constantly-nil))))
;; --- Mutation: Delete Project
(declare delete-file)
(s/def ::delete-project-file
(s/keys :req-un [::id ::user]))
(sm/defmutation ::delete-project-file
[{:keys [id user] :as params}]
(db/with-atomic [conn db/pool]
(check-edition-permissions! conn user id)
(delete-file conn params)))
(def ^:private sql:delete-file
"update project_files
set deleted_at = clock_timestamp()
where id = $1
and deleted_at is null")
(defn delete-file
[conn {:keys [id] :as params}]
(let [sql sql:delete-file]
(-> (db/query-one conn [sql id])
(p/then' su/constantly-nil))))

View file

@ -0,0 +1,190 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.services.mutations.project-pages
(:require
[clojure.spec.alpha :as s]
[promesa.core :as p]
[uxbox.db :as db]
[uxbox.services.mutations :as sm]
[uxbox.services.mutations.project-files :as files]
[uxbox.services.queries.project-pages :refer [decode-row]]
[uxbox.services.util :as su]
[uxbox.util.blob :as blob]
[uxbox.util.spec :as us]
[uxbox.util.sql :as sql]
[uxbox.util.uuid :as uuid]))
;; --- Helpers & Specs
;; TODO: validate `:data` and `:metadata`
(s/def ::id ::us/uuid)
(s/def ::name ::us/string)
(s/def ::data any?)
(s/def ::user ::us/uuid)
(s/def ::project-id ::us/uuid)
(s/def ::metadata any?)
(s/def ::ordering ::us/number)
;; --- Mutation: Create Page
(declare create-page)
(s/def ::create-project-page
(s/keys :req-un [::user ::file-id ::name ::ordering ::metadata ::data]
:opt-un [::id]))
(sm/defmutation ::create-project-page
[{:keys [user file-id] :as params}]
(db/with-atomic [conn db/pool]
(files/check-edition-permissions! conn user file-id)
(create-page conn params)))
(defn create-page
[conn {:keys [id user file-id name ordering data metadata] :as params}]
(let [sql "insert into project_pages (id, user_id, file_id, name,
ordering, data, metadata, version)
values ($1, $2, $3, $4, $5, $6, $7, 0)
returning *"
id (or id (uuid/next))
data (blob/encode data)
mdata (blob/encode metadata)]
(-> (db/query-one conn [sql id user file-id name ordering data mdata])
(p/then' decode-row))))
;; --- Mutation: Update Page
(declare select-page-for-update)
(declare update-page)
(declare update-history)
(s/def ::update-project-page-data
(s/keys :req-un [::id ::user ::data]))
(sm/defmutation ::update-project-page-data
[{:keys [id user data] :as params}]
(db/with-atomic [conn db/pool]
(p/let [{:keys [version file-id]} (select-page-for-update conn id)]
(files/check-edition-permissions! conn user file-id)
(let [data (blob/encode data)
version (inc version)
params (assoc params :id id :data data :version version)]
(p/do! (update-page conn params)
(update-history conn params)
(select-keys params [:id :version]))))))
(defn- select-page-for-update
[conn id]
(let [sql "select p.id, p.version, p.file_id
from project_pages as p
where p.id = $1
and deleted_at is null
for update;"]
(-> (db/query-one conn [sql id])
(p/then' su/raise-not-found-if-nil))))
(defn- update-page
[conn {:keys [id name version data metadata]}]
(let [sql "update project_pages
set version = $1,
data = $2
where id = $3"]
(-> (db/query-one conn [sql version data id])
(p/then' su/constantly-nil))))
(defn- update-history
[conn {:keys [user id version data]}]
(let [sql "insert into project_page_history (user_id, page_id, version, data)
values ($1, $2, $3, $4)"]
(-> (db/query-one conn [sql user id version data])
(p/then' su/constantly-nil))))
;; --- Mutation: Rename Page
(declare rename-page)
(s/def ::rename-project-page
(s/keys :req-un [::id ::name ::user]))
(sm/defmutation ::rename-project-page
[{:keys [id name user]}]
(db/with-atomic [conn db/pool]
(p/let [page (select-page-for-update conn id)]
(files/check-edition-permissions! conn user (:file-id page))
(rename-page conn (assoc page :name name)))))
(defn- rename-page
[conn {:keys [id name] :as params}]
(let [sql "update project_pages
set name = $2
where id = $1
and deleted_at is null"]
(-> (db/query-one db/pool [sql id name])
(p/then su/constantly-nil))))
;; --- Mutation: Update Page Metadata
;; (s/def ::update-page-metadata
;; (s/keys :req-un [::user ::project-id ::name ::metadata ::id]))
;; (sm/defmutation ::update-page-metadata
;; [{:keys [id user project-id name metadata]}]
;; (let [sql "update pages
;; set name = $3,
;; metadata = $4
;; where id = $1
;; and user_id = $2
;; and deleted_at is null
;; returning *"
;; mdata (blob/encode metadata)]
;; (-> (db/query-one db/pool [sql id user name mdata])
;; (p/then' decode-row))))
;; --- Mutation: Delete Page
(declare delete-page)
(s/def ::delete-project-page
(s/keys :req-un [::user ::id]))
(sm/defmutation ::delete-project-page
[{:keys [id user]}]
(db/with-atomic [conn db/pool]
(p/let [page (select-page-for-update conn id)]
(files/check-edition-permissions! conn user (:file-id page))
(delete-page conn id))))
(defn- delete-page
[conn id]
(let [sql "update project_pages
set deleted_at = clock_timestamp()
where id = $1
and deleted_at is null"]
(-> (db/query-one conn [sql id])
(p/then su/constantly-nil))))
;; --- Update Page History
;; (defn update-page-history
;; [conn {:keys [user id label pinned]}]
;; (let [sqlv (sql/update-page-history {:user user
;; :id id
;; :label label
;; :pinned pinned})]
;; (some-> (db/fetch-one conn sqlv)
;; (decode-row))))
;; (s/def ::label ::us/string)
;; (s/def ::update-page-history
;; (s/keys :req-un [::user ::id ::pinned ::label]))
;; (sm/defmutation :update-page-history
;; {:doc "Update page history"
;; :spec ::update-page-history}
;; [params]
;; (with-open [conn (db/connection)]
;; (update-page-history conn params)))

View file

@ -13,6 +13,7 @@
[uxbox.services.mutations :as sm]
[uxbox.services.util :as su]
[uxbox.util.blob :as blob]
[uxbox.util.exceptions :as ex]
[uxbox.util.uuid :as uuid]))
;; --- Helpers & Specs
@ -22,6 +23,27 @@
(s/def ::token ::us/string)
(s/def ::user ::us/uuid)
;; --- Permissions Checks
(def ^:private sql:project-permissions
"select p.id,
pu.can_edit as can_edit
from projects as p
inner join project_users as pu
on (pu.project_id = p.id)
where pu.user_id = $1
and p.id = $2
for update of p;")
(defn check-edition-permissions!
[conn user project-id]
(-> (db/query-one conn [sql:project-permissions user project-id])
(p/then' su/raise-not-found-if-nil)
(p/then' (fn [{:keys [id can-edit] :as proj}]
(when-not can-edit
(ex/raise :type :validation
:code :not-authorized))))))
;; --- Mutation: Create Project
(declare create-project)
@ -31,11 +53,9 @@
:opt-un [::id]))
(sm/defmutation ::create-project
[{:keys [id user name] :as params}]
(let [id (or id (uuid/next))
sql "insert into projects (id, user_id, name)
values ($1, $2, $3) returning *"]
(db/query-one db/pool [sql id user name])))
[params]
(db/with-atomic [conn db/pool]
(create-project conn params)))
(defn create-project
[conn {:keys [id user name] :as params}]
@ -46,32 +66,49 @@
;; --- Mutation: Update Project
(declare update-project)
(s/def ::update-project
(s/keys :req-un [::user ::name ::id]))
(sm/defmutation ::update-project
[{:keys [id name user] :as params}]
[{:keys [id user] :as params}]
(db/with-atomic [conn db/pool]
(check-edition-permissions! conn user id)
(update-project conn params)))
(defn update-project
[conn {:keys [id name user] :as params}]
(let [sql "update projects
set name = $3
where id = $1
and user_id = $2
and deleted_at is null
returning *"]
(db/query-one db/pool [sql id user name])))
(db/query-one conn [sql id user name])))
;; --- Mutation: Delete Project
(declare delete-project)
(s/def ::delete-project
(s/keys :req-un [::id ::user]))
(sm/defmutation ::delete-project
[{:keys [id user] :as params}]
(let [sql "update projects
set deleted_at = clock_timestamp()
where id = $1
and user_id = $2
and deleted_at is null
returning id"]
(-> (db/query-one db/pool [sql id user])
(p/then' su/raise-not-found-if-nil)
(db/with-atomic [conn db/pool]
(check-edition-permissions! conn user id)
(delete-project conn params)))
(def ^:private sql:delete-project
"update projects
set deleted_at = clock_timestamp()
where id = $1
and deleted_at is null
returning id")
(defn delete-project
[conn {:keys [id user] :as params}]
(let [sql sql:delete-project]
(-> (db/query-one conn [sql id])
(p/then' su/constantly-nil))))

View file

@ -4,14 +4,14 @@
;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.services.mutations.user-storage
(ns uxbox.services.mutations.user-attrs
(:require
[clojure.spec.alpha :as s]
[promesa.core :as p]
[uxbox.db :as db]
[uxbox.services.mutations :as sm]
[uxbox.services.util :as su]
[uxbox.services.queries.user-storage :refer [decode-row]]
[uxbox.services.queries.user-attrs :refer [decode-row]]
[uxbox.util.blob :as blob]
[uxbox.util.spec :as us]))
@ -21,12 +21,12 @@
(s/def ::key ::us/string)
(s/def ::val any?)
(s/def ::upsert-user-storage-entry
(s/def ::upsert-user-attr
(s/keys :req-un [::key ::val ::user]))
(sm/defmutation ::upsert-user-storage-entry
(sm/defmutation ::upsert-user-attr
[{:keys [key val user] :as params}]
(let [sql "insert into user_storage (key, val, user_id)
(let [sql "insert into user_attrs (key, val, user_id)
values ($1, $2, $3)
on conflict (user_id, key)
do update set val = $2"
@ -36,12 +36,12 @@
;; --- Delete KVStore
(s/def ::delete-user-storage-entry
(s/def ::delete-user-attr
(s/keys :req-un [::key ::user]))
(sm/defmutation ::delete-user-storage-entry
(sm/defmutation ::delete-user-attr
[{:keys [user key] :as params}]
(let [sql "delete from user_storage
(let [sql "delete from user_attrs
where user_id = $2
and key = $1"]
(-> (db/query-one db/pool [sql key user])

View file

@ -4,7 +4,7 @@
;;
;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.services.mutations.profiles
(ns uxbox.services.mutations.users
(:require
[buddy.hashers :as hashers]
[clojure.spec.alpha :as s]
@ -19,10 +19,10 @@
[uxbox.media :as media]
[uxbox.services.mutations :as sm]
[uxbox.services.util :as su]
[uxbox.services.queries.profiles :refer [get-profile
decode-profile-row
strip-private-attrs
resolve-thumbnail]]
[uxbox.services.queries.users :refer [get-profile
decode-profile-row
strip-private-attrs
resolve-thumbnail]]
[uxbox.util.blob :as blob]
[uxbox.util.exceptions :as ex]
[uxbox.util.spec :as us]
@ -56,7 +56,7 @@
and id != $1
) as val"]
(p/let [res1 (db/query-one conn [sql1 id username])
res2 (db/query-one conn [sql2 id email])]
res2 (db/query-one conn [sql2 id email])]
(when (:val res1)
(ex/raise :type :validation
:code ::username-already-exists))
@ -83,9 +83,7 @@
(s/def ::update-profile
(s/keys :req-un [::id ::username ::email ::fullname ::metadata]))
(sm/defmutation :update-profile
{:doc "Update self profile."
:spec ::update-profile}
(sm/defmutation ::update-profile
[params]
(db/with-atomic [conn db/pool]
(-> (p/resolved params)
@ -134,9 +132,7 @@
(def valid-image-types?
#{"image/jpeg", "image/png", "image/webp"})
(sm/defmutation :update-profile-photo
{:doc "Update profile photo."
:spec ::update-profile-photo}
(sm/defmutation ::update-profile-photo
[{:keys [user file] :as params}]
(letfn [(store-photo [{:keys [name path] :as upload}]
(let [filename (fs/name name)
@ -149,16 +145,17 @@
set photo = $1
where id = $2
and deleted_at is null
returning *"]
returning id, photo"]
(-> (db/query-one db/pool [sql (str path) user])
(p/then' su/raise-not-found-if-nil)
(p/then' strip-private-attrs)
;; (p/then' strip-private-attrs)
(p/then resolve-thumbnail))))]
(when-not (valid-image-types? (:mtype file))
(ex/raise :type :validation
:code :image-type-not-allowed
:hint "Seems like you are uploading an invalid image."))
(-> (store-photo file)
(p/then update-user-photo))))

View file

@ -11,6 +11,7 @@
(uds/defservice handle
{:dispatch-by ::type
:interceptors [uds/spec-interceptor
uds/wrap-errors
#_logging-interceptor
#_context-interceptor]})

View file

@ -1,92 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.services.queries.pages
(:require
[clojure.spec.alpha :as s]
[promesa.core :as p]
[uxbox.db :as db]
[uxbox.services.queries :as sq]
[uxbox.util.blob :as blob]
[uxbox.util.spec :as us]
[uxbox.util.sql :as sql]))
;; --- Helpers & Specs
(declare decode-row)
(s/def ::id ::us/uuid)
(s/def ::user ::us/uuid)
(s/def ::project-id ::us/uuid)
;; --- Query: Pages by Project
(s/def ::pages-by-project
(s/keys :req-un [::user ::project-id]))
(sq/defquery ::pages-by-project
[{:keys [user project-id] :as params}]
(let [sql "select pg.*,
pg.data,
pg.metadata
from pages as pg
where pg.user_id = $2
and pg.project_id = $1
and pg.deleted_at is null
order by pg.created_at asc;"]
(-> (db/query db/pool [sql project-id user])
(p/then #(mapv decode-row %)))))
;; --- Query: Page by Id
(s/def ::page
(s/keys :req-un [::user ::id]))
(sq/defquery ::page
[{:keys [user id] :as params}]
(let [sql "select pg.*,
pg.data,
pg.metadata
from pages as pg
where pg.user_id = $2
and pg.id = $1
and pg.deleted_at is null"]
(-> (db/query-one db/pool [sql id user])
(p/then' decode-row))))
;; --- Query: Page History
(s/def ::page-id ::us/uuid)
(s/def ::max ::us/integer)
(s/def ::pinned ::us/boolean)
(s/def ::since ::us/integer)
(s/def ::page-history
(s/keys :req-un [::page-id ::user]
:opt-un [::max ::pinned ::since]))
(sq/defquery ::page-history
[{:keys [page-id user since max pinned] :or {since Long/MAX_VALUE max 10}}]
(let [sql (-> (sql/from ["pages_history" "ph"])
(sql/select "ph.*")
(sql/where ["ph.user_id = ?" user]
["ph.page_id = ?" page-id]
["ph.version < ?" since]
(when pinned
["ph.pinned = ?" true]))
(sql/order "ph.version desc")
(sql/limit max))]
(-> (db/query db/pool (sql/fmt sql))
(p/then (partial mapv decode-row)))))
;; --- Helpers
(defn decode-row
[{:keys [data metadata] :as row}]
(when row
(cond-> row
data (assoc :data (blob/decode data))
metadata (assoc :metadata (blob/decode metadata)))))

View file

@ -0,0 +1,55 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.services.queries.project-files
(:require
[clojure.spec.alpha :as s]
[promesa.core :as p]
[uxbox.db :as db]
[uxbox.services.queries :as sq]
[uxbox.util.blob :as blob]
[uxbox.util.spec :as us]))
(declare decode-row)
;; --- Helpers & Specs
(s/def ::id ::us/uuid)
(s/def ::name ::us/string)
(s/def ::project-id ::us/uuid)
(s/def ::user ::us/uuid)
;; --- Query: Project Files
(def ^:private sql:project-files
"select pf.*,
array_agg(pp.id) as pages
from project_files as pf
inner join projects as p on (pf.project_id = p.id)
inner join project_users as pu on (p.id = pu.project_id)
left join project_pages as pp on (pf.id = pp.file_id)
where pu.user_id = $1
and pu.project_id = $2
and pu.can_edit = true
group by pf.id
order by pf.created_at asc;")
(s/def ::project-files
(s/keys :req-un [::user ::project-id]))
(sq/defquery ::project-files
[{:keys [user project-id] :as params}]
(-> (db/query db/pool [sql:project-files user project-id])
(p/then' (partial mapv decode-row))))
;; --- Helpers
(defn decode-row
[{:keys [metadata pages] :as row}]
(when row
(cond-> row
pages (assoc :pages (vec (remove nil? pages)))
metadata (assoc :metadata (blob/decode metadata)))))

View file

@ -0,0 +1,145 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.services.queries.project-pages
(:require
[clojure.spec.alpha :as s]
[promesa.core :as p]
[uxbox.db :as db]
[uxbox.services.queries :as sq]
[uxbox.services.util :as su]
[uxbox.util.blob :as blob]
[uxbox.util.spec :as us]
[uxbox.util.sql :as sql]))
;; --- Helpers & Specs
(declare decode-row)
(s/def ::id ::us/uuid)
(s/def ::user ::us/uuid)
(s/def ::project-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(def ^:private sql:generic-project-pages
"select pp.*
from project_pages as pp
inner join project_files as pf on (pf.id = pp.file_id)
inner join projects as p on (p.id = pf.project_id)
left join project_users as pu on (pu.project_id = p.id)
left join project_file_users as pfu on (pfu.file_id = pf.id)
where ((pfu.user_id = $1 and pfu.can_edit = true) or
(pu.user_id = $1 and pu.can_edit = true))
order by pp.created_at")
;; --- Query: Project Pages (By File ID)
(def ^:private sql:project-pages
(str "with pages as (" sql:generic-project-pages ")"
" select * from pages where file_id = $2"))
;; (defn project-pages-sql
;; [user]
;; (-> (sql/from ["project_pages" "pp"])
;; (sql/join ["project_files" "pf"] "pf.id = pp.file_id")
;; (sql/join ["projects" "p"] "p.id = pf.project_id")
;; (sql/ljoin ["project_users", "pu"] "pu.project_id = p.id")
;; (sql/ljoin ["project_file_users", "pfu"] "pfu.file_id = pf.id")
;; (sql/select "pp.*")
;; (sql/where ["((pfu.user_id = ? and pfu.can_edit = true) or
;; (pu.user_id = ? and pu.can_edit = true))" user user])
;; (sql/order "pp.created_at")))
;; (let [sql (-> (project-pages-sql user)
;; (sql/where ["pp.file_id = ?" file-id])
;; (sql/fmt))]
;; (-> (db/query db/pool sql)
;; (p/then #(mapv decode-row %)))))
(s/def ::project-pages
(s/keys :req-un [::user ::file-id]))
(sq/defquery ::project-pages
[{:keys [user file-id] :as params}]
(let [sql sql:project-pages]
(-> (db/query db/pool [sql user file-id])
(p/then #(mapv decode-row %)))))
;; --- Query: Project Page (By ID)
(def ^:private sql:project-page
(str "with pages as (" sql:generic-project-pages ")"
" select * from pages where id = $2"))
(defn retrieve-page
[conn {:keys [user id] :as params}]
(let [sql sql:project-page]
(-> (db/query-one conn [sql user id])
(p/then' su/raise-not-found-if-nil)
(p/then' decode-row))))
(s/def ::project-page
(s/keys :req-un [::user ::id]))
(sq/defquery ::project-page
[{:keys [user id] :as params}]
(retrieve-page db/pool params))
;; --- Query: Project Page History (by Page ID)
;; (def ^:private sql:generic-page-history
;; "select pph.*
;; from project_page_history as pph
;; where pph.page_id = $2
;; and pph.version < $3
;; order by pph.version < desc")
;; (def ^:private sql:page-history
;; (str "with history as (" sql:generic-page-history ")"
;; " select * from history limit $4"))
;; (def ^:private sql:pinned-page-history
;; (str "with history as (" sql:generic-page-history ")"
;; " select * from history where pinned = true limit $4"))
(s/def ::page-id ::us/uuid)
(s/def ::max ::us/integer)
(s/def ::pinned ::us/boolean)
(s/def ::since ::us/integer)
(s/def ::page-history
(s/keys :req-un [::page-id ::user]
:opt-un [::max ::pinned ::since]))
(defn retrieve-page-history
[{:keys [page-id user since max pinned] :or {since Long/MAX_VALUE max 10}}]
(let [sql (-> (sql/from ["pages_history" "ph"])
(sql/select "ph.*")
(sql/where ["ph.user_id = ?" user]
["ph.page_id = ?" page-id]
["ph.version < ?" since]
(when pinned
["ph.pinned = ?" true]))
(sql/order "ph.version desc")
(sql/limit max))]
(-> (db/query db/pool (sql/fmt sql))
(p/then (partial mapv decode-row)))))
(sq/defquery ::page-history
[{:keys [page-id user] :as params}]
(db/with-atomic [conn db/pool]
(p/do! (retrieve-page conn {:id page-id :user user})
(retrieve-page-history conn params))))
;; --- Helpers
(defn decode-row
[{:keys [data metadata] :as row}]
(when row
(cond-> row
data (assoc :data (blob/decode data))
metadata (assoc :metadata (blob/decode metadata)))))

View file

@ -13,6 +13,8 @@
[uxbox.util.blob :as blob]
[uxbox.util.spec :as us]))
(declare decode-row)
;; --- Helpers & Specs
(s/def ::id ::us/uuid)
@ -22,19 +24,26 @@
;; --- Query: Projects
;; (def ^:private projects-sql
;; "select distinct on (p.id, p.created_at)
;; p.*,
;; array_agg(pg.id) over (
;; partition by p.id
;; order by pg.created_at
;; range between unbounded preceding and unbounded following
;; ) as pages
;; from projects as p
;; left join pages as pg
;; on (pg.project_id = p.id)
;; where p.user_id = $1
;; order by p.created_at asc")
(def ^:private projects-sql
"select distinct on (p.id, p.created_at)
p.*,
array_agg(pg.id) over (
partition by p.id
order by pg.created_at
range between unbounded preceding and unbounded following
) as pages
from projects as p
left join pages as pg
on (pg.project_id = p.id)
where p.user_id = $1
order by p.created_at asc")
"select p.*
from project_users as pu
inner join projects as p on (p.id = pu.project_id)
where pu.can_edit = true
and pu.user_id = $1;")
(s/def ::projects
(s/keys :req-un [::user]))
@ -42,5 +51,12 @@
(sq/defquery ::projects
[{:keys [user] :as params}]
(-> (db/query db/pool [projects-sql user])
(p/then (fn [rows]
(mapv #(update % :pages vec) rows)))))
(p/then' (partial mapv decode-row))))
;; --- Helpers
(defn decode-row
[{:keys [metadata] :as row}]
(when row
(cond-> row
metadata (assoc :metadata (blob/decode metadata)))))

View file

@ -4,7 +4,7 @@
;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.services.queries.user-storage
(ns uxbox.services.queries.user-attrs
(:require
[clojure.spec.alpha :as s]
[promesa.core :as p]
@ -20,13 +20,13 @@
(cond-> row
val (assoc :val (blob/decode val)))))
(s/def ::user-storage-entry
(s/def ::user-attr
(s/keys :req-un [::key ::user]))
(sq/defquery ::user-storage-entry
(sq/defquery ::user-attr
[{:keys [key user]}]
(let [sql "select kv.*
from user_storage as kv
from user_attrs as kv
where kv.user_id = $2
and kv.key = $1"]
(-> (db/query-one db/pool [sql key user])

View file

@ -4,7 +4,7 @@
;;
;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.services.queries.profiles
(ns uxbox.services.queries.users
(:require
[clojure.spec.alpha :as s]
[promesa.core :as p]
@ -51,9 +51,7 @@
(s/def ::profile
(s/keys :req-un [::user]))
(sq/defquery :profile
{:doc "Retrieve the user profile."
:spec ::profile}
(sq/defquery ::profile
[{:keys [user] :as params}]
(-> (get-profile db/pool user)
(p/then' strip-private-attrs)))

View file

@ -28,7 +28,7 @@
IDispatcher
(add [this key f metadata]
(.put ^Map reg key (MapEntry/create f metadata))
nil)
this)
clojure.lang.IDeref
(deref [_]
@ -56,7 +56,7 @@
(defn dispatcher?
[v]
(instance? Dispatcher v))
(instance? IDispatcher v))
(defmacro defservice
[sname {:keys [dispatch-by interceptors]}]
@ -118,5 +118,16 @@
:code :spec-validation
:explain (with-out-str
(expound/printer data))
:data data))))
:data (::s/problems data)))))
data)))})
(def wrap-errors
{:error
(fn [data]
(let [error (:error data)
mdata (meta (:request data))]
(assoc data :error (ex/error :type :service-error
:name (:spec mdata)
:cause error))))})

View file

@ -12,7 +12,7 @@
(s/def ::code keyword?)
(s/def ::mesage string?)
(s/def ::hint string?)
(s/def ::cause #(instance? Exception %))
(s/def ::cause #(instance? Throwable %))
(s/def ::error-params
(s/keys :req-un [::type]
:opt-un [::code

View file

@ -4,213 +4,5 @@
;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.util.http)
(defn response
"Create a response instance."
([body] (response body 200 {}))
([body status] (response body status {}))
([body status headers] {:body body :status status :headers headers}))
(defn response?
[resp]
(and (map? resp)
(integer? (:status resp))
(map? (:headers resp))))
(defn continue
([body] (response body 100))
([body headers] (response body 100 headers)))
(defn ok
"HTTP 200 OK
Should be used to indicate nonspecific success. Must not be used to
communicate errors in the response body.
In most cases, 200 is the code the client hopes to see. It indicates that
the REST API successfully carried out whatever action the client requested,
and that no more specific code in the 2xx series is appropriate. Unlike
the 204 status code, a 200 response should include a response body."
([body] (response body 200))
([body headers] (response body 200 headers)))
(defn created
"HTTP 201 Created
Must be used to indicate successful resource creation.
A REST API responds with the 201 status code whenever a collection creates,
or a store adds, a new resource at the client's request. There may also be
times when a new resource is created as a result of some controller action,
in which case 201 would also be an appropriate response."
([location] (response "" 201 {"location" location}))
([location body] (response body 201 {"location" location}))
([location body headers] (response body 201 (merge headers {"location" location}))))
(defn accepted
"HTTP 202 Accepted
Must be used to indicate successful start of an asynchronous action.
A 202 response indicates that the client's request will be handled
asynchronously. This response status code tells the client that the request
appears valid, but it still may have problems once it's finally processed.
A 202 response is typically used for actions that take a long while to
process."
([body] (response body 202))
([body headers] (response body 202 headers)))
(defn no-content
"HTTP 204 No Content
Should be used when the response body is intentionally empty.
The 204 status code is usually sent out in response to a PUT, POST, or
DELETE request, when the REST API declines to send back any status message
or representation in the response message's body. An API may also send 204
in conjunction with a GET request to indicate that the requested resource
exists, but has no state representation to include in the body."
([] (response "" 204))
([headers] (response "" 204 headers)))
(defn moved-permanently
"301 Moved Permanently
Should be used to relocate resources.
The 301 status code indicates that the REST API's resource model has been
significantly redesigned and a new permanent URI has been assigned to the
client's requested resource. The REST API should specify the new URI in
the response's Location header."
([location] (response "" 301 {"location" location}))
([location body] (response body 301 {"location" location}))
([location body headers] (response body 301 (merge headers {"location" location}))))
(defn found
"HTTP 302 Found
Should not be used.
The intended semantics of the 302 response code have been misunderstood
by programmers and incorrectly implemented in programs since version 1.0
of the HTTP protocol.
The confusion centers on whether it is appropriate for a client to always
automatically issue a follow-up GET request to the URI in response's
Location header, regardless of the original request's method. For the
record, the intent of 302 is that this automatic redirect behavior only
applies if the client's original request used either the GET or HEAD
method.
To clear things up, HTTP 1.1 introduced status codes 303 (\"See Other\")
and 307 (\"Temporary Redirect\"), either of which should be used
instead of 302."
([location] (response "" 302 {"location" location}))
([location body] (response body 302 {"location" location}))
([location body headers] (response body 302 (merge headers {"location" location}))))
(defn see-other
"HTTP 303 See Other
Should be used to refer the client to a different URI.
A 303 response indicates that a controller resource has finished its work,
but instead of sending a potentially unwanted response body, it sends the
client the URI of a response resource. This can be the URI of a temporary
status message, or the URI to some already existing, more permanent,
resource.
Generally speaking, the 303 status code allows a REST API to send a
reference to a resource without forcing the client to download its state.
Instead, the client may send a GET request to the value of the Location
header."
([location] (response "" 303 {"location" location}))
([location body] (response body 303 {"location" location}))
([location body headers] (response body 303 (merge headers {"location" location}))))
(defn temporary-redirect
"HTTP 307 Temporary Redirect
Should be used to tell clients to resubmit the request to another URI.
HTTP/1.1 introduced the 307 status code to reiterate the originally
intended semantics of the 302 (\"Found\") status code. A 307 response
indicates that the REST API is not going to process the client's request.
Instead, the client should resubmit the request to the URI specified by
the response message's Location header.
A REST API can use this status code to assign a temporary URI to the
client's requested resource. For example, a 307 response can be used to
shift a client request over to another host."
([location] (response "" 307 {"location" location}))
([location body] (response body 307 {"location" location}))
([location body headers] (response body 307 (merge headers {"location" location}))))
(defn bad-request
"HTTP 400 Bad Request
May be used to indicate nonspecific failure.
400 is the generic client-side error status, used when no other 4xx error
code is appropriate."
([body] (response body 400))
([body headers] (response body 400 headers)))
(defn unauthorized
"HTTP 401 Unauthorized
Must be used when there is a problem with the client credentials.
A 401 error response indicates that the client tried to operate on a
protected resource without providing the proper authorization. It may have
provided the wrong credentials or none at all."
([body] (response body 401))
([body headers] (response body 401 headers)))
(defn forbidden
"HTTP 403 Forbidden
Should be used to forbid access regardless of authorization state.
A 403 error response indicates that the client's request is formed
correctly, but the REST API refuses to honor it. A 403 response is not a
case of insufficient client credentials; that would be 401 (\"Unauthorized\").
REST APIs use 403 to enforce application-level permissions. For example, a
client may be authorized to interact with some, but not all of a REST API's
resources. If the client attempts a resource interaction that is outside of
its permitted scope, the REST API should respond with 403."
([body] (response body 403))
([body headers] (response body 403 headers)))
(defn not-found
"HTTP 404 Not Found
Must be used when a client's URI cannot be mapped to a resource.
The 404 error status code indicates that the REST API can't map the
client's URI to a resource."
([body] (response body 404))
([body headers] (response body 404 headers)))
(defn method-not-allowed
([body] (response body 405))
([body headers] (response body 405 headers)))
(defn not-acceptable
([body] (response body 406))
([body headers] (response body 406 headers)))
(defn conflict
([body] (response body 409))
([body headers] (response body 409 headers)))
(defn gone
([body] (response body 410))
([body headers] (response body 410 headers)))
(defn precondition-failed
([body] (response body 412))
([body headers] (response body 412 headers)))
(defn unsupported-mediatype
([body] (response body 415))
([body headers] (response body 415 headers)))
(defn too-many-requests
([body] (response body 429))
([body headers] (response body 429 headers)))
(defn internal-server-error
([body] (response body 500))
([body headers] (response body 500 headers)))
(defn not-implemented
([body] (response body 501))
([body headers] (response body 501 headers)))
(ns uxbox.util.http
"Http related helpers.")

View file

@ -157,7 +157,9 @@
(into rp p)
(first n)
(rest n)))
[(str prefix (str/join join-with rs) suffix) rp]))))
(if (empty? rs)
["" []]
[(str prefix (str/join join-with rs) suffix) rp])))))
(defn- process-param-tokens
[sql]
@ -168,7 +170,7 @@
(def ^:private select-formatters
[#(format-exprs (::select %) {:prefix "SELECT "})
#(format-exprs (::from %) {:prefix "FROM "})
#(format-exprs (::join %))
#(format-exprs (::join %) {:join-with " "})
#(format-exprs (::where %) {:prefix "WHERE ("
:join-with ") AND ("
:suffix ")"})

View file

@ -6,9 +6,10 @@
[cuerdas.core :as str]
[mount.core :as mount]
[datoteka.storages :as st]
[uxbox.services.mutations.profiles :as profiles]
[uxbox.services.mutations.users :as users]
[uxbox.services.mutations.projects :as projects]
[uxbox.services.mutations.pages :as pages]
[uxbox.services.mutations.project-files :as files]
[uxbox.services.mutations.project-pages :as pages]
[uxbox.fixtures :as fixtures]
[uxbox.migrations]
[uxbox.media]
@ -24,6 +25,8 @@
#'uxbox.config/secret
#'uxbox.core/system
#'uxbox.db/pool
#'uxbox.services.init/query-services
#'uxbox.services.init/mutation-services
#'uxbox.migrations/migrations
#'uxbox.media/assets-storage
#'uxbox.media/media-storage
@ -64,39 +67,44 @@
(defn create-user
[conn i]
(profiles/create-profile conn {:id (mk-uuid "user" i)
:fullname (str "User " i)
:username (str "user" i)
:email (str "user" i ".test@uxbox.io")
:password "123123"
:metadata {}}))
(users/create-profile conn {:id (mk-uuid "user" i)
:fullname (str "User " i)
:username (str "user" i)
:email (str "user" i ".test@uxbox.io")
:password "123123"
:metadata {}}))
(defn create-project
[conn user-id i]
(projects/create-project conn {:id (mk-uuid "project" i)
:user user-id
:version 1
:name (str "sample project " i)}))
(defn create-page
[conn uid pid i]
(defn create-project-file
[conn user-id project-id i]
(files/create-file conn {:id (mk-uuid "project-file" i)
:user user-id
:project-id project-id
:name (str "sample project file" i)}))
(defn create-project-page
[conn user-id file-id i]
(pages/create-page conn {:id (mk-uuid "page" i)
:user uid
:project-id pid
:user user-id
:file-id file-id
:name (str "page" i)
:data {:shapes []}
:ordering i
:data {}
:metadata {}}))
(defn handle-error
[err]
(cond
(instance? clojure.lang.ExceptionInfo err)
(ex-data err)
(instance? java.util.concurrent.ExecutionException err)
(if (instance? java.util.concurrent.ExecutionException err)
(handle-error (.getCause err))
:else
[err nil]))
err))
(defmacro try-on
[expr]
@ -126,21 +134,28 @@
{:error (handle-error e#)
:result nil})))
(defn print-error!
[error]
(let [data (ex-data error)]
(cond
(= :spec-validation (:code data))
(println (:explain data))
:else
(.printStackTrace error))))
(defn print-result!
[{:keys [error result]}]
(if error
(do
(println "====> START ERROR")
(if (= :spec-validation (:code error))
(s/explain-out (:data error))
(prn error))
(print-error! error)
(println "====> END ERROR"))
(do
(println "====> START RESPONSE")
(prn result)
(println "====> END RESPONSE"))))
(defn exception?
[v]
(instance? Throwable v))
@ -154,6 +169,11 @@
(let [data (ex-data e)]
(= type (:type data))))
(defn ex-of-code?
[e code]
(let [data (ex-data e)]
(= code (:code data))))
(defn ex-with-code?
[e code]
(let [data (ex-data e)]

View file

@ -25,20 +25,20 @@
(t/is (contains? result :reply-to))
(t/is (vector? (:body result)))))
(t/deftest email-sending-and-sendmail-job
(let [res @(emails/send! emails/register {:to "example@uxbox.io" :name "foo"})]
(t/is (nil? res)))
(with-mock mock
{:target 'uxbox.jobs.sendmail/impl-sendmail
:return (p/resolved nil)}
;; (t/deftest email-sending-and-sendmail-job
;; (let [res @(emails/send! emails/register {:to "example@uxbox.io" :name "foo"})]
;; (t/is (nil? res)))
;; (with-mock mock
;; {:target 'uxbox.jobs.sendmail/impl-sendmail
;; :return (p/resolved nil)}
(let [res @(uxbox.jobs.sendmail/send-emails {})]
(t/is (= 1 res))
(t/is (:called? @mock))
(t/is (= 1 (:call-count @mock))))
;; (let [res @(uxbox.jobs.sendmail/send-emails {})]
;; (t/is (= 1 res))
;; (t/is (:called? @mock))
;; (t/is (= 1 (:call-count @mock))))
(let [res @(uxbox.jobs.sendmail/send-emails {})]
(t/is (= 0 res))
(t/is (:called? @mock))
(t/is (= 1 (:call-count @mock))))))
;; (let [res @(uxbox.jobs.sendmail/send-emails {})]
;; (t/is (= 0 res))
;; (t/is (:called? @mock))
;; (t/is (= 1 (:call-count @mock))))))

View file

@ -24,9 +24,14 @@
:scope "foobar"}
out (th/try-on! (sm/handle event))]
;; (th/print-result! out)
(t/is (map? (:error out)))
(t/is (= (get-in out [:error :type]) :validation))
(t/is (= (get-in out [:error :code]) :uxbox.services.mutations.auth/wrong-credentials))))
(let [error (:error out)]
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :service-error)))
(let [error (ex-cause (:error out))]
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :validation))
(t/is (th/ex-of-code? error :uxbox.services.mutations.auth/wrong-credentials)))))
(t/deftest success-auth
(let [user @(th/create-user db/pool 1)

View file

@ -1,144 +0,0 @@
(ns uxbox.tests.test-services-pages
(:require
[clojure.spec.alpha :as s]
[clojure.test :as t]
[promesa.core :as p]
[uxbox.db :as db]
[uxbox.http :as http]
[uxbox.services.mutations :as sm]
[uxbox.services.queries :as sq]
[uxbox.tests.helpers :as th]))
(t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset)
(t/deftest mutation-create-page
(let [user @(th/create-user db/pool 1)
proj @(th/create-project db/pool (:id user) 1)
data {::sm/type :create-page
:data {:shapes []}
:metadata {}
:project-id (:id proj)
:name "test page"
:user (:id user)}
res (th/try-on! (sm/handle data))]
(t/is (nil? (:error res)))
(t/is (uuid? (get-in res [:result :id])))
(let [rsp (:result res)]
(t/is (= (:user data) (:user-id rsp)))
(t/is (= (:name data) (:name rsp)))
(t/is (= (:data data) (:data rsp)))
(t/is (= (:metadata data) (:metadata rsp))))))
(t/deftest mutation-update-page
(let [user @(th/create-user db/pool 1)
proj @(th/create-project db/pool (:id user) 1)
page @(th/create-page db/pool (:id user) (:id proj) 1)
data {::sm/type :update-page
:id (:id page)
:data {:shapes [1 2 3]}
:metadata {:foo 2}
:project-id (:id proj)
:name "test page"
:user (:id user)}
res (th/try-on! (sm/handle data))]
;; (th/print-result! res)
(t/is (nil? (:error res)))
(t/is (= (:id data) (get-in res [:result :id])))
#_(t/is (= (:user data) (get-in res [:result :user-id])))
#_(t/is (= (:name data) (get-in res [:result :name])))
#_(t/is (= (:data data) (get-in res [:result :data])))
#_(t/is (= (:metadata data) (get-in res [:result :metadata])))))
(t/deftest mutation-update-page-metadata
(let [user @(th/create-user db/pool 1)
proj @(th/create-project db/pool (:id user) 1)
page @(th/create-page db/pool (:id user) (:id proj) 1)
data {::sm/type :update-page-metadata
:id (:id page)
:metadata {:foo 2}
:project-id (:id proj)
:name "test page"
:user (:id user)}
res (th/try-on! (sm/handle data))]
;; (th/print-result! res)
(t/is (nil? (:error res)))
(t/is (= (:id data) (get-in res [:result :id])))
(t/is (= (:user data) (get-in res [:result :user-id])))
(t/is (= (:name data) (get-in res [:result :name])))
(t/is (= (:metadata data) (get-in res [:result :metadata])))))
(t/deftest mutation-delete-page
(let [user @(th/create-user db/pool 1)
proj @(th/create-project db/pool (:id user) 1)
page @(th/create-page db/pool (:id user) (:id proj) 1)
data {::sm/type :delete-page
:id (:id page)
:user (:id user)}
res (th/try-on! (sm/handle data))]
;; (th/print-result! res)
(t/is (nil? (:error res)))
(t/is (nil? (:result res)))))
(t/deftest query-pages-by-project
(let [user @(th/create-user db/pool 1)
proj @(th/create-project db/pool (:id user) 1)
page @(th/create-page db/pool (:id user) (:id proj) 1)
data {::sq/type :pages-by-project
:project-id (:id proj)
:user (:id user)}
res (th/try-on! (sq/handle data))]
;; (th/print-result! res)
(t/is (nil? (:error res)))
(t/is (vector? (:result res)))
(t/is (= 1 (count (:result res))))
(t/is (= "page1" (get-in res [:result 0 :name])))
(t/is (:id proj) (get-in res [:result 0 :project-id]))))
;; (t/deftest http-page-history-update
;; (with-open [conn (db/connection)]
;; (let [user (th/create-user conn 1)
;; proj (uspr/create-project conn {:user (:id user) :name "proj1"})
;; data {:id (uuid/random)
;; :user (:id user)
;; :project (:id proj)
;; :version 0
;; :data "1"
;; :metadata "2"
;; :name "page1"
;; :width 200
;; :height 200
;; :layout "mobil"}
;; page (uspg/create-page conn data)]
;; (dotimes [i 10]
;; (let [page (uspg/get-page-by-id conn (:id data))]
;; (uspg/update-page conn (assoc page :data (str i)))))
;; ;; Check inserted history
;; (let [sql (str "SELECT * FROM pages_history "
;; " WHERE page=? ORDER BY created_at DESC")
;; result (sc/fetch conn [sql (:id data)])
;; item (first result)]
;; (th/with-server {:handler @http/app}
;; (let [uri (str th/+base-url+
;; "/api/pages/" (:id page)
;; "/history/" (:id item))
;; params {:body {:label "test" :pinned true}}
;; [status data] (th/http-put user uri params)]
;; ;; (println "RESPONSE:" status data)
;; (t/is (= 200 status))
;; (t/is (= (:id data) (:id item))))))
;; (let [sql (str "SELECT * FROM pages_history "
;; " WHERE page=? AND pinned = true "
;; " ORDER BY created_at DESC")
;; result (sc/fetch-one conn [sql (:id data)])]
;; (t/is (= "test" (:label result)))
;; (t/is (= true (:pinned result)))))))

View file

@ -0,0 +1,76 @@
(ns uxbox.tests.test-services-project-files
(:require
[clojure.test :as t]
[promesa.core :as p]
[uxbox.db :as db]
[uxbox.http :as http]
[uxbox.services.mutations :as sm]
[uxbox.services.queries :as sq]
[uxbox.tests.helpers :as th]))
(t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset)
(t/deftest query-project-files
(let [user @(th/create-user db/pool 2)
proj @(th/create-project db/pool (:id user) 1)
pf @(th/create-project-file db/pool (:id user) (:id proj) 1)
pp @(th/create-project-page db/pool (:id user) (:id pf) 1)
data {::sq/type :project-files
:user (:id user)
:project-id (:id proj)}
out (th/try-on! (sq/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (= 1 (count (:result out))))
(t/is (= (:id pf) (get-in out [:result 0 :id])))
(t/is (= (:id proj) (get-in out [:result 0 :project-id])))
(t/is (= (:name pf) (get-in out [:result 0 :name])))
(t/is (= [(:id pp)] (get-in out [:result 0 :pages])))))
(t/deftest mutation-create-project-file
(let [user @(th/create-user db/pool 1)
proj @(th/create-project db/pool (:id user) 1)
data {::sm/type :create-project-file
:user (:id user)
:name "test file"
:project-id (:id proj)}
out (th/try-on! (sm/handle data))
]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (= (:name data) (get-in out [:result :name])))
#_(t/is (= (:project-id data) (get-in out [:result :project-id])))))
(t/deftest mutation-update-project-file
(let [user @(th/create-user db/pool 1)
proj @(th/create-project db/pool (:id user) 1)
pf @(th/create-project-file db/pool (:id user) (:id proj) 1)
data {::sm/type :update-project-file
:id (:id pf)
:name "new file name"
:user (:id user)}
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
;; TODO: check the result
(t/is (nil? (:error out)))
(t/is (nil? (:result out)))))
(t/deftest mutation-delete-project-file
(let [user @(th/create-user db/pool 1)
proj @(th/create-project db/pool (:id user) 1)
pf @(th/create-project-file db/pool (:id user) (:id proj) 1)
data {::sm/type :delete-project-file
:id (:id pf)
:user (:id user)}
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (nil? (:result out)))
(let [sql "select * from project_files
where project_id=$1 and deleted_at is null"
res @(db/query db/pool [sql (:id proj)])]
(t/is (empty? res)))))
;; ;; TODO: add permisions related tests

View file

@ -0,0 +1,84 @@
(ns uxbox.tests.test-services-project-pages
(:require
[clojure.spec.alpha :as s]
[clojure.test :as t]
[promesa.core :as p]
[uxbox.db :as db]
[uxbox.http :as http]
[uxbox.services.mutations :as sm]
[uxbox.services.queries :as sq]
[uxbox.tests.helpers :as th]))
(t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset)
(t/deftest query-project-pages
(let [user @(th/create-user db/pool 1)
proj @(th/create-project db/pool (:id user) 1)
file @(th/create-project-file db/pool (:id user) (:id proj) 1)
page @(th/create-project-page db/pool (:id user) (:id file) 1)
data {::sq/type :project-pages
:file-id (:id file)
:user (:id user)}
out (th/try-on! (sq/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (vector? (:result out)))
(t/is (= 1 (count (:result out))))
(t/is (= "page1" (get-in out [:result 0 :name])))
(t/is (:id file) (get-in out [:result 0 :file-id]))))
(t/deftest mutation-create-project-page
(let [user @(th/create-user db/pool 1)
proj @(th/create-project db/pool (:id user) 1)
pf @(th/create-project-file db/pool (:id user) (:id proj) 1)
data {::sm/type :create-project-page
:data {}
:metadata {}
:file-id (:id pf)
:ordering 1
:name "test page"
:user (:id user)}
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (uuid? (get-in out [:result :id])))
(t/is (= (:user data) (get-in out [:result :user-id])))
(t/is (= (:name data) (get-in out [:result :name])))
(t/is (= (:data data) (get-in out [:result :data])))
(t/is (= (:metadata data) (get-in out [:result :metadata])))
(t/is (= 0 (get-in out [:result :version])))))
(t/deftest mutation-update-project-page-data
(let [user @(th/create-user db/pool 1)
proj @(th/create-project db/pool (:id user) 1)
file @(th/create-project-file db/pool (:id user) (:id proj) 1)
page @(th/create-project-page db/pool (:id user) (:id file) 1)
data {::sm/type :update-project-page-data
:id (:id page)
:data {:shapes [1 2 3]}
:file-id (:id file)
:user (:id user)}
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
;; TODO: check history creation
;; TODO: check correct page data update operation
(t/is (nil? (:error out)))
(t/is (= (:id data) (get-in out [:result :id])))
(t/is (= 1 (get-in out [:result :version])))))
(t/deftest mutation-delete-project-page
(let [user @(th/create-user db/pool 1)
proj @(th/create-project db/pool (:id user) 1)
file @(th/create-project-file db/pool (:id user) (:id proj) 1)
page @(th/create-project-page db/pool (:id user) (:id file) 1)
data {::sm/type :delete-project-page
:id (:id page)
:user (:id user)}
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (nil? (:result out)))))

View file

@ -11,17 +11,13 @@
(t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset)
;; TODO: migrate from try-on to try-on!
(t/deftest query-project-list
(t/deftest query-projects
(let [user @(th/create-user db/pool 1)
proj @(th/create-project db/pool (:id user) 1)
data {::sq/type :projects
:user (:id user)}
out (th/try-on! (sq/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (= 1 (count (:result out))))
(t/is (= (:id proj) (get-in out [:result 0 :id])))
@ -32,11 +28,10 @@
data {::sm/type :create-project
:user (:id user)
:name "test project"}
[err rsp] (th/try-on (sm/handle data))]
;; (prn "RESPONSE:" err rsp)
(t/is (nil? err))
(t/is (= (:user data) (:user-id rsp)))
(t/is (= (:name data) (:name rsp)))))
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (= (:name data) (get-in out [:result :name])))))
(t/deftest mutation-update-project
(let [user @(th/create-user db/pool 1)
@ -45,12 +40,12 @@
:id (:id proj)
:name "test project mod"
:user (:id user)}
[err rsp] (th/try-on (sm/handle data))]
;; (prn "RESPONSE:" err rsp)
(t/is (nil? err))
(t/is (= (:id data) (:id rsp)))
(t/is (= (:user data) (:user-id rsp)))
(t/is (= (:name data) (:name rsp)))))
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (= (:id data) (get-in out [:result :id])))
(t/is (= (:user data) (get-in out [:result :user-id])))
(t/is (= (:name data) (get-in out [:result :name])))))
(t/deftest mutation-delete-project
(let [user @(th/create-user db/pool 1)
@ -58,12 +53,13 @@
data {::sm/type :delete-project
:id (:id proj)
:user (:id user)}
[err rsp] (th/try-on (sm/handle data))]
;; (prn "RESPONSE:" err rsp)
(t/is (nil? err))
(t/is (nil? rsp))
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (nil? (:result out)))
(let [sql "SELECT * FROM projects
WHERE user_id=$1 AND deleted_at is null"
(let [sql "select * from projects where user_id=$1 and deleted_at is null"
res @(db/query db/pool [sql (:id user)])]
(t/is (empty? res)))))
;; TODO: add permisions related tests

View file

@ -1,4 +1,4 @@
(ns uxbox.tests.test-services-user-storage
(ns uxbox.tests.test-services-user-attrs
(:require
[clojure.spec.alpha :as s]
[clojure.test :as t]
@ -12,16 +12,22 @@
(t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset)
(t/deftest test-user-storage
(t/deftest test-user-attrs
(let [{:keys [id] :as user} @(th/create-user db/pool 1)]
(let [out (th/try-on! (sq/handle {::sq/type :user-storage-entry
(let [out (th/try-on! (sq/handle {::sq/type :user-attr
:key "foobar"
:user id}))]
(t/is (nil? (:result out)))
(t/is (map? (:error out)))
(t/is (= :not-found (get-in out [:error :type]))))
(let [out (th/try-on! (sm/handle {::sm/type :upsert-user-storage-entry
(let [error (:error out)]
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :service-error)))
(let [error (ex-cause (:error out))]
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :not-found))))
(let [out (th/try-on! (sm/handle {::sm/type :upsert-user-attr
:user id
:key "foobar"
:val {:some #{:value}}}))]
@ -29,7 +35,7 @@
(t/is (nil? (:error out)))
(t/is (nil? (:result out))))
(let [out (th/try-on! (sq/handle {::sq/type :user-storage-entry
(let [out (th/try-on! (sq/handle {::sq/type :user-attr
:key "foobar"
:user id}))]
;; (th/print-result! out)
@ -37,18 +43,23 @@
(t/is (= {:some #{:value}} (get-in out [:result :val])))
(t/is (= "foobar" (get-in out [:result :key]))))
(let [out (th/try-on! (sm/handle {::sm/type :delete-user-storage-entry
(let [out (th/try-on! (sm/handle {::sm/type :delete-user-attr
:user id
:key "foobar"}))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (nil? (:result out))))
(let [out (th/try-on! (sq/handle {::sq/type :user-storage-entry
(let [out (th/try-on! (sq/handle {::sq/type :user-attr
:key "foobar"
:user id}))]
;; (th/print-result! out)
(t/is (nil? (:result out)))
(t/is (map? (:error out)))
(t/is (= :not-found (get-in out [:error :type]))))))
(let [error (:error out)]
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :service-error)))
(let [error (ex-cause (:error out))]
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :not-found))))))

View file

@ -1,11 +1,10 @@
(ns uxbox.tests.test-users
(ns uxbox.tests.test-services-users
(:require
[clojure.test :as t]
[clojure.java.io :as io]
[promesa.core :as p]
[cuerdas.core :as str]
[datoteka.core :as fs]
[vertx.core :as vc]
[uxbox.db :as db]
[uxbox.services.mutations :as sm]
[uxbox.services.queries :as sq]
@ -16,47 +15,48 @@
(t/deftest test-query-profile
(let [user @(th/create-user db/pool 1)
event {::sq/type :profile
:user (:id user)}
[err rsp] (th/try-on (sq/handle event))]
;; (println "RESPONSE:" resp)))
(t/is (nil? err))
(t/is (= (:fullname rsp) "User 1"))
(t/is (= (:username rsp) "user1"))
(t/is (= (:metadata rsp) {}))
(t/is (= (:email rsp) "user1.test@uxbox.io"))
(t/is (not (contains? rsp :password)))))
data {::sq/type :profile
:user (:id user)}
out (th/try-on! (sq/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (= "User 1" (get-in out [:result :fullname])))
(t/is (= "user1" (get-in out [:result :username])))
(t/is (= "user1.test@uxbox.io" (get-in out [:result :email])))
(t/is (not (contains? (:result out) :password)))))
(t/deftest test-mutation-update-profile
(let [user @(th/create-user db/pool 1)
event (assoc user
::sm/type :update-profile
:fullname "Full Name"
:username "user222"
:metadata {:foo "bar"}
:email "user222@uxbox.io")
[err data] (th/try-on (sm/handle event))]
;; (println "RESPONSE:" err data)
(t/is (nil? err))
(t/is (= (:fullname data) "Full Name"))
(t/is (= (:username data) "user222"))
(t/is (= (:metadata data) {:foo "bar"}))
(t/is (= (:email data) "user222@uxbox.io"))
(t/is (not (contains? data :password)))))
(let [user @(th/create-user db/pool 1)
data (assoc user
::sm/type :update-profile
:fullname "Full Name"
:username "user222"
:metadata {:foo "bar"}
:email "user222@uxbox.io")
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (= (:fullname data) (get-in out [:result :fullname])))
(t/is (= (:username data) (get-in out [:result :username])))
(t/is (= (:email data) (get-in out [:result :email])))
(t/is (= (:metadata data) (get-in out [:result :metadata])))
(t/is (not (contains? (:result out) :password)))))
(t/deftest test-mutation-update-profile-photo
(let [user @(th/create-user db/pool 1)
event {::sm/type :update-profile-photo
:user (:id user)
:file {:name "sample.jpg"
:path (fs/path "test/uxbox/tests/_files/sample.jpg")
:size 123123
:mtype "image/jpeg"}}
[err rsp] (th/try-on (sm/handle event))]
;; (prn "RESPONSE:" [err rsp])
(t/is (nil? err))
(t/is (= (:id user) (:id rsp)))
(t/is (str/starts-with? (:photo rsp) "http"))))
data {::sm/type :update-profile-photo
:user (:id user)
:file {:name "sample.jpg"
:path (fs/path "test/uxbox/tests/_files/sample.jpg")
:size 123123
:mtype "image/jpeg"}}
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (= (:id user) (get-in out [:result :id])))
(t/is (str/starts-with? (get-in out [:result :photo]) "http"))))
;; (t/deftest test-mutation-register-profile
;; (let[data {:fullname "Full Name"

View file

@ -34,19 +34,20 @@
(t/deftest parse-invalid-svg-1
(let [image (io/resource "uxbox/tests/_files/sample.jpg")
result (th/try! (svg/parse image))]
(t/is (map? (:error result)))
(t/is (= (get-in result [:error :code])
::svg/invalid-input))))
out (th/try! (svg/parse image))]
(let [error (:error out)]
(t/is (th/ex-info? error))
(t/is (th/ex-of-code? error ::svg/invalid-input)))))
(t/deftest parse-invalid-svg-2
(let [result (th/try! (svg/parse-string ""))]
(t/is (map? (:error result)))
(t/is (= (get-in result [:error :code])
::svg/invalid-input))))
(let [out (th/try! (svg/parse-string ""))]
(let [error (:error out)]
(t/is (th/ex-info? error))
(t/is (th/ex-of-code? error ::svg/invalid-input)))))
(t/deftest parse-invalid-svg-3
(let [result (th/try! (svg/parse-string "<svg></svg>"))]
(t/is (map? (:error result)))
(t/is (= (get-in result [:error :code])
::svg/invalid-result))))
(let [out (th/try! (svg/parse-string "<svg></svg>"))]
(let [error (:error out)]
(t/is (th/ex-info? error))
(t/is (th/ex-of-code? error ::svg/invalid-result)))))