mirror of
https://github.com/penpot/penpot.git
synced 2026-08-28 23:59:00 +00:00
✨ Fill unset graph columns from the consumer's field defaults
A Penpot file omits an attribute whose value equals its default, so a shape that is not blocked carries no `blocked` key at all. Projected as it stands, that column reads NULL, and a reader cannot tell a missing feature from a false one. `app.graph.schema.beadpot` reads the schema manifest the parity consumer exports, checked in at `backend/resources/app/graph/beadpot-schema.json`, and `nodes/apply-defaults` fills every unset column that the manifest gives a default for. The default is withheld when the column's Ladybug type differs from the manifest's, because a default is expressed in its column's type. Applied after validation: a default belongs to the graph column, not to the Penpot schema the attributes were checked against. This stays off `graph-backend`. Penpot should declare these defaults from its own model rather than import another project's manifest, which is W24 on the workplan. AI-assisted-by: mixed models
This commit is contained in:
parent
9432c61637
commit
13229dd94e
7943
backend/resources/app/graph/beadpot-schema.json
Normal file
7943
backend/resources/app/graph/beadpot-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
136
backend/src/app/graph/schema/beadpot.clj
Normal file
136
backend/src/app/graph/schema/beadpot.clj
Normal file
@ -0,0 +1,136 @@
|
||||
;; 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.schema.beadpot
|
||||
"beadpot's graph schema, as a checked-in value.
|
||||
|
||||
`resources/app/graph/beadpot-schema.json` is produced by
|
||||
`bp graph schema export` and read back off an empty database beadpot itself
|
||||
creates, so it states what beadpot *does*: table names, column names in
|
||||
order, Ladybug types, the legal (from, to) pairs of each relationship family,
|
||||
and each column's default.
|
||||
|
||||
This namespace does not generate the DDL — that stays derived from Penpot's
|
||||
own Malli schemas (`app.graph.schema.nodes`), so the backend remains
|
||||
self-contained and a change to the design model shows up here as a diff to
|
||||
review rather than as a silent schema change. What the manifest *is* used for
|
||||
is the things Penpot cannot know on its own:
|
||||
|
||||
- **defaults.** beadpot writes a Pydantic field default where the file has no
|
||||
such attribute, so a shape with no `blocked` key holds `false`, not NULL.
|
||||
A consumer told NULL sees a missing feature where beadpot showed it a false
|
||||
one. The defaults live in beadpot's models; reading them from here keeps
|
||||
them from being restated in Clojure and drifting.
|
||||
- **review.** `backend_tests.graph-contract-test` diffs the DDL this backend
|
||||
emits against the manifest, so a divergence is a failing test naming the
|
||||
column, not something a training set discovers later.
|
||||
|
||||
Regenerate with `bp graph schema export -o
|
||||
backend/resources/app/graph/beadpot-schema.json` after any change to
|
||||
beadpot's node or edge models."
|
||||
(:require
|
||||
[app.common.json :as json]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.string :as str]))
|
||||
|
||||
(def ^:private resource-path
|
||||
"app/graph/beadpot-schema.json")
|
||||
|
||||
(defn- read-manifest
|
||||
[]
|
||||
(if-let [resource (io/resource resource-path)]
|
||||
(with-open [reader (io/reader resource)]
|
||||
(json/read reader :key-fn keyword))
|
||||
(throw (ex-info "beadpot schema manifest missing from resources"
|
||||
{:path resource-path}))))
|
||||
|
||||
(def manifest
|
||||
"The parsed manifest. Delayed so a missing resource fails where it is used."
|
||||
(delay (read-manifest)))
|
||||
|
||||
(defn- index-tables
|
||||
[tables]
|
||||
(into {} (map (juxt :table identity)) tables))
|
||||
|
||||
(def node-tables
|
||||
(delay (index-tables (:node_tables @manifest))))
|
||||
|
||||
(def rel-tables
|
||||
(delay (index-tables (:rel_tables @manifest))))
|
||||
|
||||
(defn table
|
||||
"The manifest entry for `table-name`, node or rel."
|
||||
[table-name]
|
||||
(or (get @node-tables table-name)
|
||||
(get @rel-tables table-name)))
|
||||
|
||||
(defn columns
|
||||
"Column entries for `table-name`, in beadpot's order."
|
||||
[table-name]
|
||||
(:columns (table table-name) []))
|
||||
|
||||
(defn column
|
||||
"The manifest entry for one column, or nil."
|
||||
[table-name column-name]
|
||||
(some #(when (= column-name (:name %)) %) (columns table-name)))
|
||||
|
||||
(def defaults
|
||||
"`table -> {column-name -> default}`, omitting columns with no default.
|
||||
|
||||
A JSON `null` means \"no default\": beadpot leaves the column NULL when the
|
||||
attribute is unset, and so should we."
|
||||
(delay
|
||||
(into {}
|
||||
(map (fn [[table-name entry]]
|
||||
[table-name
|
||||
(into {}
|
||||
(keep (fn [{:keys [name default]}]
|
||||
(when (some? default) [name default])))
|
||||
(:columns entry))]))
|
||||
(merge @node-tables @rel-tables))))
|
||||
|
||||
(defn column-default
|
||||
"beadpot's default for `column-name` on `table-name`, or nil."
|
||||
[table-name column-name]
|
||||
(get-in @defaults [table-name column-name]))
|
||||
|
||||
(def ^:private type-aliases
|
||||
"Declared type spelling → the spelling Ladybug's catalog reports.
|
||||
|
||||
The manifest is read back off a real database, so it holds catalog spellings;
|
||||
DDL is written in whatever Ladybug accepts. `BOOLEAN` is accepted and comes
|
||||
back as `BOOL`, so a literal comparison would decide the two sides disagree
|
||||
about every boolean column."
|
||||
{"BOOLEAN" "BOOL"})
|
||||
|
||||
(defn normalize-type
|
||||
"A Ladybug type string in the spelling the catalog uses.
|
||||
|
||||
Replaces aliases anywhere in the string, so a `BOOLEAN` nested inside a
|
||||
`STRUCT(...)` normalizes too."
|
||||
[ladybug-type]
|
||||
(when ladybug-type
|
||||
(-> (reduce-kv (fn [s declared reported] (str/replace s declared reported))
|
||||
ladybug-type
|
||||
type-aliases)
|
||||
;; STRUCT field names are written backticked (a grid cell has a field
|
||||
;; called `column`); the catalog reports them bare.
|
||||
(str/replace "`" ""))))
|
||||
|
||||
(defn typed-column-default
|
||||
"beadpot's default for a column, but only when the types agree.
|
||||
|
||||
A default is expressed in the column's type — `[]` for a `DOUBLE[2][]`,
|
||||
`{}` for a `JSON`, `[1,0,0,1,0,0]` for a `DOUBLE[6]` transform. Where this
|
||||
backend still emits a different type for that column (`app.graph.schema.types`
|
||||
is coarser than beadpot's encodings in a handful of places, tracked by
|
||||
`bp graph schema diff`), the default would be written in a shape the column
|
||||
cannot hold. So it is withheld until the types match, and the set of columns
|
||||
that get defaults grows as the types converge."
|
||||
[table-name column-name ladybug-type]
|
||||
(when (= (normalize-type ladybug-type)
|
||||
(normalize-type (:type (column table-name column-name))))
|
||||
(column-default table-name column-name)))
|
||||
@ -23,6 +23,7 @@
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.page :as ctp]
|
||||
[app.graph.ladybug :as ladybug]
|
||||
[app.graph.schema.beadpot :as beadpot]
|
||||
[app.graph.schema.contract :as contract]
|
||||
[app.graph.schema.projection :as projection]
|
||||
[app.graph.schema.types :as types]
|
||||
@ -232,6 +233,30 @@
|
||||
"; columns=" (count (column-keys table))
|
||||
" shape-keys=" (vec (keys attrs)))))
|
||||
|
||||
(defn- apply-defaults
|
||||
"Fill columns the document does not set with the consumer's field defaults.
|
||||
|
||||
A file omits an attribute equal to its default and the consumer's models
|
||||
restore it, so a shape with no `blocked` key holds `false` in the graph the
|
||||
consumer builds. Writing NULL instead hands a reader a *missing* feature
|
||||
where the consumer hands it a false one, so the defaults are read from the
|
||||
exported manifest (`app.graph.schema.beadpot`) rather than restated here,
|
||||
where they would drift.
|
||||
|
||||
Applied after validation: a default belongs to the graph column, not to the
|
||||
Penpot schema the attrs were checked against."
|
||||
[table attrs]
|
||||
(reduce (fn [attrs k]
|
||||
(if (contains? attrs k)
|
||||
attrs
|
||||
(let [column (column-name table k)]
|
||||
(if-some [default (beadpot/typed-column-default
|
||||
table column (column-ladybug-type table k))]
|
||||
(assoc attrs k default)
|
||||
attrs))))
|
||||
attrs
|
||||
(column-keys table)))
|
||||
|
||||
(defn project-attrs
|
||||
"Select and validate the projected columns for `table` from `attrs`."
|
||||
[table attrs]
|
||||
@ -245,7 +270,7 @@
|
||||
(column-keys table)))]
|
||||
(when (empty? projected)
|
||||
(raise-empty-projection! table attrs))
|
||||
(validate-node table projected)))
|
||||
(apply-defaults table (validate-node table projected))))
|
||||
|
||||
(defn match-label
|
||||
"Cypher node label for MATCH; backtick-wrapped when required by Ladybug."
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user