mirror of
https://github.com/penpot/penpot.git
synced 2026-08-08 13:58:35 +00:00
✨ Add Component nodes and IsInstanceOf edges
This commit is contained in:
parent
f234f383f5
commit
abcadb5cf8
@ -223,14 +223,23 @@
|
||||
|
||||
(defn- export-edges
|
||||
[conn]
|
||||
(let [stmt (str "MATCH (a)-[r:IsChildOf]->(b) "
|
||||
"RETURN a.id AS source, b.id AS target, r.position AS position;")
|
||||
{:keys [rows truncated?]}
|
||||
(ladybug/query-on-connection! conn stmt :max-rows export-max-rows)]
|
||||
{:edges (mapv (fn [[source target position]]
|
||||
{:source (str source) :target (str target) :position position})
|
||||
rows)
|
||||
:truncated? truncated?}))
|
||||
(let [child-stmt (str "MATCH (a)-[r:IsChildOf]->(b) "
|
||||
"RETURN a.id AS source, b.id AS target, r.position AS position, "
|
||||
"'IsChildOf' AS rel;")
|
||||
inst-stmt (str "MATCH (a)-[r:IsInstanceOf]->(b) "
|
||||
"RETURN a.id AS source, b.id AS target, NULL AS position, "
|
||||
"'IsInstanceOf' AS rel;")
|
||||
child (ladybug/query-on-connection! conn child-stmt :max-rows export-max-rows)
|
||||
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!
|
||||
"Export the node/edge inventory of the in-memory graph for `profile-id`
|
||||
|
||||
@ -66,7 +66,7 @@
|
||||
:projection {:stats stats
|
||||
:nodes nodes
|
||||
: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/summarize-connection conn))})))
|
||||
|
||||
|
||||
@ -81,13 +81,50 @@
|
||||
(coll? v) (format-json 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
|
||||
[ladybug-type v]
|
||||
(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)
|
||||
(str/ends-with? ladybug-type "[]")) (format-json v)
|
||||
:else (format-value v)))
|
||||
(str/ends-with? ladybug-type "[]"))
|
||||
(format-list ladybug-type v)
|
||||
|
||||
:else
|
||||
(format-value v)))
|
||||
|
||||
(defn- ensure-semicolon
|
||||
[statement]
|
||||
|
||||
@ -7,8 +7,8 @@
|
||||
(ns app.graph.project.document
|
||||
"Project a Penpot file-data map into Ladybug nodes and structural edges.
|
||||
|
||||
Projects Document, Page, the full shape tree (skipping the root frame),
|
||||
and `IsChildOf` edges from shapes to their page or container parent."
|
||||
Projects Document, Page, Component, the full shape tree (skipping the root
|
||||
frame), and `IsChildOf` edges from shapes/pages/components to their parent."
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.uuid :as uuid]
|
||||
@ -29,6 +29,13 @@
|
||||
(dissoc :objects)
|
||||
(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
|
||||
[shape]
|
||||
(nodes/table-for-type (:type shape)))
|
||||
@ -51,7 +58,7 @@
|
||||
[]
|
||||
{:nodes {}
|
||||
:edges []
|
||||
:stats {:documents 0 :pages 0 :shapes 0}})
|
||||
:stats {:documents 0 :pages 0 :components 0 :shapes 0}})
|
||||
|
||||
(declare project-shape-ids)
|
||||
|
||||
@ -108,26 +115,52 @@
|
||||
(project-shape-ids objects acc' "Page" page-id top-level-ids)
|
||||
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
|
||||
"Build node/edge rows for projecting `data` into Ladybug.
|
||||
|
||||
Returns `{:nodes {table [attrs ...]} :edges [...] :stats {...}}`."
|
||||
[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))
|
||||
pages (seq (reverse (:pages data)))
|
||||
acc0 (-> (initial-acc)
|
||||
(update-in [:nodes "Document"] (fnil conj []) doc-node)
|
||||
(assoc-in [:stats :documents] 1))
|
||||
acc (if (empty? pages)
|
||||
acc0
|
||||
(reduce (fn [acc [position page-id]]
|
||||
(if-let [page (get-in data [:pages-index page-id])]
|
||||
(project-page acc doc-id page position)
|
||||
(do
|
||||
(l/wrn :hint "missing page in pages-index"
|
||||
:page-id (str page-id))
|
||||
acc)))
|
||||
acc0
|
||||
(map-indexed vector pages)))]
|
||||
pages (seq (reverse (:pages data)))
|
||||
comps (seq (:components data))
|
||||
acc0 (-> (initial-acc)
|
||||
(update-in [:nodes "Document"] (fnil conj []) doc-node)
|
||||
(assoc-in [:stats :documents] 1))
|
||||
acc (cond-> acc0
|
||||
(seq comps)
|
||||
(project-components doc-id comps))
|
||||
acc (if (empty? pages)
|
||||
acc
|
||||
(reduce (fn [acc [position page-id]]
|
||||
(if-let [page (get-in data [:pages-index page-id])]
|
||||
(project-page acc doc-id page position)
|
||||
(do
|
||||
(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])))
|
||||
|
||||
@ -7,9 +7,36 @@
|
||||
(ns app.graph.project.transforms
|
||||
"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!
|
||||
"Apply derived transformations to an already projected graph."
|
||||
[_system _db-path _data _file]
|
||||
{:transforms 0})
|
||||
"Apply derived transformations on an already loaded graph."
|
||||
[_system ^Connection conn _data _file]
|
||||
(let [n (link-component-instances! conn)]
|
||||
{:transforms 1
|
||||
:IsInstanceOf n}))
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.page :as ctp]
|
||||
[app.graph.schema.projection :as projection]
|
||||
@ -20,7 +21,7 @@
|
||||
[clojure.string :as str]))
|
||||
|
||||
(def schema-version
|
||||
"penpot-graph-slice-2")
|
||||
"penpot-graph-slice-3")
|
||||
|
||||
;; beadpot/graph/schemas.py drop_fields
|
||||
(def ^:private document-projection
|
||||
@ -31,6 +32,14 @@
|
||||
{:source ctp/schema:page
|
||||
: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
|
||||
{:drop [:type]})
|
||||
|
||||
@ -77,7 +86,11 @@
|
||||
{:table "Page"
|
||||
:pk :id
|
||||
: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)))
|
||||
|
||||
(def ^:private by-table
|
||||
@ -216,6 +229,7 @@
|
||||
[]
|
||||
(str "CREATE REL TABLE `IsChildOf` ("
|
||||
"FROM `Page` TO `Document`, "
|
||||
"FROM `Component` TO `Document`, "
|
||||
(str/join ", "
|
||||
(concat
|
||||
(map (fn [shape]
|
||||
@ -226,7 +240,13 @@
|
||||
(str "FROM `" shape "` TO `" container "`"))))
|
||||
", `position` INT64);"))
|
||||
|
||||
(defn is-instance-of-ddl
|
||||
"Frame instance heads → Component (beadpot `IsInstanceOf`)."
|
||||
[]
|
||||
"CREATE REL TABLE `IsInstanceOf` (FROM `Frame` TO `Component`);")
|
||||
|
||||
(defn ddl-statements
|
||||
[]
|
||||
(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)))
|
||||
:edges {:IsChildOf (count-on-connection
|
||||
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
|
||||
"Return node/edge counts from the graph database."
|
||||
|
||||
@ -147,6 +147,30 @@
|
||||
"(p:" (nodes/match-label to-table) " {id: " (ladybug/format-uuid to-id) "}) "
|
||||
"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
|
||||
[{:keys [from-table from-id to-table to-id]}]
|
||||
(str "MATCH (s:" (nodes/match-label from-table) " {id: " (ladybug/format-uuid from-id) "})"
|
||||
@ -345,7 +369,10 @@
|
||||
:from-id id
|
||||
:to-table (:parent-table 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
|
||||
{:id id
|
||||
:name (:name attrs)
|
||||
@ -355,27 +382,30 @@
|
||||
:position position
|
||||
:page-id (or page-id (when (= (:parent-table parent) "Page")
|
||||
(:parent-id parent)))})
|
||||
:statements [(create-node-statement table attrs)
|
||||
(create-edge-statement edge)]
|
||||
:statements stmts
|
||||
:applied? true}))))))
|
||||
|
||||
(defn- apply-mod-obj
|
||||
[index {:keys [id operations]}]
|
||||
(if-let [shape (get-in index [:shapes id])]
|
||||
(let [table (:table shape)
|
||||
(let [table (:table shape)
|
||||
syncable (mod-attrs-for-table table)
|
||||
set-ops (filter #(and (= :set (:type %))
|
||||
(contains? syncable (:attr %)))
|
||||
operations)]
|
||||
set-ops (filter #(and (= :set (:type %))
|
||||
(contains? syncable (:attr %)))
|
||||
operations)]
|
||||
(if (empty? set-ops)
|
||||
{:index index :statements [] :applied? false :reason :unsupported-operations}
|
||||
(let [updates (into {} (map (juxt :attr :val) set-ops))
|
||||
statements (for [[attr value] updates]
|
||||
(set-node-attr-statement table id attr value))
|
||||
index' (reduce (fn [idx [attr value]]
|
||||
(assoc-in idx [:shapes id attr] value))
|
||||
index
|
||||
updates)]
|
||||
(let [updates (into {} (map (juxt :attr :val) set-ops))
|
||||
statements
|
||||
(into (vec (for [[attr value] updates]
|
||||
(set-node-attr-statement table id attr value)))
|
||||
;; Relink when component-id is among the synced attrs.
|
||||
(when (contains? updates :component-id)
|
||||
(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'
|
||||
:statements statements
|
||||
:applied? true})))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user