mirror of
https://github.com/logseq/logseq.git
synced 2026-02-01 22:47:36 +00:00
Move db malli schema into db dep since it's stable
Also add a validate-db task. Part of LOG-2739
This commit is contained in:
7
bb.edn
7
bb.edn
@@ -42,6 +42,13 @@
|
||||
"yarn -s nbb-logseq -cp src -m logseq.tasks.dev.publishing"
|
||||
(into ["static"] *command-line-args*))}
|
||||
|
||||
dev:validate-db
|
||||
{:doc "Validate a DB graph's datascript schema"
|
||||
:requires ([babashka.fs :as fs])
|
||||
:task (apply shell {:dir "deps/db" :extra-env {"ORIGINAL_PWD" (fs/cwd)}}
|
||||
"yarn -s nbb-logseq script/validate_client_db.cljs"
|
||||
*command-line-args*)}
|
||||
|
||||
dev:npx-cap-run-ios
|
||||
logseq.tasks.dev.mobile/npx-cap-run-ios
|
||||
|
||||
|
||||
1
deps/db/.carve/config.edn
vendored
1
deps/db/.carve/config.edn
vendored
@@ -5,6 +5,7 @@
|
||||
logseq.db.sqlite.util
|
||||
logseq.db.sqlite.cli
|
||||
logseq.db.property
|
||||
logseq.db.malli-schema
|
||||
;; Some fns are used by frontend but not worth moving over yet
|
||||
logseq.db.schema]
|
||||
:report {:format :ignore}}
|
||||
|
||||
4
deps/db/nbb.edn
vendored
4
deps/db/nbb.edn
vendored
@@ -1,4 +1,6 @@
|
||||
{:paths ["src"]
|
||||
:deps
|
||||
{io.github.nextjournal/nbb-test-runner
|
||||
{metosin/malli
|
||||
{:mvn/version "0.10.0"}
|
||||
io.github.nextjournal/nbb-test-runner
|
||||
{:git/sha "60ed57aa04bca8d604f5ba6b28848bd887109347"}}}
|
||||
|
||||
133
deps/db/script/validate_client_db.cljs
vendored
Normal file
133
deps/db/script/validate_client_db.cljs
vendored
Normal file
@@ -0,0 +1,133 @@
|
||||
(ns validate-client-db
|
||||
"Script that validates the datascript db of a DB graph"
|
||||
(:require [logseq.db.sqlite.cli :as sqlite-cli]
|
||||
[logseq.db.sqlite.db :as sqlite-db]
|
||||
[logseq.db.schema :as db-schema]
|
||||
[logseq.db.malli-schema :as db-malli-schema]
|
||||
[datascript.core :as d]
|
||||
[clojure.string :as string]
|
||||
[nbb.core :as nbb]
|
||||
[clojure.walk :as walk]
|
||||
[malli.core :as m]
|
||||
[babashka.cli :as cli]
|
||||
["path" :as node-path]
|
||||
["os" :as os]
|
||||
[cljs.pprint :as pprint]))
|
||||
|
||||
(defn- build-grouped-errors [db full-maps errors]
|
||||
(->> errors
|
||||
(group-by #(-> % :in first))
|
||||
(map (fn [[idx errors']]
|
||||
{:entity (cond-> (get full-maps idx)
|
||||
;; Provide additional page info for debugging
|
||||
(:block/page (get full-maps idx))
|
||||
(update :block/page
|
||||
(fn [id] (select-keys (d/entity db id)
|
||||
[:block/name :block/type :db/id :block/created-at]))))
|
||||
;; Group by type to reduce verbosity
|
||||
:errors-by-type
|
||||
(->> (group-by :type errors')
|
||||
(map (fn [[type' type-errors]]
|
||||
[type'
|
||||
{:in-value-distinct (->> type-errors
|
||||
(map #(select-keys % [:in :value]))
|
||||
distinct
|
||||
vec)
|
||||
:schema-distinct (->> (map :schema type-errors)
|
||||
(map m/form)
|
||||
distinct
|
||||
vec)}]))
|
||||
(into {}))}))))
|
||||
|
||||
(defn- update-schema
|
||||
"Updates the db schema to add a datascript db for property validations
|
||||
and to optionally close maps"
|
||||
[db-schema db {:keys [closed-maps]}]
|
||||
(let [db-schema-with-property-vals (db-malli-schema/update-properties-in-schema db-schema db)]
|
||||
(if closed-maps
|
||||
(walk/postwalk (fn [e]
|
||||
(if (and (vector? e)
|
||||
(= :map (first e))
|
||||
(contains? (second e) :closed))
|
||||
(assoc e 1 (assoc (second e) :closed true))
|
||||
e))
|
||||
db-schema-with-property-vals)
|
||||
db-schema-with-property-vals)))
|
||||
|
||||
(defn validate-client-db
|
||||
"Validate datascript db as a vec of entity maps"
|
||||
[db ent-maps* {:keys [verbose group-errors] :as options}]
|
||||
(let [ent-maps (vec (db-malli-schema/update-properties-in-ents (vals ent-maps*)))
|
||||
schema (update-schema db-malli-schema/DB db options)]
|
||||
(if-let [errors (->> ent-maps
|
||||
(m/explain schema)
|
||||
:errors)]
|
||||
(do
|
||||
(if group-errors
|
||||
(let [ent-errors (build-grouped-errors db ent-maps errors)]
|
||||
(println "Found" (count ent-errors) "entities in errors:")
|
||||
(if verbose
|
||||
(pprint/pprint ent-errors)
|
||||
(pprint/pprint (map :entity ent-errors))))
|
||||
(do
|
||||
(println "Found" (count errors) "errors:")
|
||||
(if verbose
|
||||
(pprint/pprint
|
||||
(map #(assoc %
|
||||
:entity (get ent-maps (-> % :in first))
|
||||
:schema (m/form (:schema %)))
|
||||
errors))
|
||||
(pprint/pprint errors))))
|
||||
(js/process.exit 1))
|
||||
(println "Valid!"))))
|
||||
|
||||
(defn- datoms->entity-maps
|
||||
"Returns entity maps for given :eavt datoms"
|
||||
[datoms]
|
||||
(->> datoms
|
||||
(reduce (fn [acc m]
|
||||
(if (contains? db-schema/card-many-attributes (:a m))
|
||||
(update acc (:e m) update (:a m) (fnil conj #{}) (:v m))
|
||||
(update acc (:e m) assoc (:a m) (:v m))))
|
||||
{})))
|
||||
|
||||
(def spec
|
||||
"Options spec"
|
||||
{:help {:alias :h
|
||||
:desc "Print help"}
|
||||
:verbose {:alias :v
|
||||
:desc "Print more info"}
|
||||
:closed-maps {:alias :c
|
||||
:desc "Validate maps marked with closed as :closed"}
|
||||
:group-errors {:alias :g
|
||||
:desc "Groups errors by their entity id"}})
|
||||
|
||||
(defn- validate-graph [graph-dir options]
|
||||
(let [[dir db-name] (if (string/includes? graph-dir "/")
|
||||
(let [graph-dir'
|
||||
(node-path/join (or js/process.env.ORIGINAL_PWD ".") graph-dir)]
|
||||
((juxt node-path/dirname node-path/basename) graph-dir'))
|
||||
[(node-path/join (os/homedir) "logseq" "graphs") graph-dir])
|
||||
_ (try (sqlite-db/open-db! dir db-name)
|
||||
(catch :default e
|
||||
(println "Error: For graph" (str (pr-str graph-dir) ":") (str e))
|
||||
(js/process.exit 1)))
|
||||
conn (sqlite-cli/read-graph db-name)
|
||||
datoms (d/datoms @conn :eavt)
|
||||
ent-maps (datoms->entity-maps datoms)]
|
||||
(println "Read graph" (str db-name " with " (count datoms) " datoms, "
|
||||
(count ent-maps) " entities and "
|
||||
(count (mapcat :block/properties (vals ent-maps))) " properties"))
|
||||
(validate-client-db @conn ent-maps options)))
|
||||
|
||||
(defn -main [argv]
|
||||
(let [{:keys [args opts]} (cli/parse-args argv {:spec spec})
|
||||
_ (when (or (empty? args) (:help opts))
|
||||
(println (str "Usage: $0 GRAPH-NAME [& ADDITIONAL-GRAPHS] [OPTIONS]\nOptions:\n"
|
||||
(cli/format-opts {:spec spec})))
|
||||
(js/process.exit 1))]
|
||||
(doseq [graph-dir args]
|
||||
(validate-graph graph-dir opts))))
|
||||
|
||||
(when (= nbb/*file* (:file (meta #'-main)))
|
||||
(-main *command-line-args*))
|
||||
236
deps/db/src/logseq/db/malli_schema.cljs
vendored
Normal file
236
deps/db/src/logseq/db/malli_schema.cljs
vendored
Normal file
@@ -0,0 +1,236 @@
|
||||
(ns logseq.db.malli-schema
|
||||
"Malli schemas and fns for logseq.db.*"
|
||||
(:require [clojure.walk :as walk]
|
||||
[datascript.core :as d]
|
||||
[logseq.db.property :as db-property]
|
||||
[logseq.db.property.type :as db-property-type]))
|
||||
|
||||
;; Helper fns
|
||||
;; ==========
|
||||
(defn validate-property-value
|
||||
"Validates the value in a property tuple. The property value can be one or
|
||||
many of a value to validated"
|
||||
[prop-type schema-fn val]
|
||||
(if (and (or (sequential? val) (set? val))
|
||||
(not= :coll prop-type))
|
||||
(every? schema-fn val)
|
||||
(schema-fn val)))
|
||||
|
||||
(defn update-properties-in-schema
|
||||
"Needs to be called on the DB schema to add the datascript db to it"
|
||||
[db-schema db]
|
||||
(walk/postwalk (fn [e]
|
||||
(let [meta' (meta e)]
|
||||
(cond
|
||||
(:add-db meta')
|
||||
(partial e db)
|
||||
(:property-value meta')
|
||||
(let [[property-type schema-fn] e
|
||||
schema-fn' (if (db-property-type/property-types-with-db property-type) (partial schema-fn db) schema-fn)
|
||||
validation-fn #(validate-property-value property-type schema-fn' %)]
|
||||
[property-type [:tuple :uuid [:fn validation-fn]]])
|
||||
:else
|
||||
e)))
|
||||
db-schema))
|
||||
|
||||
(defn update-properties-in-ents
|
||||
"Prepares entities to be validated by DB schema"
|
||||
[ents]
|
||||
(map #(if (:block/properties %)
|
||||
(update % :block/properties (fn [x] (mapv identity x)))
|
||||
%)
|
||||
ents))
|
||||
|
||||
;; Malli schemas
|
||||
;; =============
|
||||
;; These schemas should be data vars to remain as simple and reusable as possible
|
||||
(def property-tuple
|
||||
"Represents a tuple of a property and its property value. This schema
|
||||
has 2 metadata hooks which are used to inject a datascript db later"
|
||||
(into
|
||||
[:multi {:dispatch ^:add-db (fn [db property-tuple]
|
||||
(get-in (d/entity db [:block/uuid (first property-tuple)])
|
||||
[:block/schema :type]))}]
|
||||
(map (fn [[prop-type value-schema]]
|
||||
^:property-value [prop-type (if (vector? value-schema) (last value-schema) value-schema)])
|
||||
db-property-type/builtin-schema-types)))
|
||||
|
||||
(def block-properties
|
||||
"Validates a slightly modified verson of :block/properties. Properties are
|
||||
expected to be a vector of tuples instead of a map in order to validate each
|
||||
property with its property value that is valid for its type"
|
||||
[:sequential property-tuple])
|
||||
|
||||
(def page-or-block-attrs
|
||||
"Common attributes for page and normal blocks"
|
||||
[[:block/uuid :uuid]
|
||||
[:block/created-at :int]
|
||||
[:block/updated-at :int]
|
||||
[:block/properties {:optional true}
|
||||
block-properties]
|
||||
[:block/refs {:optional true} [:set :int]]
|
||||
[:block/tags {:optional true} [:set :int]]
|
||||
[:block/tx-id {:optional true} :int]])
|
||||
|
||||
(def page-attrs
|
||||
"Common attributes for pages"
|
||||
[[:block/name :string]
|
||||
[:block/original-name :string]
|
||||
[:block/type {:optional true} [:enum #{"property"} #{"class"} #{"object"} #{"whiteboard"} #{"hidden"}]]
|
||||
[:block/journal? :boolean]
|
||||
;; TODO: Consider moving to just normal and class after figuring out journal attributes
|
||||
[:block/format {:optional true} [:enum :markdown]]
|
||||
;; TODO: Should this be here or in common?
|
||||
[:block/path-refs {:optional true} [:set :int]]])
|
||||
|
||||
(def normal-page
|
||||
(vec
|
||||
(concat
|
||||
[:map {:closed false}]
|
||||
page-attrs
|
||||
;; journal-day is only set for journal pages
|
||||
[[:block/journal-day {:optional true} :int]
|
||||
[:block/namespace {:optional true} :int]]
|
||||
page-or-block-attrs)))
|
||||
|
||||
(def object-page
|
||||
(vec
|
||||
(concat
|
||||
[:map {:closed false}]
|
||||
[[:block/collapsed? {:optional true} :boolean]
|
||||
[:block/tags [:set :int]]]
|
||||
page-attrs
|
||||
(remove #(= :block/tags (first %)) page-or-block-attrs))))
|
||||
|
||||
(def class-page
|
||||
(vec
|
||||
(concat
|
||||
[:map {:closed false}]
|
||||
[[:block/namespace {:optional true} :int]
|
||||
;; TODO: Require :block/schema
|
||||
[:block/schema
|
||||
{:optional true}
|
||||
[:map
|
||||
{:closed false}
|
||||
[:properties {:optional true} [:vector :uuid]]]]]
|
||||
page-attrs
|
||||
page-or-block-attrs)))
|
||||
|
||||
(def internal-property
|
||||
(vec
|
||||
(concat
|
||||
[:map {:closed false}]
|
||||
[[:block/schema
|
||||
[:map
|
||||
{:closed false}
|
||||
[:type (apply vector :enum (into db-property-type/internal-builtin-schema-types
|
||||
db-property-type/user-builtin-schema-types))]
|
||||
[:hide? {:optional true} :boolean]
|
||||
[:cardinality {:optional true} [:enum :one :many]]]]]
|
||||
page-attrs
|
||||
page-or-block-attrs)))
|
||||
|
||||
(def user-property
|
||||
(vec
|
||||
(concat
|
||||
[:map {:closed false}]
|
||||
[[:block/schema
|
||||
[:map
|
||||
{:closed false}
|
||||
[:type (apply vector :enum db-property-type/user-builtin-schema-types)]
|
||||
[:hide? {:optional true} :boolean]
|
||||
[:description {:optional true} :string]
|
||||
;; For any types except for :checkbox :default :template :enum
|
||||
[:cardinality {:optional true} [:enum :one :many]]
|
||||
;; Just for :enum type
|
||||
[:enum-config {:optional true} :map]
|
||||
;; :template uses :sequential and :page uses :set.
|
||||
;; Should :template should use :set?
|
||||
[:classes {:optional true} [:or
|
||||
[:set :uuid]
|
||||
[:sequential :uuid]]]]]]
|
||||
page-attrs
|
||||
page-or-block-attrs)))
|
||||
|
||||
(def property-page
|
||||
[:multi {:dispatch
|
||||
(fn [m] (contains? db-property/built-in-properties-keys-str (:block/name m)))}
|
||||
[true internal-property]
|
||||
[:malli.core/default user-property]])
|
||||
|
||||
(def page
|
||||
[:multi {:dispatch :block/type}
|
||||
[#{"property"} property-page]
|
||||
[#{"class"} class-page]
|
||||
[#{"object"} object-page]
|
||||
[:malli.core/default normal-page]])
|
||||
|
||||
(def block-attrs
|
||||
"Common attributes for normal blocks"
|
||||
[[:block/content :string]
|
||||
[:block/left :int]
|
||||
[:block/parent :int]
|
||||
[:block/metadata {:optional true}
|
||||
[:map {:closed false}
|
||||
[:created-from-block :uuid]
|
||||
[:created-from-property :uuid]
|
||||
[:created-from-template {:optional true} :uuid]]]
|
||||
;; refs
|
||||
[:block/page :int]
|
||||
[:block/path-refs {:optional true} [:set :int]]
|
||||
[:block/link {:optional true} :int]
|
||||
;; other
|
||||
[:block/format [:enum :markdown]]
|
||||
[:block/marker {:optional true} :string]
|
||||
[:block/priority {:optional true} :string]
|
||||
[:block/collapsed? {:optional true} :boolean]])
|
||||
|
||||
(def object-block
|
||||
"A normal block with tags"
|
||||
(vec
|
||||
(concat
|
||||
[:map {:closed false}]
|
||||
[[:block/type [:= #{"object"}]]
|
||||
[:block/tags [:set :int]]]
|
||||
block-attrs
|
||||
(remove #(= :block/tags (first %)) page-or-block-attrs))))
|
||||
|
||||
(def normal-block
|
||||
"A block with content and no special type or tag behavior"
|
||||
(vec
|
||||
(concat
|
||||
[:map {:closed false}]
|
||||
block-attrs
|
||||
page-or-block-attrs)))
|
||||
|
||||
(def block
|
||||
"A block has content and a page"
|
||||
[:or
|
||||
normal-block
|
||||
object-block])
|
||||
|
||||
;; TODO: Figure out where this is coming from
|
||||
(def unknown-empty-block
|
||||
[:map {:closed true}
|
||||
[:block/uuid :uuid]])
|
||||
|
||||
(def file-block
|
||||
[:map {:closed true}
|
||||
[:block/uuid :uuid]
|
||||
[:block/tx-id {:optional true} :int]
|
||||
[:file/content :string]
|
||||
[:file/path :string]
|
||||
;; TODO: Remove when bug is fixed
|
||||
[:file/last-modified-at {:optional true} :any]])
|
||||
|
||||
(def DB
|
||||
"Malli schema for entities from schema/schema-for-db-based-graph. In order to
|
||||
thoroughly validate properties, the entities and this schema should be
|
||||
prepared with update-properties-in-ents and update-properties-in-schema
|
||||
respectively"
|
||||
[:sequential
|
||||
[:or
|
||||
page
|
||||
block
|
||||
file-block
|
||||
unknown-empty-block]])
|
||||
2
deps/db/src/logseq/db/schema.cljs
vendored
2
deps/db/src/logseq/db/schema.cljs
vendored
@@ -1,5 +1,5 @@
|
||||
(ns logseq.db.schema
|
||||
"Main db schemas for the Logseq app"
|
||||
"Main datascript schemas for the Logseq app"
|
||||
(:require [clojure.set :as set]))
|
||||
|
||||
(defonce version 2)
|
||||
|
||||
@@ -301,6 +301,19 @@ point out:
|
||||
bb dev:validate-repo-config-edn deps/common/resources/templates/config.edn
|
||||
```
|
||||
|
||||
* `dev:validate-db` - Validates a DB graph's datascript schema
|
||||
|
||||
```sh
|
||||
# One time setup
|
||||
$ cd deps/db && yarn install && cd -
|
||||
# One or more graphs can be validated e.g.
|
||||
$ bb dev:validate-db test-db schema -c -g
|
||||
Read graph test-db with 1572 datoms, 220 entities and 13 properties
|
||||
Valid!
|
||||
Read graph schema with 26105 datoms, 2320 entities and 3168 properties
|
||||
Valid!
|
||||
```
|
||||
|
||||
* `dev:publishing` - Build a publishing app for a given graph dir. If the
|
||||
publishing frontend is out of date, it builds that first which takes time.
|
||||
Subsequent runs are quick.
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{:paths ["src"]
|
||||
:deps
|
||||
{metosin/malli
|
||||
{:mvn/version "0.10.0"}
|
||||
logseq/graph-parser
|
||||
{logseq/graph-parser
|
||||
{:local/root "../deps/graph-parser"}
|
||||
logseq/outliner
|
||||
{:local/root "../deps/outliner"}
|
||||
|
||||
@@ -1,345 +0,0 @@
|
||||
(ns logseq.tasks.db-graph.validate-client-db
|
||||
"Script that validates the datascript db of a db graph"
|
||||
(:require [logseq.db.sqlite.cli :as sqlite-cli]
|
||||
[logseq.db.sqlite.db :as sqlite-db]
|
||||
[logseq.db.schema :as db-schema]
|
||||
[logseq.db.property :as db-property]
|
||||
[logseq.db.property.type :as db-property-type]
|
||||
[datascript.core :as d]
|
||||
[clojure.string :as string]
|
||||
[nbb.core :as nbb]
|
||||
[clojure.pprint :as pprint]
|
||||
[clojure.walk :as walk]
|
||||
[malli.core :as m]
|
||||
[babashka.cli :as cli]
|
||||
["path" :as node-path]
|
||||
["os" :as os]
|
||||
[cljs.pprint :as pprint]))
|
||||
|
||||
(defn- validate-property-value
|
||||
"Validates the value in a property tuple. The property value can be one or
|
||||
many of a value to validated"
|
||||
[prop-type schema-fn val]
|
||||
(if (and (or (sequential? val) (set? val))
|
||||
(not= :coll prop-type))
|
||||
(every? schema-fn val)
|
||||
(schema-fn val)))
|
||||
|
||||
(def property-tuple
|
||||
"Represents a tuple of a property and its property value. This schema
|
||||
has 2 metadata hooks which are used to inject a datascript db later"
|
||||
(into
|
||||
[:multi {:dispatch ^:add-db (fn [db property-tuple]
|
||||
(get-in (d/entity db [:block/uuid (first property-tuple)])
|
||||
[:block/schema :type]))}]
|
||||
(map (fn [[prop-type value-schema]]
|
||||
^:property-value [prop-type (if (vector? value-schema) (last value-schema) value-schema)])
|
||||
db-property-type/builtin-schema-types)))
|
||||
|
||||
(def block-properties
|
||||
"Validates a slightly modified verson of :block/properties. Properties are
|
||||
expected to be a vector of tuples instead of a map in order to validate each
|
||||
property with its property value that is valid for its type"
|
||||
[:sequential property-tuple])
|
||||
|
||||
(def page-or-block-attrs
|
||||
"Common attributes for page and normal blocks"
|
||||
[[:block/uuid :uuid]
|
||||
[:block/created-at :int]
|
||||
[:block/updated-at :int]
|
||||
[:block/properties {:optional true}
|
||||
block-properties]
|
||||
[:block/refs {:optional true} [:set :int]]
|
||||
[:block/tags {:optional true} [:set :int]]
|
||||
[:block/tx-id {:optional true} :int]])
|
||||
|
||||
(def page-attrs
|
||||
"Common attributes for pages"
|
||||
[[:block/name :string]
|
||||
[:block/original-name :string]
|
||||
[:block/type {:optional true} [:enum #{"property"} #{"class"} #{"object"} #{"whiteboard"} #{"hidden"}]]
|
||||
[:block/journal? :boolean]
|
||||
;; TODO: Consider moving to just normal and class after figuring out journal attributes
|
||||
[:block/format {:optional true} [:enum :markdown]]
|
||||
;; TODO: Should this be here or in common?
|
||||
[:block/path-refs {:optional true} [:set :int]]])
|
||||
|
||||
(def normal-page
|
||||
(vec
|
||||
(concat
|
||||
[:map {:closed false}]
|
||||
page-attrs
|
||||
;; journal-day is only set for journal pages
|
||||
[[:block/journal-day {:optional true} :int]
|
||||
[:block/namespace {:optional true} :int]]
|
||||
page-or-block-attrs)))
|
||||
|
||||
(def object-page
|
||||
(vec
|
||||
(concat
|
||||
[:map {:closed false}]
|
||||
[[:block/collapsed? {:optional true} :boolean]
|
||||
[:block/tags [:set :int]]]
|
||||
page-attrs
|
||||
(remove #(= :block/tags (first %)) page-or-block-attrs))))
|
||||
|
||||
(def class-page
|
||||
(vec
|
||||
(concat
|
||||
[:map {:closed false}]
|
||||
[[:block/namespace {:optional true} :int]
|
||||
;; TODO: Require :block/schema
|
||||
[:block/schema
|
||||
{:optional true}
|
||||
[:map
|
||||
{:closed false}
|
||||
[:properties {:optional true} [:vector :uuid]]]]]
|
||||
page-attrs
|
||||
page-or-block-attrs)))
|
||||
|
||||
(def internal-property
|
||||
(vec
|
||||
(concat
|
||||
[:map {:closed false}]
|
||||
[[:block/schema
|
||||
[:map
|
||||
{:closed false}
|
||||
[:type (apply vector :enum (into db-property-type/internal-builtin-schema-types
|
||||
db-property-type/user-builtin-schema-types))]
|
||||
[:hide? {:optional true} :boolean]
|
||||
[:cardinality {:optional true} [:enum :one :many]]]]]
|
||||
page-attrs
|
||||
page-or-block-attrs)))
|
||||
|
||||
(def user-property
|
||||
(vec
|
||||
(concat
|
||||
[:map {:closed false}]
|
||||
[[:block/schema
|
||||
[:map
|
||||
{:closed false}
|
||||
[:type (apply vector :enum db-property-type/user-builtin-schema-types)]
|
||||
[:hide? {:optional true} :boolean]
|
||||
[:description {:optional true} :string]
|
||||
;; For any types except for :checkbox :default :template :enum
|
||||
[:cardinality {:optional true} [:enum :one :many]]
|
||||
;; Just for :enum type
|
||||
[:enum-config {:optional true} :map]
|
||||
;; :template uses :sequential and :page uses :set.
|
||||
;; Should :template should use :set?
|
||||
[:classes {:optional true} [:or
|
||||
[:set :uuid]
|
||||
[:sequential :uuid]]]]]]
|
||||
page-attrs
|
||||
page-or-block-attrs)))
|
||||
|
||||
(def property-page
|
||||
[:multi {:dispatch
|
||||
(fn [m] (contains? db-property/built-in-properties-keys-str (:block/name m)))}
|
||||
[true internal-property]
|
||||
[::m/default user-property]])
|
||||
|
||||
(def page
|
||||
[:multi {:dispatch :block/type}
|
||||
[#{"property"} property-page]
|
||||
[#{"class"} class-page]
|
||||
[#{"object"} object-page]
|
||||
[::m/default normal-page]])
|
||||
|
||||
(def block-attrs
|
||||
"Common attributes for normal blocks"
|
||||
[[:block/content :string]
|
||||
[:block/left :int]
|
||||
[:block/parent :int]
|
||||
[:block/metadata {:optional true}
|
||||
[:map {:closed false}
|
||||
[:created-from-block :uuid]
|
||||
[:created-from-property :uuid]
|
||||
[:created-from-template {:optional true} :uuid]]]
|
||||
;; refs
|
||||
[:block/page :int]
|
||||
[:block/path-refs {:optional true} [:set :int]]
|
||||
[:block/link {:optional true} :int]
|
||||
;; other
|
||||
[:block/format [:enum :markdown]]
|
||||
[:block/marker {:optional true} :string]
|
||||
[:block/priority {:optional true} :string]
|
||||
[:block/collapsed? {:optional true} :boolean]])
|
||||
|
||||
(def object-block
|
||||
"A normal block with tags"
|
||||
(vec
|
||||
(concat
|
||||
[:map {:closed false}]
|
||||
[[:block/type [:= #{"object"}]]
|
||||
[:block/tags [:set :int]]]
|
||||
block-attrs
|
||||
(remove #(= :block/tags (first %)) page-or-block-attrs))))
|
||||
|
||||
(def normal-block
|
||||
"A block with content and no special type or tag behavior"
|
||||
(vec
|
||||
(concat
|
||||
[:map {:closed false}]
|
||||
block-attrs
|
||||
page-or-block-attrs)))
|
||||
|
||||
(def block
|
||||
"A block has content and a page"
|
||||
[:or
|
||||
normal-block
|
||||
object-block])
|
||||
|
||||
;; TODO: Figure out where this is coming from
|
||||
(def unknown-empty-block
|
||||
[:map {:closed true}
|
||||
[:block/uuid :uuid]])
|
||||
|
||||
(def file-block
|
||||
[:map {:closed true}
|
||||
[:block/uuid :uuid]
|
||||
[:block/tx-id {:optional true} :int]
|
||||
[:file/content :string]
|
||||
[:file/path :string]
|
||||
;; TODO: Remove when bug is fixed
|
||||
[:file/last-modified-at {:optional true} :any]])
|
||||
|
||||
(def client-db-schema
|
||||
[:sequential
|
||||
[:or
|
||||
page
|
||||
block
|
||||
file-block
|
||||
unknown-empty-block]])
|
||||
|
||||
(defn- build-grouped-errors [db full-maps errors]
|
||||
(->> errors
|
||||
(group-by #(-> % :in first))
|
||||
(map (fn [[idx errors']]
|
||||
{:entity (cond-> (get full-maps idx)
|
||||
;; Provide additional page info for debugging
|
||||
(:block/page (get full-maps idx))
|
||||
(update :block/page
|
||||
(fn [id] (select-keys (d/entity db id)
|
||||
[:block/name :block/type :db/id :block/created-at]))))
|
||||
;; Group by type to reduce verbosity
|
||||
:errors-by-type
|
||||
(->> (group-by :type errors')
|
||||
(map (fn [[type' type-errors]]
|
||||
[type'
|
||||
{:in-value-distinct (->> type-errors
|
||||
(map #(select-keys % [:in :value]))
|
||||
distinct
|
||||
vec)
|
||||
:schema-distinct (->> (map :schema type-errors)
|
||||
(map m/form)
|
||||
distinct
|
||||
vec)}]))
|
||||
(into {}))}))))
|
||||
|
||||
(defn- update-schema
|
||||
"Updates the db schema to add a datascript db for property validations
|
||||
and to optionally close maps"
|
||||
[db-schema db {:keys [closed-maps]}]
|
||||
(let [db-schema-with-property-vals
|
||||
(walk/postwalk (fn [e]
|
||||
(let [meta' (meta e)]
|
||||
(cond
|
||||
(:add-db meta')
|
||||
(partial e db)
|
||||
(:property-value meta')
|
||||
(let [[property-type schema-fn] e
|
||||
schema-fn' (if (db-property-type/property-types-with-db property-type) (partial schema-fn db) schema-fn)
|
||||
validation-fn #(validate-property-value property-type schema-fn' %)]
|
||||
[property-type [:tuple :uuid [:fn validation-fn]]])
|
||||
:else
|
||||
e)))
|
||||
db-schema)]
|
||||
(if closed-maps
|
||||
(walk/postwalk (fn [e]
|
||||
(if (and (vector? e)
|
||||
(= :map (first e))
|
||||
(contains? (second e) :closed))
|
||||
(assoc e 1 (assoc (second e) :closed true))
|
||||
e))
|
||||
db-schema-with-property-vals)
|
||||
db-schema-with-property-vals)))
|
||||
|
||||
(defn validate-client-db
|
||||
"Validate datascript db as a vec of entity maps"
|
||||
[db ent-maps* {:keys [verbose group-errors] :as options}]
|
||||
(let [ent-maps (vec (map #(if (:block/properties %)
|
||||
(update % :block/properties (fn [x] (mapv identity x)))
|
||||
%)
|
||||
(vals ent-maps*)))
|
||||
schema (update-schema client-db-schema db options)]
|
||||
(if-let [errors (->> ent-maps
|
||||
(m/explain schema)
|
||||
:errors)]
|
||||
(do
|
||||
(if group-errors
|
||||
(let [ent-errors (build-grouped-errors db ent-maps errors)]
|
||||
(println "Found" (count ent-errors) "entities in errors:")
|
||||
(if verbose
|
||||
(pprint/pprint ent-errors)
|
||||
(pprint/pprint (map :entity ent-errors))))
|
||||
(do
|
||||
(println "Found" (count errors) "errors:")
|
||||
(if verbose
|
||||
(pprint/pprint
|
||||
(map #(assoc %
|
||||
:entity (get ent-maps (-> % :in first))
|
||||
:schema (m/form (:schema %)))
|
||||
errors))
|
||||
(pprint/pprint errors))))
|
||||
(js/process.exit 1))
|
||||
(println "Valid!"))))
|
||||
|
||||
(defn- datoms->entity-maps
|
||||
"Returns entity maps for given :eavt datoms"
|
||||
[datoms]
|
||||
(->> datoms
|
||||
(reduce (fn [acc m]
|
||||
(if (contains? db-schema/card-many-attributes (:a m))
|
||||
(update acc (:e m) update (:a m) (fnil conj #{}) (:v m))
|
||||
(update acc (:e m) assoc (:a m) (:v m))))
|
||||
{})))
|
||||
|
||||
(def spec
|
||||
"Options spec"
|
||||
{:help {:alias :h
|
||||
:desc "Print help"}
|
||||
:verbose {:alias :v
|
||||
:desc "Print more info"}
|
||||
:closed-maps {:alias :c
|
||||
:desc "Validate maps marked with closed as :closed"}
|
||||
:group-errors {:alias :g
|
||||
:desc "Groups errors by their entity id"}})
|
||||
|
||||
(defn- validate-graph [graph-dir options]
|
||||
(let [[dir db-name] (if (string/includes? graph-dir "/")
|
||||
((juxt node-path/dirname node-path/basename) graph-dir)
|
||||
[(node-path/join (os/homedir) "logseq" "graphs") graph-dir])
|
||||
_ (try (sqlite-db/open-db! dir db-name)
|
||||
(catch :default e
|
||||
(println "Error: For graph" (str (pr-str graph-dir) ":") (str e))
|
||||
(js/process.exit 1)))
|
||||
conn (sqlite-cli/read-graph db-name)
|
||||
datoms (d/datoms @conn :eavt)
|
||||
ent-maps (datoms->entity-maps datoms)]
|
||||
(println "Read graph" (str db-name " with " (count datoms) " datoms, "
|
||||
(count ent-maps) " entities and "
|
||||
(count (mapcat :block/properties (vals ent-maps))) " properties"))
|
||||
(validate-client-db @conn ent-maps options)))
|
||||
|
||||
(defn -main [argv]
|
||||
(let [{:keys [args opts]} (cli/parse-args argv {:spec spec})
|
||||
_ (when (or (empty? args) (:help opts))
|
||||
(println (str "Usage: $0 GRAPH-NAME [& ADDITIONAL-GRAPHS] [OPTIONS]\nOptions:\n"
|
||||
(cli/format-opts {:spec spec})))
|
||||
(js/process.exit 1))]
|
||||
(doseq [graph-dir args]
|
||||
(validate-graph graph-dir opts))))
|
||||
|
||||
(when (= nbb/*file* (:file (meta #'-main)))
|
||||
(-main *command-line-args*))
|
||||
Reference in New Issue
Block a user