From 67873887bf28a26c11fb99d7e111c5a25f8244dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Tejero=20Cantero?= Date: Fri, 7 Aug 2026 11:58:08 +0200 Subject: [PATCH] :sparkles: Add the file-level graph columns and tighten the svg ones Split out of ":bug: Declare the shape attributes stored files carry", which is now #11125 and carries only its `common/` half. This commit is the graph's own side of that change, and it stays on this branch. `app.graph.schema.contract` pins `svg_viewbox` to `DOUBLE[4]` and `svg_transform` to `DOUBLE[6]`. The shape schema types both `:map` on purpose, because legacy files hold them as plain maps rather than as `::grc/rect` and `::gmt/matrix` records, and a tighter *schema* would reject those files. A tighter *column* costs nothing, since `app.graph.schema.values/coerce` reads either form. `app.graph.schema.nodes` declares four file-level attributes as projection `:extra` rather than in `ctf/schema:file`: `:options`, `:backend`, `:comment-thread-seqn`, and `:ignore-sync-until`. Declaring them in the file schema breaks saving, measured at 185 failures, because `app.binfile.common/update-file!` derives its UPDATE column list from a file map's keys and the `file` table has no `backend` column, that value being synthesized on read. An `:extra` is local to the graph and cannot reach a write. `app.graph.project.document` lifts `:options` out of `:data` before the blob is dropped, so a consumer reads file-level configuration without opening the blob. AI-assisted-by: mixed models --- backend/src/app/graph/project/document.clj | 5 +++++ backend/src/app/graph/schema/contract.clj | 9 ++++++++- backend/src/app/graph/schema/nodes.clj | 20 +++++++++++++++++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/backend/src/app/graph/project/document.clj b/backend/src/app/graph/project/document.clj index b7310cb6f7..d859c64594 100644 --- a/backend/src/app/graph/project/document.clj +++ b/backend/src/app/graph/project/document.clj @@ -29,9 +29,14 @@ uuid/zero) (defn- document-attrs + "The Document node's attrs: the file row, minus its data blob. + + `:options` is lifted out of the blob before it goes: it is file-level + configuration a consumer wants without opening `:data`." [file data] (-> file (assoc :id (or (:id data) (:id file))) + (cond-> (:options data) (assoc :options (:options data))) (dissoc :data))) (defn- page-attrs diff --git a/backend/src/app/graph/schema/contract.clj b/backend/src/app/graph/schema/contract.clj index 7d2c74aebd..798f4ae460 100644 --- a/backend/src/app/graph/schema/contract.clj +++ b/backend/src/app/graph/schema/contract.clj @@ -107,7 +107,14 @@ ;; derivable from `x`/`y`/`width`/`height`, and a fixed-size array is a ;; tensor row a consumer reads without parsing. "selrect" "DOUBLE[4]" - "svg_viewbox" "DOUBLE[4]" + + ;; The SVG provenance attributes are typed `:map` in the shape schema on + ;; purpose — legacy files hold them as plain maps rather than as `::grc/rect` + ;; and `::gmt/matrix` records, and a tighter *schema* would reject those files + ;; (`app.common.types.shape/schema:shape-generic-attrs`). A tighter *column* + ;; is free: `app.graph.schema.values/coerce` reads either form. + "svg_viewbox" "DOUBLE[4]" + "svg_transform" "DOUBLE[6]" ;; `:fills` is an `:or` — the packed `app.common.types.fills` value or a ;; plain vector of fill maps — so the schema alone cannot say it is a diff --git a/backend/src/app/graph/schema/nodes.clj b/backend/src/app/graph/schema/nodes.clj index b80cb1ae99..2a7ff0c4ae 100644 --- a/backend/src/app/graph/schema/nodes.clj +++ b/backend/src/app/graph/schema/nodes.clj @@ -18,6 +18,7 @@ (:require [app.common.exceptions :as ex] [app.common.schema :as sm] + [app.common.time :as ct] [app.common.types.component :as ctk] [app.common.types.file :as ctf] [app.common.types.page :as ctp] @@ -33,7 +34,24 @@ ;; beadpot/graph/schemas.py drop_fields (def ^:private document-projection {:source ctf/schema:file - :drop [:data]}) + :drop [:data] + ;; Attributes a file map carries that `ctf/schema:file` does not declare. + ;; + ;; They belong here rather than in that schema, even though the graph wants + ;; them, because `schema:file` is on the *write* path too: + ;; `app.binfile.common/update-file!` derives its UPDATE columns from a file + ;; map's keys, so declaring `:backend` there made it try to write a `backend` + ;; column, which the `file` table does not have — it is synthesized on read. + ;; A projection `:extra` is local to the graph and cannot reach a write. + ;; + ;; `:options` is lifted out of `:data` before the blob is dropped + ;; (`app.graph.project.document/document-attrs`); the rest come off the file + ;; map as `get-file` returns it. + :extra [:map + [:options {:optional true} [:maybe :map]] + [:backend {:optional true} [:maybe :string]] + [:comment-thread-seqn {:optional true} [:maybe :int]] + [:ignore-sync-until {:optional true} [:maybe ::ct/inst]]]}) (def ^:private page-projection {:source ctp/schema:page