validate_client_db.cljs 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. (ns validate-client-db
  2. "Script that validates the datascript db of a DB graph
  3. NOTE: This script is also used in CI to confirm our db's schema is up to date"
  4. (:require [logseq.db.sqlite.cli :as sqlite-cli]
  5. [logseq.db.sqlite.db :as sqlite-db]
  6. [logseq.db.frontend.schema :as db-schema]
  7. [logseq.db.frontend.malli-schema :as db-malli-schema]
  8. [datascript.core :as d]
  9. [clojure.string :as string]
  10. [nbb.core :as nbb]
  11. [clojure.walk :as walk]
  12. [malli.core :as m]
  13. [babashka.cli :as cli]
  14. ["path" :as node-path]
  15. ["os" :as os]
  16. [cljs.pprint :as pprint]))
  17. (defn- build-grouped-errors [db full-maps errors]
  18. (->> errors
  19. (group-by #(-> % :in first))
  20. (map (fn [[idx errors']]
  21. {:entity (cond-> (get full-maps idx)
  22. ;; Provide additional page info for debugging
  23. (:block/page (get full-maps idx))
  24. (update :block/page
  25. (fn [id] (select-keys (d/entity db id)
  26. [:block/name :block/type :db/id :block/created-at]))))
  27. ;; Group by type to reduce verbosity
  28. :errors-by-type
  29. (->> (group-by :type errors')
  30. (map (fn [[type' type-errors]]
  31. [type'
  32. {:in-value-distinct (->> type-errors
  33. (map #(select-keys % [:in :value]))
  34. distinct
  35. vec)
  36. :schema-distinct (->> (map :schema type-errors)
  37. (map m/form)
  38. distinct
  39. vec)}]))
  40. (into {}))}))))
  41. (defn- update-schema
  42. "Updates the db schema to add a datascript db for property validations
  43. and to optionally close maps"
  44. [db-schema db {:keys [closed-maps]}]
  45. (let [db-schema-with-property-vals (db-malli-schema/update-properties-in-schema db-schema db)]
  46. (if closed-maps
  47. ;; closes maps that don't have an explicit :closed option
  48. (walk/postwalk (fn [e]
  49. (if (and (vector? e)
  50. (= :map (first e)))
  51. (if (map? (second e))
  52. (if (not (contains? (second e) :closed))
  53. (assoc e 1 (assoc (second e) :closed true))
  54. e)
  55. (into [:map {:closed true}] (rest e)))
  56. e))
  57. db-schema-with-property-vals)
  58. db-schema-with-property-vals)))
  59. (defn validate-client-db
  60. "Validate datascript db as a vec of entity maps"
  61. [db ent-maps* {:keys [verbose group-errors] :as options}]
  62. (let [ent-maps (vec (db-malli-schema/update-properties-in-ents (vals ent-maps*)))
  63. schema (update-schema db-malli-schema/DB db options)]
  64. (if-let [errors (->> ent-maps
  65. (m/explain schema)
  66. :errors)]
  67. (do
  68. (if group-errors
  69. (let [ent-errors (build-grouped-errors db ent-maps errors)]
  70. (println "Found" (count ent-errors) "entities in errors:")
  71. (if verbose
  72. (pprint/pprint ent-errors)
  73. (pprint/pprint (map :entity ent-errors))))
  74. (do
  75. (println "Found" (count errors) "errors:")
  76. (if verbose
  77. (pprint/pprint
  78. (map #(assoc %
  79. :entity (get ent-maps (-> % :in first))
  80. :schema (m/form (:schema %)))
  81. errors))
  82. (pprint/pprint errors))))
  83. (js/process.exit 1))
  84. (println "Valid!"))))
  85. (defn- datoms->entity-maps
  86. "Returns entity maps for given :eavt datoms"
  87. [datoms]
  88. (->> datoms
  89. (reduce (fn [acc m]
  90. (if (contains? db-schema/card-many-attributes (:a m))
  91. (update acc (:e m) update (:a m) (fnil conj #{}) (:v m))
  92. (update acc (:e m) assoc (:a m) (:v m))))
  93. {})))
  94. (def spec
  95. "Options spec"
  96. {:help {:alias :h
  97. :desc "Print help"}
  98. :verbose {:alias :v
  99. :desc "Print more info"}
  100. :closed-maps {:alias :c
  101. :desc "Validate maps marked with closed as :closed"}
  102. :group-errors {:alias :g
  103. :desc "Groups errors by their entity id"}})
  104. (defn- validate-graph [graph-dir options]
  105. (let [[dir db-name] (if (string/includes? graph-dir "/")
  106. (let [graph-dir'
  107. (node-path/join (or js/process.env.ORIGINAL_PWD ".") graph-dir)]
  108. ((juxt node-path/dirname node-path/basename) graph-dir'))
  109. [(node-path/join (os/homedir) "logseq" "graphs") graph-dir])
  110. _ (try (sqlite-db/open-db! dir db-name)
  111. (catch :default e
  112. (println "Error: For graph" (str (pr-str graph-dir) ":") (str e))
  113. (js/process.exit 1)))
  114. conn (sqlite-cli/read-graph db-name)
  115. datoms (d/datoms @conn :eavt)
  116. ent-maps (datoms->entity-maps datoms)]
  117. (println "Read graph" (str db-name " with " (count datoms) " datoms, "
  118. (count ent-maps) " entities and "
  119. (count (mapcat :block/properties (vals ent-maps))) " properties"))
  120. (validate-client-db @conn ent-maps options)))
  121. (defn -main [argv]
  122. (let [{:keys [args opts]} (cli/parse-args argv {:spec spec})
  123. _ (when (or (empty? args) (:help opts))
  124. (println (str "Usage: $0 GRAPH-NAME [& ADDITIONAL-GRAPHS] [OPTIONS]\nOptions:\n"
  125. (cli/format-opts {:spec spec})))
  126. (js/process.exit 1))]
  127. (doseq [graph-dir args]
  128. (validate-graph graph-dir opts))))
  129. (when (= nbb/*file* (:file (meta #'-main)))
  130. (-main *command-line-args*))