mirror of
https://github.com/penpot/penpot.git
synced 2026-08-24 05:38:38 +00:00
✨ Add Component nodes and IsInstanceOf edges
This commit is contained in:
parent
f234f383f5
commit
abcadb5cf8
@ -223,14 +223,23 @@
|
|||||||
|
|
||||||
(defn- export-edges
|
(defn- export-edges
|
||||||
[conn]
|
[conn]
|
||||||
(let [stmt (str "MATCH (a)-[r:IsChildOf]->(b) "
|
(let [child-stmt (str "MATCH (a)-[r:IsChildOf]->(b) "
|
||||||
"RETURN a.id AS source, b.id AS target, r.position AS position;")
|
"RETURN a.id AS source, b.id AS target, r.position AS position, "
|
||||||
{:keys [rows truncated?]}
|
"'IsChildOf' AS rel;")
|
||||||
(ladybug/query-on-connection! conn stmt :max-rows export-max-rows)]
|
inst-stmt (str "MATCH (a)-[r:IsInstanceOf]->(b) "
|
||||||
{:edges (mapv (fn [[source target position]]
|
"RETURN a.id AS source, b.id AS target, NULL AS position, "
|
||||||
{:source (str source) :target (str target) :position position})
|
"'IsInstanceOf' AS rel;")
|
||||||
rows)
|
child (ladybug/query-on-connection! conn child-stmt :max-rows export-max-rows)
|
||||||
:truncated? truncated?}))
|
inst (ladybug/query-on-connection! conn inst-stmt :max-rows export-max-rows)
|
||||||
|
->edge (fn [[source target position rel]]
|
||||||
|
(cond-> {:source (str source)
|
||||||
|
:target (str target)
|
||||||
|
:rel (str rel)}
|
||||||
|
(some? position) (assoc :position position)))]
|
||||||
|
{:edges (into (mapv ->edge (:rows child))
|
||||||
|
(map ->edge)
|
||||||
|
(:rows inst))
|
||||||
|
:truncated? (boolean (or (:truncated? child) (:truncated? inst)))}))
|
||||||
|
|
||||||
(defn export-graph-data!
|
(defn export-graph-data!
|
||||||
"Export the node/edge inventory of the in-memory graph for `profile-id`
|
"Export the node/edge inventory of the in-memory graph for `profile-id`
|
||||||
|
|||||||
@ -66,7 +66,7 @@
|
|||||||
:projection {:stats stats
|
:projection {:stats stats
|
||||||
:nodes nodes
|
:nodes nodes
|
||||||
:edges edges}
|
:edges edges}
|
||||||
:transforms (project.transforms/apply-transforms! system db-path data file)
|
:transforms (project.transforms/apply-transforms! system conn data file)
|
||||||
:stats (when-not skip-stats?
|
:stats (when-not skip-stats?
|
||||||
(stats/summarize-connection conn))})))
|
(stats/summarize-connection conn))})))
|
||||||
|
|
||||||
|
|||||||
@ -81,13 +81,50 @@
|
|||||||
(coll? v) (format-json v)
|
(coll? v) (format-json v)
|
||||||
:else (format-string (str v))))
|
:else (format-string (str v))))
|
||||||
|
|
||||||
|
(defn- format-list-element
|
||||||
|
"Format one element of a Cypher LIST literal for typed `elem-type`."
|
||||||
|
[elem-type v]
|
||||||
|
(case elem-type
|
||||||
|
"UUID" (format-uuid v)
|
||||||
|
"STRING" (format-string (str v))
|
||||||
|
"JSON" (format-json v)
|
||||||
|
"INT64" (format-int v)
|
||||||
|
"DOUBLE" (format-number v)
|
||||||
|
"BOOLEAN" (if v "true" "false")
|
||||||
|
(format-value v)))
|
||||||
|
|
||||||
|
(defn- format-list
|
||||||
|
"Cypher LIST literal for Ladybug LIST columns (`UUID[]`, `STRING[]`, …).
|
||||||
|
|
||||||
|
Must not use `json(...)`: assigning a JSON value to `UUID[]` yields
|
||||||
|
`Conversion exception: Invalid UUID` (e.g. Frame.`shapes` on component
|
||||||
|
instantiate via sync)."
|
||||||
|
[ladybug-type v]
|
||||||
|
(let [elem-type (subs ladybug-type 0 (- (count ladybug-type) 2))
|
||||||
|
elems (if (coll? v) (seq v) [v])]
|
||||||
|
(str "["
|
||||||
|
(str/join ", " (map #(format-list-element elem-type %) elems))
|
||||||
|
"]")))
|
||||||
|
|
||||||
(defn format-typed-value
|
(defn format-typed-value
|
||||||
[ladybug-type v]
|
[ladybug-type v]
|
||||||
(cond
|
(cond
|
||||||
(= ladybug-type "JSON") (format-json v)
|
(nil? v)
|
||||||
|
"NULL"
|
||||||
|
|
||||||
|
(= ladybug-type "JSON")
|
||||||
|
(format-json v)
|
||||||
|
|
||||||
|
;; Coerce string ids from transit edge-cases into UUID literals.
|
||||||
|
(= ladybug-type "UUID")
|
||||||
|
(format-uuid v)
|
||||||
|
|
||||||
(and (string? ladybug-type)
|
(and (string? ladybug-type)
|
||||||
(str/ends-with? ladybug-type "[]")) (format-json v)
|
(str/ends-with? ladybug-type "[]"))
|
||||||
:else (format-value v)))
|
(format-list ladybug-type v)
|
||||||
|
|
||||||
|
:else
|
||||||
|
(format-value v)))
|
||||||
|
|
||||||
(defn- ensure-semicolon
|
(defn- ensure-semicolon
|
||||||
[statement]
|
[statement]
|
||||||
|
|||||||
@ -7,8 +7,8 @@
|
|||||||
(ns app.graph.project.document
|
(ns app.graph.project.document
|
||||||
"Project a Penpot file-data map into Ladybug nodes and structural edges.
|
"Project a Penpot file-data map into Ladybug nodes and structural edges.
|
||||||
|
|
||||||
Projects Document, Page, the full shape tree (skipping the root frame),
|
Projects Document, Page, Component, the full shape tree (skipping the root
|
||||||
and `IsChildOf` edges from shapes to their page or container parent."
|
frame), and `IsChildOf` edges from shapes/pages/components to their parent."
|
||||||
(:require
|
(:require
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
@ -29,6 +29,13 @@
|
|||||||
(dissoc :objects)
|
(dissoc :objects)
|
||||||
(cond-> (some? index) (assoc :index (long index)))))
|
(cond-> (some? index) (assoc :index (long index)))))
|
||||||
|
|
||||||
|
(defn- component-attrs
|
||||||
|
[component]
|
||||||
|
(-> component
|
||||||
|
(dissoc :objects)
|
||||||
|
;; schema:component requires :path; some legacy rows omit it
|
||||||
|
(update :path #(or % ""))))
|
||||||
|
|
||||||
(defn- shape-table
|
(defn- shape-table
|
||||||
[shape]
|
[shape]
|
||||||
(nodes/table-for-type (:type shape)))
|
(nodes/table-for-type (:type shape)))
|
||||||
@ -51,7 +58,7 @@
|
|||||||
[]
|
[]
|
||||||
{:nodes {}
|
{:nodes {}
|
||||||
:edges []
|
:edges []
|
||||||
:stats {:documents 0 :pages 0 :shapes 0}})
|
:stats {:documents 0 :pages 0 :components 0 :shapes 0}})
|
||||||
|
|
||||||
(declare project-shape-ids)
|
(declare project-shape-ids)
|
||||||
|
|
||||||
@ -108,26 +115,52 @@
|
|||||||
(project-shape-ids objects acc' "Page" page-id top-level-ids)
|
(project-shape-ids objects acc' "Page" page-id top-level-ids)
|
||||||
acc')))
|
acc')))
|
||||||
|
|
||||||
|
(defn- project-component
|
||||||
|
[acc doc-id component position]
|
||||||
|
(if (:deleted component)
|
||||||
|
acc
|
||||||
|
(let [comp-id (:id component)
|
||||||
|
node (nodes/project-attrs "Component" (component-attrs component))]
|
||||||
|
(-> acc
|
||||||
|
(update-in [:nodes "Component"] (fnil conj []) node)
|
||||||
|
(update :edges conj {:from-table "Component"
|
||||||
|
:from-id comp-id
|
||||||
|
:to-table "Document"
|
||||||
|
:to-id doc-id
|
||||||
|
:position position})
|
||||||
|
(update-in [:stats :components] inc)))))
|
||||||
|
|
||||||
|
(defn- project-components
|
||||||
|
[acc doc-id components]
|
||||||
|
(reduce (fn [acc [position [_id component]]]
|
||||||
|
(project-component acc doc-id component position))
|
||||||
|
acc
|
||||||
|
(map-indexed vector components)))
|
||||||
|
|
||||||
(defn projection-data
|
(defn projection-data
|
||||||
"Build node/edge rows for projecting `data` into Ladybug.
|
"Build node/edge rows for projecting `data` into Ladybug.
|
||||||
|
|
||||||
Returns `{:nodes {table [attrs ...]} :edges [...] :stats {...}}`."
|
Returns `{:nodes {table [attrs ...]} :edges [...] :stats {...}}`."
|
||||||
[data file]
|
[data file]
|
||||||
(let [doc-id (or (:id data) (:id file))
|
(let [doc-id (or (:id data) (:id file))
|
||||||
doc-node (nodes/project-attrs "Document" (document-attrs file data))
|
doc-node (nodes/project-attrs "Document" (document-attrs file data))
|
||||||
pages (seq (reverse (:pages data)))
|
pages (seq (reverse (:pages data)))
|
||||||
acc0 (-> (initial-acc)
|
comps (seq (:components data))
|
||||||
(update-in [:nodes "Document"] (fnil conj []) doc-node)
|
acc0 (-> (initial-acc)
|
||||||
(assoc-in [:stats :documents] 1))
|
(update-in [:nodes "Document"] (fnil conj []) doc-node)
|
||||||
acc (if (empty? pages)
|
(assoc-in [:stats :documents] 1))
|
||||||
acc0
|
acc (cond-> acc0
|
||||||
(reduce (fn [acc [position page-id]]
|
(seq comps)
|
||||||
(if-let [page (get-in data [:pages-index page-id])]
|
(project-components doc-id comps))
|
||||||
(project-page acc doc-id page position)
|
acc (if (empty? pages)
|
||||||
(do
|
acc
|
||||||
(l/wrn :hint "missing page in pages-index"
|
(reduce (fn [acc [position page-id]]
|
||||||
:page-id (str page-id))
|
(if-let [page (get-in data [:pages-index page-id])]
|
||||||
acc)))
|
(project-page acc doc-id page position)
|
||||||
acc0
|
(do
|
||||||
(map-indexed vector pages)))]
|
(l/wrn :hint "missing page in pages-index"
|
||||||
|
:page-id (str page-id))
|
||||||
|
acc)))
|
||||||
|
acc
|
||||||
|
(map-indexed vector pages)))]
|
||||||
(select-keys acc [:nodes :edges :stats])))
|
(select-keys acc [:nodes :edges :stats])))
|
||||||
|
|||||||
@ -7,9 +7,36 @@
|
|||||||
(ns app.graph.project.transforms
|
(ns app.graph.project.transforms
|
||||||
"Derived graph links (instances, tokens, nested containment, etc.).
|
"Derived graph links (instances, tokens, nested containment, etc.).
|
||||||
|
|
||||||
Stub for now: beadpot's `apply_transformations` will be ported here.")
|
Ports beadpot's post-projection transforms. Currently:
|
||||||
|
- `LinkComponentInstances` → `IsInstanceOf` (Frame → Component)."
|
||||||
|
(:require
|
||||||
|
[app.common.logging :as l]
|
||||||
|
[app.graph.ladybug :as ladybug])
|
||||||
|
(:import
|
||||||
|
com.ladybugdb.Connection))
|
||||||
|
|
||||||
|
(defn- link-component-instances!
|
||||||
|
"Create `IsInstanceOf` edges from Frame instance heads to Component nodes.
|
||||||
|
|
||||||
|
Same semantics as beadpot `LinkComponentInstances`: every frame with
|
||||||
|
`:component-id` matching a non-deleted Component in this graph (main
|
||||||
|
instance and copy roots)."
|
||||||
|
[^Connection conn]
|
||||||
|
(let [n (or (ladybug/query-scalar-on-connection!
|
||||||
|
conn
|
||||||
|
(str "MATCH (f:Frame), (c:Component) "
|
||||||
|
"WHERE f.`component-id` = c.id "
|
||||||
|
"AND NOT COALESCE(c.deleted, false) "
|
||||||
|
"MERGE (f)-[:IsInstanceOf]->(c) "
|
||||||
|
"RETURN count(*);"))
|
||||||
|
0)]
|
||||||
|
(l/inf :hint "graph transform IsInstanceOf"
|
||||||
|
:edges n)
|
||||||
|
n))
|
||||||
|
|
||||||
(defn apply-transforms!
|
(defn apply-transforms!
|
||||||
"Apply derived transformations to an already projected graph."
|
"Apply derived transformations on an already loaded graph."
|
||||||
[_system _db-path _data _file]
|
[_system ^Connection conn _data _file]
|
||||||
{:transforms 0})
|
(let [n (link-component-instances! conn)]
|
||||||
|
{:transforms 1
|
||||||
|
:IsInstanceOf n}))
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
(:require
|
(:require
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
|
[app.common.types.component :as ctk]
|
||||||
[app.common.types.file :as ctf]
|
[app.common.types.file :as ctf]
|
||||||
[app.common.types.page :as ctp]
|
[app.common.types.page :as ctp]
|
||||||
[app.graph.schema.projection :as projection]
|
[app.graph.schema.projection :as projection]
|
||||||
@ -20,7 +21,7 @@
|
|||||||
[clojure.string :as str]))
|
[clojure.string :as str]))
|
||||||
|
|
||||||
(def schema-version
|
(def schema-version
|
||||||
"penpot-graph-slice-2")
|
"penpot-graph-slice-3")
|
||||||
|
|
||||||
;; beadpot/graph/schemas.py drop_fields
|
;; beadpot/graph/schemas.py drop_fields
|
||||||
(def ^:private document-projection
|
(def ^:private document-projection
|
||||||
@ -31,6 +32,14 @@
|
|||||||
{:source ctp/schema:page
|
{:source ctp/schema:page
|
||||||
:drop [:objects]})
|
:drop [:objects]})
|
||||||
|
|
||||||
|
(def ^:private component-projection
|
||||||
|
{:source ctk/schema:component
|
||||||
|
:drop [:objects]
|
||||||
|
;; Soft-delete flag used at runtime; not in schema:component.
|
||||||
|
:extra [:map
|
||||||
|
[:deleted {:optional true} :boolean]
|
||||||
|
[:annotation {:optional true} :string]]})
|
||||||
|
|
||||||
(def ^:private shape-projection
|
(def ^:private shape-projection
|
||||||
{:drop [:type]})
|
{:drop [:type]})
|
||||||
|
|
||||||
@ -77,7 +86,11 @@
|
|||||||
{:table "Page"
|
{:table "Page"
|
||||||
:pk :id
|
:pk :id
|
||||||
:projection page-projection
|
:projection page-projection
|
||||||
:schema (resolve-schema page-projection)}]
|
:schema (resolve-schema page-projection)}
|
||||||
|
{:table "Component"
|
||||||
|
:pk :id
|
||||||
|
:projection component-projection
|
||||||
|
:schema (resolve-schema component-projection)}]
|
||||||
(map shape-node-entry shape-node-types)))
|
(map shape-node-entry shape-node-types)))
|
||||||
|
|
||||||
(def ^:private by-table
|
(def ^:private by-table
|
||||||
@ -216,6 +229,7 @@
|
|||||||
[]
|
[]
|
||||||
(str "CREATE REL TABLE `IsChildOf` ("
|
(str "CREATE REL TABLE `IsChildOf` ("
|
||||||
"FROM `Page` TO `Document`, "
|
"FROM `Page` TO `Document`, "
|
||||||
|
"FROM `Component` TO `Document`, "
|
||||||
(str/join ", "
|
(str/join ", "
|
||||||
(concat
|
(concat
|
||||||
(map (fn [shape]
|
(map (fn [shape]
|
||||||
@ -226,7 +240,13 @@
|
|||||||
(str "FROM `" shape "` TO `" container "`"))))
|
(str "FROM `" shape "` TO `" container "`"))))
|
||||||
", `position` INT64);"))
|
", `position` INT64);"))
|
||||||
|
|
||||||
|
(defn is-instance-of-ddl
|
||||||
|
"Frame instance heads → Component (beadpot `IsInstanceOf`)."
|
||||||
|
[]
|
||||||
|
"CREATE REL TABLE `IsInstanceOf` (FROM `Frame` TO `Component`);")
|
||||||
|
|
||||||
(defn ddl-statements
|
(defn ddl-statements
|
||||||
[]
|
[]
|
||||||
(conj (mapv create-node-table-ddl node-types)
|
(conj (mapv create-node-table-ddl node-types)
|
||||||
(is-child-of-ddl)))
|
(is-child-of-ddl)
|
||||||
|
(is-instance-of-ddl)))
|
||||||
@ -25,7 +25,10 @@
|
|||||||
(map :table nodes/node-types)))
|
(map :table nodes/node-types)))
|
||||||
:edges {:IsChildOf (count-on-connection
|
:edges {:IsChildOf (count-on-connection
|
||||||
conn
|
conn
|
||||||
"MATCH ()-[e:IsChildOf]->() RETURN count(e) AS IsChildOf_c;")}})
|
"MATCH ()-[e:IsChildOf]->() RETURN count(e) AS IsChildOf_c;")
|
||||||
|
:IsInstanceOf (count-on-connection
|
||||||
|
conn
|
||||||
|
"MATCH ()-[e:IsInstanceOf]->() RETURN count(e) AS IsInstanceOf_c;")}})
|
||||||
|
|
||||||
(defn summarize
|
(defn summarize
|
||||||
"Return node/edge counts from the graph database."
|
"Return node/edge counts from the graph database."
|
||||||
|
|||||||
@ -147,6 +147,30 @@
|
|||||||
"(p:" (nodes/match-label to-table) " {id: " (ladybug/format-uuid to-id) "}) "
|
"(p:" (nodes/match-label to-table) " {id: " (ladybug/format-uuid to-id) "}) "
|
||||||
"CREATE (s)-[:IsChildOf {position: " (ladybug/format-int position) "}]->(p);"))
|
"CREATE (s)-[:IsChildOf {position: " (ladybug/format-int position) "}]->(p);"))
|
||||||
|
|
||||||
|
(defn- create-instance-of-statement
|
||||||
|
"Link a Frame instance head to its Component (beadpot `IsInstanceOf`).
|
||||||
|
|
||||||
|
No-op when the Component is absent (e.g. library component not ingested)."
|
||||||
|
[frame-id component-id]
|
||||||
|
(str "MATCH (f:Frame {id: " (ladybug/format-uuid frame-id) "}), "
|
||||||
|
"(c:Component {id: " (ladybug/format-uuid component-id) "}) "
|
||||||
|
"WHERE NOT COALESCE(c.deleted, false) "
|
||||||
|
"MERGE (f)-[:IsInstanceOf]->(c);"))
|
||||||
|
|
||||||
|
(defn- delete-instance-of-statement
|
||||||
|
[frame-id]
|
||||||
|
(str "MATCH (f:Frame {id: " (ladybug/format-uuid frame-id) "})"
|
||||||
|
"-[r:IsInstanceOf]->(:Component) "
|
||||||
|
"DELETE r;"))
|
||||||
|
|
||||||
|
(defn- instance-of-statements
|
||||||
|
"Cypher to (re)link `IsInstanceOf` after add/mod of a Frame's component-id."
|
||||||
|
[table shape-id component-id]
|
||||||
|
(when (= table "Frame")
|
||||||
|
(cond-> [(delete-instance-of-statement shape-id)]
|
||||||
|
(some? component-id)
|
||||||
|
(conj (create-instance-of-statement shape-id component-id)))))
|
||||||
|
|
||||||
(defn- delete-edge-statement
|
(defn- delete-edge-statement
|
||||||
[{:keys [from-table from-id to-table to-id]}]
|
[{:keys [from-table from-id to-table to-id]}]
|
||||||
(str "MATCH (s:" (nodes/match-label from-table) " {id: " (ladybug/format-uuid from-id) "})"
|
(str "MATCH (s:" (nodes/match-label from-table) " {id: " (ladybug/format-uuid from-id) "})"
|
||||||
@ -345,7 +369,10 @@
|
|||||||
:from-id id
|
:from-id id
|
||||||
:to-table (:parent-table parent)
|
:to-table (:parent-table parent)
|
||||||
:to-id (:parent-id parent)
|
:to-id (:parent-id parent)
|
||||||
:position position})]
|
:position position})
|
||||||
|
stmts (into [(create-node-statement table attrs)
|
||||||
|
(create-edge-statement edge)]
|
||||||
|
(instance-of-statements table id (:component-id attrs)))]
|
||||||
{:index (index-add-shape! index
|
{:index (index-add-shape! index
|
||||||
{:id id
|
{:id id
|
||||||
:name (:name attrs)
|
:name (:name attrs)
|
||||||
@ -355,27 +382,30 @@
|
|||||||
:position position
|
:position position
|
||||||
:page-id (or page-id (when (= (:parent-table parent) "Page")
|
:page-id (or page-id (when (= (:parent-table parent) "Page")
|
||||||
(:parent-id parent)))})
|
(:parent-id parent)))})
|
||||||
:statements [(create-node-statement table attrs)
|
:statements stmts
|
||||||
(create-edge-statement edge)]
|
|
||||||
:applied? true}))))))
|
:applied? true}))))))
|
||||||
|
|
||||||
(defn- apply-mod-obj
|
(defn- apply-mod-obj
|
||||||
[index {:keys [id operations]}]
|
[index {:keys [id operations]}]
|
||||||
(if-let [shape (get-in index [:shapes id])]
|
(if-let [shape (get-in index [:shapes id])]
|
||||||
(let [table (:table shape)
|
(let [table (:table shape)
|
||||||
syncable (mod-attrs-for-table table)
|
syncable (mod-attrs-for-table table)
|
||||||
set-ops (filter #(and (= :set (:type %))
|
set-ops (filter #(and (= :set (:type %))
|
||||||
(contains? syncable (:attr %)))
|
(contains? syncable (:attr %)))
|
||||||
operations)]
|
operations)]
|
||||||
(if (empty? set-ops)
|
(if (empty? set-ops)
|
||||||
{:index index :statements [] :applied? false :reason :unsupported-operations}
|
{:index index :statements [] :applied? false :reason :unsupported-operations}
|
||||||
(let [updates (into {} (map (juxt :attr :val) set-ops))
|
(let [updates (into {} (map (juxt :attr :val) set-ops))
|
||||||
statements (for [[attr value] updates]
|
statements
|
||||||
(set-node-attr-statement table id attr value))
|
(into (vec (for [[attr value] updates]
|
||||||
index' (reduce (fn [idx [attr value]]
|
(set-node-attr-statement table id attr value)))
|
||||||
(assoc-in idx [:shapes id attr] value))
|
;; Relink when component-id is among the synced attrs.
|
||||||
index
|
(when (contains? updates :component-id)
|
||||||
updates)]
|
(instance-of-statements table id (:component-id updates))))
|
||||||
|
index' (reduce (fn [idx [attr value]]
|
||||||
|
(assoc-in idx [:shapes id attr] value))
|
||||||
|
index
|
||||||
|
updates)]
|
||||||
{:index index'
|
{:index index'
|
||||||
:statements statements
|
:statements statements
|
||||||
:applied? true})))
|
:applied? true})))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user