I'm picking up a low-key charity project for my church, replacing a horrifying bag of Excel macros with a RAD app. since it involves financial data and some history editing, XTDB is a superb fit. I'm adding XTDB2 as a new backend for the RAD demo. XTQL is much nicer to generate than SQL strings. I don't need anything, just wanted to say it aloud! I'll post the RAD adapter for XTDB2 under my own Github and send a PR to the demo repo in due course. (I also want to use pathom3, and that branch of the demo is quite behind master and has lots of conflicts if you try to rebase it, but one thing at a time. I'm definitely inclined to spend the first 80% of the time sharpening my tools.)
You saw the existing (older) xtdb plugin?
I did, yeah. XTDB2 is quite a different beast! (the big gaps are no Datalog, and 2 has tables where 1 does not.)
honestly one could probably use the SQL adapter, since it's wire-compatible with Postgres. but there's no schema up front, so I dunno what happens if you send CREATE TABLEs to XTDB2. it might error or it might do nothing and succeed, like "yes, you can store into that table, postcondition achieved!" I'm sure there would be some adjustment of the adapter needed somewhere.
In the exercise 6 of fulcro-excercises one needs to get the idents of the entities that satisfy a certain predicate. The solution uses map and filter. Given that the DB is normalized and having EQL I would expect that it should be possible to solve it using a query. Treating the DB as a map feels low-level and takes more effort than using a DSL. Is this what everybody does? The question is not about the exercise, but rather whether it is possible to query the client DB using predicates and select all idents, not only one. Or the only way is to use core Clojure functions?
@mardukbp did you mean by “exercise 6 of fulcro-excercises” https://github.com/fulcro-community/fulcro-exercises/blob/main/src/holyjak/fulcro_exercises.cljs#L276-L301? I don’t see how “one needs to get the idents of the entities that satisfy a certain predicate” applies there.
Option B of the solution is implemented using merge/remove-ident*. In order to use this function it is necessary to get the idents first. In the solution the idents are built from the IDs (which are obtained after filtering the DB), but one could also get them directly from the DB by concat-ing the nested maps. I was only asking if there's a convenience function that abstracts filtering the DB and extracting the idents, because I think it's a common use case.
After reading section 12.2 of the Developer's Guide I was under the impression that a mutation must always have an "action" section, but today I rewatched your talk "Software Development Leverage" and I noticed that you defined a mutation without it. Then I went back to the guide and found out that this is mentioned in the section "Optimistic vs Pessimistic". I think that this information belongs in section 12.2.2 where the sections are described.
;; OPTION B: DIY deletion:
(defn delete-player [state-map [player-id team-id]]
(-> state-map
(update :player/id dissoc player-id)
(merge/remove-ident* [:player/id player-id]
[:team/id team-id :team/players])))
(defn delete-selected* [{player-map :player/id :as state-map}]
(let [player->team (make-player->team (-> state-map :team/id vals))
selected-player-ids
(->> player-map
vals
(filter :ui/checked?)
(map :player/id))
player-teams (map player->team selected-player-ids)]
;; OPTION B: DIY deletion:
(reduce
delete-player
state-map
(map vector selected-player-ids player-teams))
;; OPTION A: using built-in helpers
#_...))
? Since it is called delete-selected* I assume the IDs actually come from the user triggering the action, and not from “filtering the DB”? I guess we are partly speaking past each other here… .
Is your problem that you don’t want to construct idents manually from IDs but would prefer to find them in the DB, given the ids?! I don’t see why you’d want that…
Doing “filtering the DB and extracting the idents” would make sense to me e.g. when I am asked to delete all “bad” players with an imaginary :player/score being 0. But that is rather trivial, I don’t have any needs for some help from fulcro other than its norm/remove-entity .Sorry for the confusion. I should have specified that I was talking about delete-player, not delete-selected*. My idea was that instead of calculating selected-player-ids, which involves calling vals, filter, and map, one could just get the idents declaratively with a function such as (norm/filter-ident pred state-map) . Because in the end one needs the idents in order to delete them.
The point of the exercise is just to show you that it’s just plain data, and get you used to the structure. There is a db->tree function for converting the db data into a tree if you want it, but often you want to do a particular manipulation on the tree that isn’t part of EQL (EQL is NOT like SQL…it’s a simple graph query, not a programming language). So there are no “EQL predicates”.
The main point OF Fulcro is that your view is a direct simple projection of your state, and you write side-effect functions to take the state from S1 to S2. As much as possible you do not mix the view function with additional logic. Think about your statement on predicates: Who edits the EQL to change the predicates? Where are those predicate changes stored? You get into a whole mess of “mutate this thing” when your head is in that space.
A Fulcro view is (react-render (db->tree state-map root-query)). db->tree follows idents (and a few other simple things). There is NO magic in the rendering layer, by design.
There are some additional helpers in Fulcro, for example in the com.fulcrologic.fulcro.algorithms.normalized-state namespace, like sort-idents-by, remove-entity, remove-ident, etc.
This model means that IF you have a copy of an (immutable) version of the fulcro state map, AND you’ve avoided side-effects in the UI (e.g. React hooks and other mutable state), THEN your UI is a PURE function of state, and you can record a sequence of states, play them back, write unit tests against the state, and show your logic is correct with straightforward pure logic/tests.
So, want a helper for doing some arbitrary “thing” to your UI: it’s simple data manipulation of the normalized state: all pure functions which are trivially unit testable.
So, the larger answer is that Fulcro is designed to make UI programming a pure-functional system as much as possible: S1 -> mutation -> S2 -> mutation -> S3… All side-effects are moved into a “remote” or as a response to use events (react event handlers) and those interactions with the world result in execution of mutations like “the internal predefined merge the result of a network query”. So, even async operation is S1 -> mutation -> S2 -> … async result… -> S3 -> …
with a render at each ->
Thank you very much for taking the time to write such a detailed response! I will take a look at specter. I think it might satisfy my desire to use a DSL for transforming nested data. I have a follow-up question. My understanding is that one usually mutates the client DB optimistically and then replicates the mutation on the server in order to persist the changes. So it's kind of like an offline-first approach where changes happen on the client and the server is mostly used for persistence and loading new data? I just want to know what is the usual data flow.
There isn't a preferred usual data flow. I'm just supplying primitives for you to build whatever it is you need