diff --git a/backend/src/app/http/debug.clj b/backend/src/app/http/debug.clj index 1b9995069..f7f2fb1d8 100644 --- a/backend/src/app/http/debug.clj +++ b/backend/src/app/http/debug.clj @@ -139,16 +139,17 @@ data (some-> params :file :path io/read-as-bytes)] (if (and data project-id) - (let [fname (str "Imported file *: " (dt/now)) - overwrite? (contains? params :overwrite?) - file-id (or (and overwrite? (ex/ignoring (-> params :file :filename parse-uuid))) - (uuid/next))] + (let [fname (str "Imported file *: " (dt/now)) + reuse-id? (contains? params :reuseid) + file-id (or (and reuse-id? (ex/ignoring (-> params :file :filename parse-uuid))) + (uuid/next))] - (if (and overwrite? file-id + (if (and reuse-id? file-id (is-file-exists? pool file-id)) (do (db/update! pool :file - {:data data} + {:data data + :deleted-at nil} {:id file-id}) {::yrs/status 200 ::yrs/body "OK UPDATED"}) diff --git a/backend/src/app/rpc/commands/files_update.clj b/backend/src/app/rpc/commands/files_update.clj index 2b9b94d51..843e2aafc 100644 --- a/backend/src/app/rpc/commands/files_update.clj +++ b/backend/src/app/rpc/commands/files_update.clj @@ -124,6 +124,7 @@ (declare send-notifications!) (declare update-file) (declare update-file*) +(declare update-file-data) (declare take-snapshot?) ;; If features are specified from params and the final feature @@ -208,26 +209,6 @@ :project-id (:project-id file) :team-id (:team-id file)})))))) -(defn- update-file-data - [file changes] - (-> file - (update :revn inc) - (update :data (fn [data] - (cond-> data - :always - (-> (blob/decode) - (assoc :id (:id file)) - (pmg/migrate-data)) - - (and (contains? ffeat/*current* "components/v2") - (not (contains? ffeat/*previous* "components/v2"))) - (ctf/migrate-to-components-v2) - - :always - (-> (cp/process-changes changes) - (blob/encode))))))) - - (defn- update-file* [{:keys [::db/conn] :as cfg} {:keys [profile-id file changes session-id ::created-at] :as params}] (let [;; Process the file data in the CLIMIT context; scheduling it @@ -267,6 +248,25 @@ ;; Retrieve and return lagged data (get-lagged-changes conn params)))) +(defn- update-file-data + [file changes] + (-> file + (update :revn inc) + (update :data (fn [data] + (cond-> data + :always + (-> (blob/decode) + (assoc :id (:id file)) + (pmg/migrate-data)) + + (and (contains? ffeat/*current* "components/v2") + (not (contains? ffeat/*previous* "components/v2"))) + (ctf/migrate-to-components-v2) + + :always + (-> (cp/process-changes changes) + (blob/encode))))))) + (defn- take-snapshot? "Defines the rule when file `data` snapshot should be saved." [{:keys [revn modified-at] :as file}] diff --git a/backend/src/app/srepl/fixes.clj b/backend/src/app/srepl/fixes.clj index 5d15973d1..3124e2f85 100644 --- a/backend/src/app/srepl/fixes.clj +++ b/backend/src/app/srepl/fixes.clj @@ -8,6 +8,7 @@ "A collection of adhoc fixes scripts." (:require [app.common.data :as d] + [app.common.files.validate :as cfv] [app.common.geom.shapes :as gsh] [app.common.logging :as l] [app.common.pages.helpers :as cph] @@ -29,7 +30,7 @@ (d/index-by :id)) update-page (fn [page] - (let [errors (ctf/validate-shape uuid/zero file page libs)] + (let [errors (cfv/validate-shape uuid/zero file page libs)] (when (seq errors) (println "******Errors in file " (:id file) " page " (:id page)) (pprint errors {:level 3}))))] diff --git a/common/src/app/common/files/repair.cljc b/common/src/app/common/files/repair.cljc new file mode 100644 index 000000000..4cc8454fe --- /dev/null +++ b/common/src/app/common/files/repair.cljc @@ -0,0 +1,368 @@ +;; 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) KALEIDOS INC + +(ns app.common.files.repair + (:require + [app.common.data :as d] + [app.common.logging :as log] + [app.common.pages.changes-builder :as pcb] + [app.common.pages.helpers :as cph] + [app.common.types.component :as ctk] + [app.common.types.components-list :as ctkl] + [app.common.types.container :as ctn] + [app.common.types.file :as ctf] + [app.common.types.pages-list :as ctpl] + [app.common.uuid :as uuid])) + +(log/set-level! :debug) + +(defmulti repair-error + (fn [code _error _file-data _libraries] code)) + +(defmethod repair-error :parent-not-found + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Set parent to root frame. + (log/debug :hint " -> Set to " :parent-id uuid/zero) + (assoc shape :parent-id uuid/zero))] + + (log/info :hint "Repairing shape :parent-not-found" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :child-not-in-parent + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [parent-shape] + ; Add shape to parent's children list + (log/debug :hint " -> Add children to" :parent-id (:id parent-shape)) + (update parent-shape :shapes conj (:id shape)))] + + (log/info :hint "Repairing shape :child-not-in-parent" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:parent-id shape)] repair-shape)))) + +(defmethod repair-error :child-not-found + [_ {:keys [shape page-id args] :as error} file-data _] + (let [repair-shape + (fn [parent-shape] + ; Remove child shape from children list + (log/debug :hint " -> Remove child " :child-id (:child-id args)) + (update parent-shape :shapes d/removev #(= % (:child-id args))))] + + (log/info :hint "Repairing shape :child-not-found" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :frame-not-found + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Locate the first frame in parents and set frame-id to it. + (let [page (ctpl/get-page file-data page-id) + frame (cph/get-frame (:objects page) (:parent-id shape)) + frame-id (or (:id frame) uuid/zero)] + (log/debug :hint " -> Set to " :frame-id frame-id) + (assoc shape :frame-id frame-id)))] + + (log/info :hint "Repairing shape :frame-not-found" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :invalid-frame + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Locate the first frame in parents and set frame-id to it. + (let [page (ctpl/get-page file-data page-id) + frame (cph/get-frame (:objects page) (:parent-id shape)) + frame-id (or (:id frame) uuid/zero)] + (log/debug :hint " -> Set to " :frame-id frame-id) + (assoc shape :frame-id frame-id)))] + + (log/info :hint "Repairing shape :invalid-frame" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :component-not-main + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Set the :shape as main instance root + (log/debug :hint " -> Set :main-instance") + (assoc shape :main-instance true))] + + (log/info :hint "Repairing shape :component-not-main" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :component-main-external + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; There is no solution that may recover it with confidence + (log/warn :hint " -> CANNOT REPAIR THIS AUTOMATICALLY.") + shape)] + + (log/info :hint "Repairing shape :component-main-external" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :component-not-found + [_ {:keys [shape page-id] :as error} file-data _] + (let [page (ctpl/get-page file-data page-id) + shape-ids (cph/get-children-ids-with-self (:objects page) (:id shape)) + + repair-shape + (fn [shape] + ; Detach the shape and convert it to non instance. + (log/debug :hint " -> Detach shape" :shape-id (:id shape)) + (ctk/detach-shape shape))] + + (log/info :hint "Repairing shape :component-not-found" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes shape-ids repair-shape)))) + +(defmethod repair-error :invalid-main-instance-id + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-component + (fn [component] + ; Assign main instance in the component to current shape + (log/debug :hint " -> Assign main-instance-id" :component-id (:id component)) + (assoc component :main-instance-id (:id shape)))] + (log/info :hint "Repairing shape :invalid-main-instance-id" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-library-data file-data) + (pcb/update-component [(:component-id shape)] repair-component)))) + +(defmethod repair-error :invalid-main-instance-page + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-component + (fn [component] + ; Assign main instance in the component to current shape + (log/debug :hint " -> Assign main-instance-page" :component-id (:id component)) + (assoc component :main-instance-page page-id))] + (log/info :hint "Repairing shape :invalid-main-instance-page" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-library-data file-data) + (pcb/update-component [(:component-id shape)] repair-component)))) + +(defmethod repair-error :invalid-main-instance + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; There is no solution that may recover it with confidence + (log/warn :hint " -> CANNOT REPAIR THIS AUTOMATICALLY.") + shape)] + + (log/info :hint "Repairing shape :invalid-main-instance" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :component-main + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Unset the :shape as main instance root + (log/debug :hint " -> Unset :main-instance") + (dissoc shape :main-instance))] + + (log/info :hint "Repairing shape :component-main" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :should-be-component-root + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Convert the shape in a top copy root. + (log/debug :hint " -> Set :component-root") + (assoc shape :component-root true))] + + (log/info :hint "Repairing shape :should-be-component-root" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :should-not-be-component-root + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Convert the shape in a nested copy root. + (log/debug :hint " -> Unset :component-root") + (dissoc shape :component-root))] + + (log/info :hint "Repairing shape :should-not-be-component-root" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :ref-shape-not-found + [_ {:keys [shape page-id] :as error} file-data libraries] + (let [matching-shape (let [page (ctpl/get-page file-data page-id) + root-shape (ctn/get-component-shape (:objects page) shape) + component-file (if (= (:component-file root-shape) (:id file-data)) + file-data + (-> (get libraries (:component-file root-shape)) :data)) + component (when component-file + (ctkl/get-component (:data component-file) (:component-id root-shape) true)) + shapes (ctf/get-component-shapes file-data component)] + (d/seek #(= (:shape-ref %) (:shape-ref shape)) shapes)) + + reassign-shape + (fn [shape] + (log/debug :hint " -> Reassign shape-ref to" :shape-ref (:id matching-shape)) + (assoc shape :shape-ref (:id matching-shape))) + + detach-shape + (fn [shape] + (log/debug :hint " -> Detach shape" :shape-id (:id shape)) + (ctk/detach-shape shape))] + + ; If the shape still refers to the remote component, try to find the corresponding near one + ; and link to it. If not, detach the shape. + (log/info :hint "Repairing shape :ref-shape-not-found" :id (:id shape) :name (:name shape) :page-id page-id) + (if (some? matching-shape) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] reassign-shape)) + (let [page (ctpl/get-page file-data page-id) + shape-ids (cph/get-children-ids-with-self (:objects page) (:id shape))] + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes shape-ids detach-shape)))))) + +(defmethod repair-error :shape-ref-in-main + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Remove shape-ref + (log/debug :hint " -> Unset :shape-ref") + (dissoc shape :shape-ref))] + + (log/info :hint "Repairing shape :shape-ref-in-main" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :root-main-not-allowed + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Convert the shape in a nested main head. + (log/debug :hint " -> Unset :component-root") + (dissoc shape :component-root))] + + (log/info :hint "Repairing shape :root-main-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :nested-main-not-allowed + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Convert the shape in a top main head. + (log/debug :hint " -> Set :component-root") + (assoc shape :component-root true))] + + (log/info :hint "Repairing shape :nested-main-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :root-copy-not-allowed + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Convert the shape in a nested copy head. + (log/debug :hint " -> Unset :component-root") + (dissoc shape :component-root))] + + (log/info :hint "Repairing shape :root-copy-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :nested-copy-not-allowed + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Convert the shape in a top copy root. + (log/debug :hint " -> Set :component-root") + (assoc shape :component-root true))] + + (log/info :hint "Repairing shape :nested-copy-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :not-head-main-not-allowed + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Detach the shape and convert it to non instance. + (log/debug :hint " -> Detach shape" :shape-id (:id shape)) + (ctk/detach-shape shape))] + + (log/info :hint "Repairing shape :not-head-main-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :not-head-copy-not-allowed + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Detach the shape and convert it to non instance. + (log/debug :hint " -> Detach shape" :shape-id (:id shape)) + (ctk/detach-shape shape))] + + (log/info :hint "Repairing shape :not-head-copy-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :not-component-not-allowed + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; There is no solution that may recover it with confidence + (log/warn :hint " -> CANNOT REPAIR THIS AUTOMATICALLY.") + shape)] + + (log/info :hint "Repairing shape :not-component-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :default + [_ error file _] + (log/error :hint "Unknown error code, don't know how to repair" :code (:code error)) + file) + +(defn repair-file + [file-data libraries errors] + (log/info :hint "Repairing file" :id (:id file-data) :error-count (count errors)) + (reduce (fn [changes error] + (pcb/concat-changes changes + (repair-error (:code error) + error + file-data + libraries))) + (pcb/empty-changes nil) + errors)) diff --git a/common/src/app/common/files/validate.cljc b/common/src/app/common/files/validate.cljc new file mode 100644 index 000000000..9991fae50 --- /dev/null +++ b/common/src/app/common/files/validate.cljc @@ -0,0 +1,380 @@ +;; 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) KALEIDOS INC + +(ns app.common.files.validate + (:require + [app.common.data.macros :as dm] + [app.common.exceptions :as ex] + [app.common.pages.helpers :as cph] + [app.common.schema :as sm] + [app.common.types.component :as ctk] + [app.common.types.container :as ctn] + [app.common.types.file :as ctf] + [app.common.types.pages-list :as ctpl] + [app.common.types.shape-tree :as ctst] + [app.common.uuid :as uuid] + [cuerdas.core :as str])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; SCHEMA +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def error-codes + #{:parent-not-found + :child-not-in-parent + :child-not-found + :frame-not-found + :invalid-frame + :component-not-main + :component-main-external + :component-not-found + :invalid-main-instance-id + :invalid-main-instance-page + :invalid-main-instance + :component-main + :should-be-component-root + :should-not-be-component-root + :ref-shape-not-found + :shape-ref-in-main + :root-main-not-allowed + :nested-main-not-allowed + :root-copy-not-allowed + :nested-copy-not-allowed + :not-head-main-not-allowed + :not-head-copy-not-allowed + :not-component-not-allowed}) + +(def validation-error + [:map {:title "ValidationError"} + [:code {:optional false} [::sm/one-of error-codes]] + [:hint {:optional false} :string] + [:shape {:optional true} :map] ; Cannot validate a shape because here it may be broken + [:file-id ::sm/uuid] + [:page-id ::sm/uuid]]) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; ERROR HANDLING +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def ^:dynamic *errors* nil) +(def ^:dynamic *throw-on-error* false) + +(defn- report-error + [code msg shape file page & args] + (when (some? *errors*) + (if (true? *throw-on-error*) + (ex/raise {:type :validation + :code code + :hint msg + :args args + ::explain (str/format "file %s\npage %s\nshape %s" + (:id file) + (:id page) + (:id shape))}) + (vswap! *errors* conj {:code code + :hint msg + :shape shape + :file-id (:id file) + :page-id (:id page) + :args args})))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; VALIDATION FUNCTIONS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(declare validate-shape) + +(defn validate-parent-children + "Validate parent and children exists, and the link is bidirectional." + [shape file page] + (let [parent (ctst/get-shape page (:parent-id shape))] + (if (nil? parent) + (report-error :parent-not-found + (str/format "Parent %s not found" (:parent-id shape)) + shape file page) + (do + (when-not (cph/root? shape) + (when-not (some #{(:id shape)} (:shapes parent)) + (report-error :child-not-in-parent + (str/format "Shape %s not in parent's children list" (:id shape)) + shape file page))) + + (doseq [child-id (:shapes shape)] + (when (nil? (ctst/get-shape page child-id)) + (report-error :child-not-found + (str/format "Child %s not found" child-id) + shape file page + :child-id child-id))))))) + +(defn validate-frame + "Validate that the frame-id shape exists and is indeed a frame." + [shape file page] + (let [frame (ctst/get-shape page (:frame-id shape))] + (if (nil? frame) + (report-error :frame-not-found + (str/format "Frame %s not found" (:frame-id shape)) + shape file page) + (when (not= (:type frame) :frame) + (report-error :invalid-frame + (str/format "Frame %s is not actually a frame" (:frame-id shape)) + shape file page))))) + +(defn validate-component-main-head + "Validate shape is a main instance head, component exists and its main-instance points to this shape." + [shape file page libraries] + (when (nil? (:main-instance shape)) + (report-error :component-not-main + (str/format "Shape expected to be main instance") + shape file page)) + (when-not (= (:component-file shape) (:id file)) + (report-error :component-main-external + (str/format "Main instance should refer to a component in the same file") + shape file page)) + (let [component (ctf/resolve-component shape file libraries {:include-deleted? true})] + (if (nil? component) + (report-error :component-not-found + (str/format "Component %s not found in file" (:component-id shape) (:component-file shape)) + shape file page) + (do + (when-not (= (:main-instance-id component) (:id shape)) + (report-error :nvalid-main-instance-id + (str/format "Main instance id of component %s is not valid" (:component-id shape)) + shape file page)) + (when-not (= (:main-instance-page component) (:id page)) + (report-error :invalid-main-instance-page + (str/format "Main instance page of component %s is not valid" (:component-id shape)) + shape file page)))))) + +(defn validate-component-not-main-head + "Validate shape is a not-main instance head, component exists and its main-instance does not point to this shape." + [shape file page libraries] + (when (some? (:main-instance shape)) + (report-error :component-not-main + (str/format "Shape not expected to be main instance") + shape file page)) + (let [component (ctf/resolve-component shape file libraries {:include-deleted? true})] + (if (nil? component) + (report-error :component-not-found + (str/format "Component %s not found in file" (:component-id shape) (:component-file shape)) + shape file page) + (do + (when (and (= (:main-instance-id component) (:id shape)) + (= (:main-instance-page component) (:id page))) + (report-error :invalid-main-instance + (str/format "Main instance of component %s should not be this shape" (:id component)) + shape file page)))))) + +(defn validate-component-not-main-not-head + "Validate that this shape is not main instance and not head." + [shape file page] + (when (some? (:main-instance shape)) + (report-error :component-main + (str/format "Shape not expected to be main instance") + shape file page)) + (when (or (some? (:component-id shape)) + (some? (:component-file shape))) + (report-error :component-main + (str/format "Shape not expected to be component head") + shape file page))) + +(defn validate-component-root + "Validate that this shape is an instance root." + [shape file page] + (when (nil? (:component-root shape)) + (report-error :should-be-component-root + (str/format "Shape should be component root") + shape file page))) + +(defn validate-component-not-root + "Validate that this shape is not an instance root." + [shape file page] + (when (some? (:component-root shape)) + (report-error :should-not-be-component-root + (str/format "Shape should not be component root") + shape file page))) + +(defn validate-component-ref + "Validate that the referenced shape exists in the near component." + [shape file page libraries] + (let [ref-shape (ctf/find-ref-shape file page libraries shape :include-deleted? true)] + (when (nil? ref-shape) + (report-error :ref-shape-not-found + (str/format "Referenced shape %s not found in near component" (:shape-ref shape)) + shape file page)))) + +(defn validate-component-not-ref + "Validate that this shape does not reference other one." + [shape file page] + (when (some? (:shape-ref shape)) + (report-error :shape-ref-in-main + (str/format "Shape inside main instance should not have shape-ref") + shape file page))) + +(defn validate-shape-main-root-top + "Root shape of a top main instance + :main-instance + :component-id + :component-file + :component-root" + [shape file page libraries] + (validate-component-main-head shape file page libraries) + (validate-component-root shape file page) + (validate-component-not-ref shape file page) + (doseq [child-id (:shapes shape)] + (validate-shape child-id file page libraries :context :main-top :clear-errors? false))) + +(defn validate-shape-main-root-nested + "Root shape of a nested main instance + :main-instance + :component-id + :component-file" + [shape file page libraries] + (validate-component-main-head shape file page libraries) + (validate-component-not-root shape file page) + (validate-component-not-ref shape file page) + (doseq [child-id (:shapes shape)] + (validate-shape child-id file page libraries :context :main-nested :clear-errors? false))) + +(defn validate-shape-copy-root-top + "Root shape of a top copy instance + :component-id + :component-file + :component-root + :shape-ref" + [shape file page libraries] + (validate-component-not-main-head shape file page libraries) + (validate-component-root shape file page) + (validate-component-ref shape file page libraries) + (doseq [child-id (:shapes shape)] + (validate-shape child-id file page libraries :context :copy-top :clear-errors? false))) + +(defn validate-shape-copy-root-nested + "Root shape of a nested copy instance + :component-id + :component-file + :shape-ref" + [shape file page libraries] + (validate-component-not-main-head shape file page libraries) + (validate-component-not-root shape file page) + (validate-component-ref shape file page libraries) + (doseq [child-id (:shapes shape)] + (validate-shape child-id file page libraries :context :copy-nested :clear-errors? false))) + +(defn validate-shape-main-not-root + "Not-root shape of a main instance + (not any attribute)" + [shape file page libraries] + (validate-component-not-main-not-head shape file page) + (validate-component-not-root shape file page) + (validate-component-not-ref shape file page) + (doseq [child-id (:shapes shape)] + (validate-shape child-id file page libraries :context :main-any :clear-errors? false))) + +(defn validate-shape-copy-not-root + "Not-root shape of a copy instance + :shape-ref" + [shape file page libraries] + (validate-component-not-main-not-head shape file page) + (validate-component-not-root shape file page) + (validate-component-ref shape file page libraries) + (doseq [child-id (:shapes shape)] + (validate-shape child-id file page libraries :context :copy-any :clear-errors? false))) + +(defn validate-shape-not-component + "Shape is not in a component or is a fostered children + (not any attribute)" + [shape file page libraries] + (validate-component-not-main-not-head shape file page) + (validate-component-not-root shape file page) + (validate-component-not-ref shape file page) + (doseq [child-id (:shapes shape)] + (validate-shape child-id file page libraries :context :not-component :clear-errors? false))) + +(defn validate-shape + "Validate referential integrity and semantic coherence of a shape and all its children. + + The context is the situation of the parent in respect to components: + :not-component + :main-top + :main-nested + :copy-top + :copy-nested + :main-any + :copy-any" + [shape-id file page libraries & {:keys [context throw?] + :or {context :not-component + throw? false}}] + (binding [*throw-on-error* throw? + *errors* (or *errors* (volatile! []))] + (let [shape (ctst/get-shape page shape-id)] + + ; If this happens it's a bug in this validate functions + (dm/verify! (str/format "Shape %s not found" shape-id) (some? shape)) + + (validate-parent-children shape file page) + (validate-frame shape file page) + + (if (ctk/main-instance? shape) + + (if (ctk/instance-root? shape) + (if (not= context :not-component) + (report-error :root-main-not-allowed + (str/format "Root main component not allowed inside other component") + shape file page) + (validate-shape-main-root-top shape file page libraries)) + + (if (= context :not-component) + (report-error :nested-main-not-allowed + (str/format "Nested main component only allowed inside other component") + shape file page) + (validate-shape-main-root-nested shape file page libraries))) + + (if (ctk/instance-head? shape) + + (if (ctk/instance-root? shape) + (if (not= context :not-component) + (report-error :root-copy-not-allowed + (str/format "Root copy not allowed inside other component") + shape file page) + (validate-shape-copy-root-top shape file page libraries)) + + (if (= context :not-component) + (report-error :nested-copy-not-allowed + (str/format "Nested copy only allowed inside other component") + shape file page) + (validate-shape-copy-root-nested shape file page libraries))) + + (if (ctn/component-main? (:objects page) shape) + (if-not (#{:main-top :main-nested :main-any} context) + (report-error :not-head-main-not-allowed + (str/format "Non-root main only allowed inside a main component") + shape file page) + (validate-shape-main-not-root shape file page libraries)) + + (if (ctk/in-component-copy? shape) + (if-not (#{:copy-top :copy-nested :copy-any} context) + (report-error :not-head-copy-not-allowed + (str/format "Non-root copy only allowed inside a copy") + shape file page) + (validate-shape-copy-not-root shape file page libraries)) + + (if (#{:main-top :main-nested :main-any} context) + (report-error :not-component-not-allowed + (str/format "Not compoments are not allowed inside a main") + shape file page) + (validate-shape-not-component shape file page libraries)))))) + + (deref *errors*)))) + +(defn validate-file + "Validate referencial integrity and semantic coherence of all contents of a file." + [file libraries & {:keys [throw?] :or {throw? false}}] + (binding [*throw-on-error* throw? + *errors* (volatile! [])] + (->> (ctpl/pages-seq (:data file)) + (run! #(validate-shape uuid/zero file % libraries :throw? throw?))) + + (deref *errors*))) diff --git a/common/src/app/common/pages/changes.cljc b/common/src/app/common/pages/changes.cljc index db7742809..0133d2978 100644 --- a/common/src/app/common/pages/changes.cljc +++ b/common/src/app/common/pages/changes.cljc @@ -76,7 +76,6 @@ [:index {:optional true} [:maybe :int]] [:ignore-touched {:optional true} :boolean]]] - [:mod-obj [:map {:title "ModObjChange"} [:type [:= :mod-obj]] diff --git a/common/src/app/common/pages/changes_builder.cljc b/common/src/app/common/pages/changes_builder.cljc index af9b5f452..ba4f9aecd 100644 --- a/common/src/app/common/pages/changes_builder.cljc +++ b/common/src/app/common/pages/changes_builder.cljc @@ -82,6 +82,15 @@ ::file-data fdata ::applied-changes-count 0))) +(defn with-file-data + [changes fdata] + (let [page-id (::page-id (meta changes)) + fdata (assoc-in fdata [:pages-index uuid/zero] + (get-in fdata [:pages-index page-id]))] + (vary-meta changes assoc + ::file-data fdata + ::applied-changes-count 0))) + (defn with-library-data [changes data] (vary-meta changes assoc @@ -711,14 +720,18 @@ :id id :name (:name new-component) :path (:path new-component) + :main-instance-id (:main-instance-id new-component) + :main-instance-page (:main-instance-page new-component) :annotation (:annotation new-component) :objects (:objects new-component)}) ;; this won't exist in components-v2 (update :undo-changes conj {:type :mod-component - :id id - :name (:name prev-component) - :path (:path prev-component) - :annotation (:annotation prev-component) - :objects (:objects prev-component)})) + :id id + :name (:name prev-component) + :path (:path prev-component) + :main-instance-id (:main-instance-id prev-component) + :main-instance-page (:main-instance-page prev-component) + :annotation (:annotation prev-component) + :objects (:objects prev-component)})) changes))) (defn delete-component diff --git a/common/src/app/common/types/component.cljc b/common/src/app/common/types/component.cljc index 2e8cf3dc3..0d1b19943 100644 --- a/common/src/app/common/types/component.cljc +++ b/common/src/app/common/types/component.cljc @@ -166,6 +166,7 @@ :component-id :component-file :component-root + :main-instance :remote-synced :shape-ref :touched)) diff --git a/common/src/app/common/types/components_list.cljc b/common/src/app/common/types/components_list.cljc index fa9e7e7c2..bcec70c24 100644 --- a/common/src/app/common/types/components_list.cljc +++ b/common/src/app/common/types/components_list.cljc @@ -47,7 +47,7 @@ (wrap-object-fn))))))) (defn mod-component - [file-data {:keys [id name path objects annotation]}] + [file-data {:keys [id name path main-instance-id main-instance-page objects annotation]}] (let [wrap-objects-fn feat/*wrap-with-objects-map-fn*] (d/update-in-when file-data [:components id] (fn [component] @@ -59,6 +59,12 @@ (some? path) (assoc :path path) + (some? main-instance-id) + (assoc :main-instance-id main-instance-id) + + (some? main-instance-page) + (assoc :main-instance-page main-instance-page) + (some? objects) (assoc :objects objects) diff --git a/common/src/app/common/types/container.cljc b/common/src/app/common/types/container.cljc index 8e9a0de78..edd216db0 100644 --- a/common/src/app/common/types/container.cljc +++ b/common/src/app/common/types/container.cljc @@ -164,6 +164,8 @@ false (ctk/main-instance? shape) true + (ctk/instance-head? shape) + false :else (component-main? objects (get objects (:parent-id shape))))) diff --git a/common/src/app/common/types/file.cljc b/common/src/app/common/types/file.cljc index 4f4bbd394..1dd585e28 100644 --- a/common/src/app/common/types/file.cljc +++ b/common/src/app/common/types/file.cljc @@ -867,298 +867,6 @@ (println) (dump-page page file libraries* (assoc flags :root-id (:id root)))))))))))))))) -;; Validation - -(declare validate-shape) - -(defn validate-parent-children - "Validate parent and children exists, and the link is bidirectional." - [shape file page report-error] - (let [parent (ctst/get-shape page (:parent-id shape))] - (if (nil? parent) - (report-error :parent-not-found - (str/format "Parent %s not found" (:parent-id shape)) - shape file page) - (do - (when-not (cph/root? shape) - (when-not (some #{(:id shape)} (:shapes parent)) - (report-error :child-not-in-parent - (str/format "Shape %s not in parent's children list" (:id shape)) - shape file page))) - - (doseq [child-id (:shapes shape)] - (when (nil? (ctst/get-shape page child-id)) - (report-error :child-not-found - (str/format "Child %s not found" child-id) - shape file page))))))) - -(defn validate-frame - "Validate that the frame-id shape exists and is indeed a frame." - [shape file page report-error] - (let [frame (ctst/get-shape page (:frame-id shape))] - (if (nil? frame) - (report-error :frame-not-found - (str/format "Frame %s not found" (:frame-id shape)) - shape file page) - (when (not= (:type frame) :frame) - (report-error :invalid-frame - (str/format "Frame %s is not actually a frame" (:frame-id shape)) - shape file page))))) - -(defn validate-component-main-head - "Validate shape is a main instance head, component exists and its main-instance points to this shape." - [shape file page libraries report-error] - (when (nil? (:main-instance shape)) - (report-error :component-not-main - (str/format "Shape expected to be main instance") - shape file page)) - (let [component (resolve-component shape file libraries {:include-deleted? true})] - (if (nil? component) - (report-error :component-not-found - (str/format "Component %s not found in file" (:component-id shape) (:component-file shape)) - shape file page) - (do - (when-not (= (:main-instance-id component) (:id shape)) - (report-error :invalid-main-instance-id - (str/format "Main instance id of component %s is not valid" (:component-id shape)) - shape file page)) - (when-not (= (:main-instance-page component) (:id page)) - (report-error :invalid-main-instance-page - (str/format "Main instance page of component %s is not valid" (:component-id shape)) - shape file page)))))) - -(defn validate-component-not-main-head - "Validate shape is a not-main instance head, component exists and its main-instance does not point to this shape." - [shape file page libraries report-error] - (when (some? (:main-instance shape)) - (report-error :component-not-main - (str/format "Shape not expected to be main instance") - shape file page)) - (let [component (resolve-component shape file libraries {:include-deleted? true})] - (if (nil? component) - (report-error :component-not-found - (str/format "Component %s not found in file" (:component-id shape) (:component-file shape)) - shape file page) - (do - (when (and (= (:main-instance-id component) (:id shape)) - (= (:main-instance-page component) (:id page))) - (report-error :invalid-main-instance - (str/format "Main instance of component %s should not be this shape" (:id component)) - shape file page)))))) - -(defn validate-component-not-main-not-head - "Validate that this shape is not main instance and not head." - [shape file page report-error] - (when (some? (:main-instance shape)) - (report-error :component-main - (str/format "Shape not expected to be main instance") - shape file page)) - (when (or (some? (:component-id shape)) - (some? (:component-file shape))) - (report-error :component-main - (str/format "Shape not expected to be component head") - shape file page))) - -(defn validate-component-root - "Validate that this shape is an instance root." - [shape file page report-error] - (when (nil? (:component-root shape)) - (report-error :missing-component-root - (str/format "Shape should be component root") - shape file page))) - -(defn validate-component-not-root - "Validate that this shape is not an instance root." - [shape file page report-error] - (when (some? (:component-root shape)) - (report-error :missing-component-root - (str/format "Shape should not be component root") - shape file page))) - -(defn validate-component-ref - "Validate that the referenced shape exists in the near component." - [shape file page libraries report-error] - (let [ref-shape (find-ref-shape file page libraries shape)] - (when (nil? ref-shape) - (report-error :missing-component-root - (str/format "Referenced shape %s not found in near component" (:shape-ref shape)) - shape file page)))) - -(defn validate-component-not-ref - "Validate that this shape does not reference other one." - [shape file page report-error] - (when (some? (:shape-ref shape)) - (report-error :shape-ref-in-main - (str/format "Shape inside main instance should not have shape-ref") - shape file page))) - -(defn validate-shape-main-root-top - "Root shape of a top main instance - :main-instance - :component-id - :component-file - :component-root" - [shape file page libraries report-error] - (validate-component-main-head shape file page libraries report-error) - (validate-component-root shape file page report-error) - (validate-component-not-ref shape file page report-error) - (doseq [child-id (:shapes shape)] - (validate-shape child-id file page libraries :context :main-top :report-error report-error))) - -(defn validate-shape-main-root-nested - "Root shape of a nested main instance - :main-instance - :component-id - :component-file" - [shape file page libraries report-error] - (validate-component-main-head shape file page libraries report-error) - (validate-component-not-root shape file page report-error) - (validate-component-not-ref shape file page report-error) - (doseq [child-id (:shapes shape)] - (validate-shape child-id file page libraries :context :main-nested :report-error report-error))) - -(defn validate-shape-copy-root-top - "Root shape of a top copy instance - :component-id - :component-file - :component-root - :shape-ref" - [shape file page libraries report-error] - (validate-component-not-main-head shape file page libraries report-error) - (validate-component-root shape file page report-error) - (validate-component-ref shape file page libraries report-error) - (doseq [child-id (:shapes shape)] - (validate-shape child-id file page libraries :context :copy-top :report-error report-error))) - -(defn validate-shape-copy-root-nested - "Root shape of a nested copy instance - :component-id - :component-file - :shape-ref" - [shape file page libraries report-error] - (validate-component-not-main-head shape file page libraries report-error) - (validate-component-not-root shape file page report-error) - (validate-component-ref shape file page libraries report-error) - (doseq [child-id (:shapes shape)] - (validate-shape child-id file page libraries :context :copy-nested :report-error report-error))) - -(defn validate-shape-main-not-root - "Not-root shape of a main instance - (not any attribute)" - [shape file page libraries report-error] - (validate-component-not-main-not-head shape file page report-error) - (validate-component-not-root shape file page report-error) - (validate-component-not-ref shape file page report-error) - (doseq [child-id (:shapes shape)] - (validate-shape child-id file page libraries :context :main-any :report-error report-error))) - -(defn validate-shape-copy-not-root - "Not-root shape of a copy instance - :shape-ref" - [shape file page libraries report-error] - (validate-component-not-main-not-head shape file page report-error) - (validate-component-not-root shape file page report-error) - (validate-component-ref shape file page libraries report-error) - (doseq [child-id (:shapes shape)] - (validate-shape child-id file page libraries :context :copy-any :report-error report-error))) - -(defn validate-shape-not-component - "Shape is not in a component or is a fostered children - (not any attribute)" - [shape file page libraries report-error] - (validate-component-not-main-not-head shape file page report-error) - (validate-component-not-root shape file page report-error) - (validate-component-not-ref shape file page report-error) - (doseq [child-id (:shapes shape)] - (validate-shape child-id file page libraries :context :not-component :report-error report-error))) - -(defn validate-shape - "Validate referential integrity and semantic coherence of a shape and all its children. - - The context is the situation of the parent in respect to components: - :not-component - :main-top - :main-nested - :copy-top - :copy-nested - :main-any - :copy-any" - [shape-id file page libraries & {:keys [context throw? report-error] - :or {context :not-component throw? false}}] - (let [shape (ctst/get-shape page shape-id) - errors (volatile! []) - - report-error (or report-error - (fn [code msg shape file page] - (if throw? - (throw (ex-info msg {:type :validation - :code code - :hint msg - ::explain (str/format "file %s\npage %s\nshape %s" - (:id file) - (:id page) - (:id shape))})) - (vswap! errors conj {:hint msg - :code code - :shape shape}))))] - - (dm/assert! (str/format "Shape %s not found" shape-id) (some? shape)) - - (validate-parent-children shape file page report-error) - (validate-frame shape file page report-error) - - (if (ctk/main-instance? shape) - - (if (ctk/instance-root? shape) - (if (not= context :not-component) - (report-error :root-main-not-allowed - (str/format "Root main component not allowed inside other component") - shape file page) - (validate-shape-main-root-top shape file page libraries report-error)) - - (if (= context :not-component) - (report-error :nested-main-not-allowed - (str/format "Nested main component only allowed inside other component") - shape file page) - (validate-shape-main-root-nested shape file page libraries report-error))) - - (if (ctk/instance-head? shape) - - (if (ctk/instance-root? shape) - (if (not= context :not-component) - (report-error :root-copy-not-allowed - (str/format "Root copy not allowed inside other component") - shape file page) - (validate-shape-copy-root-top shape file page libraries report-error)) - - (if (= context :not-component) - (report-error :nested-copy-not-allowed - (str/format "Nested copy only allowed inside other component") - shape file page) - (validate-shape-copy-root-nested shape file page libraries report-error))) - - (if (ctn/component-main? (:objects page) shape) - (if-not (#{:main-top :main-nested :main-any} context) - (report-error :not-head-main-not-allowed - (str/format "Non-root main only allowed inside a main component") - shape file page) - (validate-shape-main-not-root shape file page libraries report-error)) - - (if (ctk/in-component-copy? shape) - (if-not (#{:copy-top :copy-nested :copy-any} context) - (report-error :not-head-copy-not-allowed - (str/format "Non-root copy only allowed inside a copy") - shape file page) - (validate-shape-copy-not-root shape file page libraries report-error)) - - (if (#{:main-top :main-nested :main-any} context) - (report-error :not-component-not-allowed - (str/format "Not compoments are not allowed inside a main") - shape file page) - (validate-shape-not-component shape file page libraries report-error)))))) - - @errors)) - ;; Export (defn- get-component-ref-file diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs index 158e564a2..fd01c7c58 100644 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ b/frontend/src/app/main/data/workspace/persistence.cljs @@ -271,6 +271,3 @@ (update-in [:workspace-libraries file-id :data] cp/process-changes changes))) state)))) - - - diff --git a/frontend/src/debug.cljs b/frontend/src/debug.cljs index 016a9fa1b..d9f1bd308 100644 --- a/frontend/src/debug.cljs +++ b/frontend/src/debug.cljs @@ -8,6 +8,8 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.repair :as cfr] + [app.common.files.validate :as cfv] [app.common.logging :as l] [app.common.math :as mth] [app.common.transit :as t] @@ -21,6 +23,7 @@ [app.main.data.workspace.changes :as dwc] [app.main.data.workspace.path.shortcuts] [app.main.data.workspace.shortcuts] + [app.main.features :as features] [app.main.repo :as rp] [app.main.store :as st] [app.util.dom :as dom] @@ -33,6 +36,8 @@ [potok.core :as ptk] [promesa.core :as p])) +(l/set-level! :debug) + (defn ^:export set-logging ([level] (l/set-level! :app (keyword level))) @@ -431,14 +436,49 @@ ([shape-id] (let [file (assoc (get @st/state :workspace-file) :data (get @st/state :workspace-data)) - page (dm/get-in file [:data :pages-index (get @st/state :current-page-id)]) libraries (get @st/state :workspace-libraries) - errors (ctf/validate-shape (or shape-id uuid/zero) - file - page - libraries)] - (clj->js errors)))) + errors (if shape-id + (let [page (dm/get-in file [:data :pages-index (get @st/state :current-page-id)])] + (cfv/validate-shape (uuid shape-id) file page libraries)) + (cfv/validate-file file libraries))] + + (clj->js (d/group-by :code errors))))) + +;; --- Repair file + +(defn ^:export repair + [] + (let [file (assoc (get @st/state :workspace-file) + :data (get @st/state :workspace-data)) + libraries (get @st/state :workspace-libraries) + errors (cfv/validate-file file libraries)] + + (l/dbg :hint "repair current file" :errors (count errors)) + + (st/emit! + (ptk/reify ::repair-current-file + ptk/WatchEvent + (watch [_ state _] + (let [features (cond-> #{} + (features/active-feature? state :components-v2) + (conj "components/v2")) + sid (:session-id state) + file (get state :workspace-file) + file-data (get state :workspace-data) + libraries (get state :workspace-libraries) + + changes (-> (cfr/repair-file file-data libraries errors) + (get :redo-changes)) + + params {:id (:id file) + :revn (:revn file) + :session-id sid + :changes changes + :features features}] + + (->> (rp/cmd! :update-file params) + (rx/tap #(dom/reload-current-window))))))))) (defn ^:export fix-orphan-shapes []