biff

jf 2024-09-10T13:28:41.325399Z

what is the (a?) recommended schema if I have an app that needs to cater to different types of users? user type A has a certain set of attributes, user type B has a different set... and so on (3 different types to be precise). The way I see it, I have 3 options: 1. one table to unify them all: the single table that will contain all users, with all possible attributes thrown in there. In that case, I would have to mark ALL of the non-common attributes as {:optional true} 2. one table to contain all the users with their common, and only common attributes, with the user type-specific attributes in their own separate tables. 3. one table for each user type: I get the ability to leave schema enforcement of the user tables to malli... but in return for that I get to have to do user type differentiation for everywhere where multiple user types may be involved. I'm guessing based on the tradeoff for approach 3 that 1 or 2 is probably preferable... any thoughts?

2024-09-18T16:37:03.014119Z

forgot to look at this till just now-- It shouldn't be necessary to pass the whole [:map ...] thing to malli.core/validate. If malli-opts is set correctly, you should be able to just use :user etc as aliases. The problem here might be that you've got the print-table call in the schema.clj file, and perhaps it's getting evaluated before com.eelchat/malli-opts gets the updated schema value. I should've mentioned that I was evaluated print-table in the com.example file (or com.eelchat in your case), below the malli-opts def. Here's a full git diff--this time I've done things a bit more cleanly by putting the print-table code in repl.clj and getting malli-opts from the context map, which is how you'd want to do it if you needed to validate schemas manually somewhere (instead of just letting submit-tx do the schema validation for you):

diff --git a/starter/dev/repl.clj b/starter/dev/repl.clj
index b3782a6..81f870f 100644
--- a/starter/dev/repl.clj
+++ b/starter/dev/repl.clj
@@ -2,7 +2,9 @@
   (:require [com.example :as main]
             [com.biffweb :as biff :refer [q]]
             [clojure.edn :as edn]
-            [clojure.java.io :as io]))
+            [clojure.java.io :as io]
+            [malli.core :as malli]
+            [clojure.pprint :as pprint]))
 
 ;; REPL-driven development
 ;; ----------------------------------------------------------------------------------------
@@ -64,6 +66,26 @@
   ;; restarting your app, and calling add-fixtures again.
   (add-fixtures)
 
+
+  (let [malli-opts @(:biff/malli-opts (get-context))]
+    (pprint/print-table
+     (for [doc [{:xt/id #uuid "51da2256-c048-441f-92fd-e3978a4fcd5c"
+                 :user/email "a@example.com"
+                 :user/joined-at #inst "2024-09-10T19:17:53.818-00:00"
+                 :user/type :user/type1
+                 :user/foo "foo"
+                 :user/bar "bar"}
+                {:xt/id #uuid "190d0d7f-ed21-41c2-b483-db35b0fa2af7"
+                 :user/email "b@example.com"
+                 :user/joined-at #inst "2024-09-10T19:17:53.818-00:00"
+                 :user/type :user/type2
+                 :user/spam "spam"
+                 :user/eggs "eggs"}]
+           schema [:user :user/type1 :user/type2]]
+       {:user (:user/email doc)
+        :schema schema
+        :valid (malli/validate schema doc malli-opts)})))
+
   ;; Query the database
   (let [{:keys [biff/db] :as ctx} (get-context)]
     (q db
diff --git a/starter/src/com/example.clj b/starter/src/com/example.clj
index 63fafb1..a5b6be5 100644
--- a/starter/src/com/example.clj
+++ b/starter/src/com/example.clj
@@ -41,7 +41,7 @@
   (biff/add-libs)
   (biff/eval-files! ctx)
   (generate-assets! ctx)
-  (test/run-all-tests #"com.example.*-test"))
+  #_(test/run-all-tests #"com.example.*-test"))
 
 (def malli-opts
   {:registry (malr/composite-registry
diff --git a/starter/src/com/example/schema.clj b/starter/src/com/example/schema.clj
index e8e979a..c240929 100644
--- a/starter/src/com/example/schema.clj
+++ b/starter/src/com/example/schema.clj
@@ -1,20 +1,25 @@
 (ns com.example.schema)
 
-(def schema
-  {:user/id :uuid
-   :user [:map {:closed true}
-          [:xt/id                     :user/id]
-          [:user/email                :string]
-          [:user/joined-at            inst?]
-          [:user/foo {:optional true} :string]
-          [:user/bar {:optional true} :string]]
+(def user-types [:user/type1 :user/type2])
+
+(defn user-schema [& extra]
+  (into [:map {:closed true}
+         ;; Common attributes go here
+         [:xt/id          :uuid]
+         [:user/email     :string]
+         [:user/joined-at inst?]
+         [:user/type      (into [:enum] user-types)]]
+        ;; Extra contains attributes specific to the different user types
+        extra))
 
-   :msg/id :uuid
-   :msg [:map {:closed true}
-         [:xt/id       :msg/id]
-         [:msg/user    :user/id]
-         [:msg/text    :string]
-         [:msg/sent-at inst?]]})
+(def schema
+  {:user/type1 (user-schema [:user/foo :string]
+                            [:user/bar :string])
+   :user/type2 (user-schema [:user/spam :string]
+                            [:user/eggs :string])
+   :user       (into [:multi {:dispatch :user/type}]
+                     (for [t user-types]
+                       [t t]))})
 
 (def module
   {:schema schema})
And from the print-table call I get:
|         :user |     :schema | :valid |
|---------------+-------------+--------|
| a@example.com |       :user |   true |
| a@example.com | :user/type1 |   true |
| a@example.com | :user/type2 |  false |
| b@example.com |       :user |   true |
| b@example.com | :user/type1 |  false |
| b@example.com | :user/type2 |   true |

2024-09-10T19:25:47.555839Z

Since malli schemas are just an extra abstraction on top of XTDB/there aren't any actual "tables", you can do both #1 and #3--make a separate schema for each user type, like :user/type1, :user/type2 etc, and also have a :user schema that matches user documents of all sub-types. I would use https://github.com/metosin/malli?tab=readme-ov-file#multi-schemas (you can also accomplish the same with :user [:or :user/type1 :user/type2], but it'll be a little less efficient):

(ns com.example.schema)

(def user-types [:user/type1 :user/type2])

(defn user-schema [& extra]
  (into [:map {:closed true}
         ;; Common attributes go here
         [:xt/id          :uuid]
         [:user/email     :string]
         [:user/joined-at inst?]
         [:user/type      (into [:enum] user-types)]]
        ;; Extra contains attributes specific to the different user types
        extra))

(def schema
  {:user/type1 (user-schema [:user/foo :string]
                            [:user/bar :string])
   :user/type2 (user-schema [:user/spam :string]
                            [:user/eggs :string])
   :user       (into [:multi {:dispatch :user/type}]
                     (for [t user-types]
                       [t t]))})

(def module
  {:schema schema})
(clojure.pprint/print-table
 (for [doc [{:xt/id #uuid "51da2256-c048-441f-92fd-e3978a4fcd5c"
             :user/email "a@example.com"
             :user/joined-at #inst "2024-09-10T19:17:53.818-00:00"
             :user/type :user/type1
             :user/foo "foo"
             :user/bar "bar"}
            {:xt/id #uuid "190d0d7f-ed21-41c2-b483-db35b0fa2af7"
             :user/email "b@example.com"
             :user/joined-at #inst "2024-09-10T19:17:53.818-00:00"
             :user/type :user/type2
             :user/spam "spam"
             :user/eggs "eggs"}]
       schema [:user :user/type1 :user/type2]]
   {:user (:user/email doc)
    :schema schema
    :valid (malli.core/validate schema doc malli-opts)}))
;; =>
;; |         :user |     :schema | :valid |
;; |---------------+-------------+--------|
;; | a@example.com |       :user |   true |
;; | a@example.com | :user/type1 |   true |
;; | a@example.com | :user/type2 |  false |
;; | b@example.com |       :user |   true |
;; | b@example.com | :user/type1 |  false |
;; | b@example.com | :user/type2 |   true |

jf 2024-09-12T14:48:58.651889Z

thank you, Jacob! I'm still working through this, but one question about the validate code: what value do you have for malli-opts? I had to remove the parameter (`Unable to resolve symbol: :malli-opts in this context`) in order to get the code to run, but running it gives me the following:

1. Unhandled clojure.lang.ExceptionInfo
   :malli.core/invalid-schema
   {:type :malli.core/invalid-schema,
    :message :malli.core/invalid-schema,
    :data {:schema :user, :form :user}}

jf 2024-09-14T04:14:08.219549Z

thanks. That helps me get further, but I am still getting the same invalid-schema exception. I did, however, manage to get the validation code to run / print with this modified code:

(clojure.pprint/print-table
 (for [doc [{:xt/id #uuid "51da2256-c048-441f-92fd-e3978a4fcd5c"
             :user/email "a@example.com"
             :user/joined-at #inst "2024-09-10T19:17:53.818-00:00"
             :user/type :user/type1
             :user/foo "foo"
             :user/bar "bar"}
            {:xt/id #uuid "190d0d7f-ed21-41c2-b483-db35b0fa2af7"
             :user/email "b@example.com"
             :user/joined-at #inst "2024-09-10T19:17:53.818-00:00"
             :user/type :user/type2
             :user/spam "spam"
             :user/eggs "eggs"}]
       type-schema [:user/type1 :user/type2]]
   {:user (:user/email doc)
    :schema type-schema
    :valid (malli.core/validate (type-schema schema) doc com.eelchat/malli-opts)}))
This is my diff:
--- validate.1  2024-09-14 12:07:59
+++ validate.2  2024-09-14 12:16:42
@@ -11,7 +11,7 @@
              :user/type :user/type2
              :user/spam "spam"
              :user/eggs "eggs"}]
-       type-schema [:user :user/type1 :user/type2]]
+       schema [:user/type1 :user/type2]]
    {:user (:user/email doc)
-    :schema schema
-    :valid (malli.core/validate schema doc malli-opts)}))
+    :schema type-schema
+    :valid (malli.core/validate (type-schema schema) doc com.eelchat/malli-opts)}))
based on the following: • it looks like malli.core/validate ultimately expects a schema (what is basically the value in the schema that is defined in the typical biff schema.clj; so something like [:map {:closed true} ...] . So instead of passing the symbol, I did a lookup into schema, and passed that to malli.core/validate: (type-schema schema) • validation with the :user schema still fails though. I suspect that this has to do with the :user schema construction? I'm not sure that :user [ :multi { :dispatch :user/type } [ :user/type1 :user/type1 ] [ :user/type2 :user/type2 ] ] works. At least, it does not validate with malli.core/validate

jf 2024-09-14T04:28:15.302929Z

ok it looks like this works:

(def user-types [:user/type1 :user/type2])

(defn user-schema [& extra]
  (into [:map {:closed true}
         ;; Common attributes go here
         [:xt/id          :uuid]
         [:user/email     :string]
         [:user/joined-at inst?]
         [:user/type      (into [:enum] user-types)]]
        ;; Extra contains attributes specific to the different user types
        extra))

(def schema
  {:user/type1 (user-schema [:user/foo :string]
                            [:user/bar :string])
   :user/type2 (user-schema [:user/spam :string]
                            [:user/eggs :string])})

(def schema (merge schema
                   {:user (into [:multi {:dispatch :user/type}]
                                (for [t user-types]
                                  [t (t schema)]))}))

(clojure.pprint/print-table
 (for [doc [{:xt/id #uuid "51da2256-c048-441f-92fd-e3978a4fcd5c"
             :user/email ""
             :user/joined-at #inst "2024-09-10T19:17:53.818-00:00"
             :user/type :user/type1
             :user/foo "foo"
             :user/bar "bar"}
            {:xt/id #uuid "190d0d7f-ed21-41c2-b483-db35b0fa2af7"
             :user/email ""
             :user/joined-at #inst "2024-09-10T19:17:53.818-00:00"
             :user/type :user/type2
             :user/spam "spam"
             :user/eggs "eggs"}]
       type-schema [:user :user/type1 :user/type2]]
   {:user (:user/email doc)
    :schema type-schema
    :valid (malli.core/validate (type-schema schema) doc com.eelchat/malli-opts)}))

jf 2024-09-22T16:51:02.055119Z

thank you, Jacob! it does indeed look like you do not need to pass the whole [:map ...] to the schema. I am still uanble to have the malli/vaidate call work, though, for whatever reason (arrgh), but that's fine for now. I'll try to figure it out. In the meantime, my schema is simpler (no need to def and def again!), and my transactions still work! that's still a win. Thank you!

🎅 1