diff --git a/backend/src/app/graph/ingest.clj b/backend/src/app/graph/ingest.clj new file mode 100644 index 0000000000..569bb82008 --- /dev/null +++ b/backend/src/app/graph/ingest.clj @@ -0,0 +1,41 @@ +;; 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 Sucursal en España SL + +(ns app.graph.ingest + "Penpot file -> Ladybug graph projection. + + Skeleton stage: loads the canonical file from the backend and exercises + Ladybug. Document projection will replace the smoke test step." + (:require + [app.binfile.common :as bfc] + [app.common.exceptions :as ex] + [app.common.logging :as l] + [app.db :as db] + [app.graph.ladybug :as ladybug] + [app.srepl.helpers :as h])) + +(defn ingest-file! + [system file-id & {:keys [db-path smoke-test?] + :or {smoke-test? true}}] + (let [file-id (h/parse-uuid file-id) + file (db/run! system #(bfc/get-file % file-id :realize? true)) + db-path (or db-path (ladybug/db-path-for-file file-id))] + (when-not file + (ex/raise :type :not-found + :code :file-not-found + :file-id (str file-id))) + (l/inf :hint "graph ingest skeleton" + :file-id (str file-id) + :revn (:revn file) + :db-path db-path) + ;; TODO: project (:data file) into Ladybug node/rel tables. + (let [ladybug-result (when smoke-test? + (ladybug/smoke-test! system :db-path db-path))] + {:file-id file-id + :revn (:revn file) + :name (get-in file [:data :name]) + :db-path db-path + :ladybug ladybug-result}))) diff --git a/backend/src/app/graph/ladybug.clj b/backend/src/app/graph/ladybug.clj new file mode 100644 index 0000000000..4b309db524 --- /dev/null +++ b/backend/src/app/graph/ladybug.clj @@ -0,0 +1,78 @@ +;; 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 Sucursal en España SL + +(ns app.graph.ladybug + "Thin Ladybug access layer for graph-backed Penpot. + + Uses the `lbug` CLI for now. A JNI-backed implementation can replace + `exec!` later without changing callers." + (:require + [app.common.exceptions :as ex] + [app.util.shell :as shell] + [clojure.string :as str] + [datoteka.fs :as fs]) + (:import + java.io.File + java.nio.file.Files)) + +(set! *warn-on-reflection* true) + +(defn- lbug-bin + [] + (or (System/getenv "PENPOT_LBUG_BIN") "lbug")) + +(defn default-graph-dir + [] + (or (System/getenv "PENPOT_GRAPH_DIR") "/tmp/penpot-graph")) + +(defn db-path-for-file + [file-id] + (str (fs/path (default-graph-dir) (str file-id ".lbug")))) + +(defn- write-temp-script! + [statements] + (let [^File file (File/createTempFile "penpot-graph-" ".cypher") + content (str/join "\n" (concat statements [":quit"]))] + (spit file content) + (.getAbsolutePath file))) + +(defn exec! + "Execute one or more Cypher statements against a Ladybug database. + + `db-path` is either `:memory:` or a filesystem path to a `.lbug` database." + [system db-path statements & {:keys [timeout] :or {timeout 120}}] + (assert (sequential? statements) "statements should be a sequential collection") + (when-not (= db-path ":memory:") + (fs/create-dir (fs/parent db-path))) + (let [script-path (write-temp-script! statements) + result (shell/exec! + system + {:cmd [(lbug-bin) db-path "-i" script-path "-m" "csv" "-s" "-b"] + :timeout timeout})] + (Files/deleteIfExists (fs/path script-path)) + (when (not= 0 (:exit result)) + (ex/raise :type :internal + :code :ladybug-exec-failed + :hint "Ladybug query execution failed" + :db-path db-path + :exit (:exit result) + :err (:err result) + :out (:out result))) + result)) + +(defn smoke-test-statements + [] + ["CREATE NODE TABLE Person(name STRING, age INT64, PRIMARY KEY(name));" + "CREATE (:Person {name: 'Alice', age: 25});" + "CREATE (:Person {name: 'Bob', age: 30});" + "MATCH (a:Person) RETURN a.name AS NAME, a.age AS AGE ORDER BY NAME;"]) + +(defn smoke-test! + "Run a minimal CREATE + MATCH against Ladybug." + [system & {:keys [db-path] :or {db-path ":memory:"}}] + (let [result (exec! system db-path (smoke-test-statements))] + {:db-path db-path + :out (:out result)})) diff --git a/backend/src/app/srepl/main.clj b/backend/src/app/srepl/main.clj index 72792b7f5e..3e5c85fb2a 100644 --- a/backend/src/app/srepl/main.clj +++ b/backend/src/app/srepl/main.clj @@ -25,6 +25,8 @@ [app.db.sql :as-alias sql] [app.features.fdata :as fdata] [app.features.file-snapshots :as fsnap] + [app.graph.ingest :as graph.ingest] + [app.graph.ladybug :as graph.ladybug] [app.http.session :as session] [app.loggers.audit :as audit] [app.main :as main] @@ -398,6 +400,27 @@ (println (sm/humanize-explain explain)) (ex/print-throwable cause)))))))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; GRAPH / LADYBUG +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn graph-smoke-test! + "Execute a basic Ladybug smoke test (CREATE + MATCH). + + Requires the `lbug` CLI on PATH, or set PENPOT_LBUG_BIN. Use :db-path + \":memory:\" (default) or a filesystem path such as /tmp/test.lbug." + [& {:keys [db-path] :or {db-path ":memory:"}}] + (graph.ladybug/smoke-test! main/system :db-path db-path)) + +(defn ingest-file-to-graph! + "Skeleton graph ingest for a Penpot file. + + Loads and realizes the file from the database, prepares the per-file + Ladybug database path, and (for now) runs the Ladybug smoke test. + Full document projection is not implemented yet." + [file-id & {:as opts}] + (graph.ingest/ingest-file! main/system file-id opts)) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; PROCESSING ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;