Fork me on GitHub
#fulcro
<
2021-11-23
>
Mark Wardle09:11:55

Hi all. I’m probably being stupid here but I am struggling to get the results of a mutation normalized. This is my first fulcro project. Here is my state after the mutation has run:

{:fulcro.inspect.core/app-id "pc4.ui.root/Root",
 :fulcro.inspect.core/app-uuid
 #uuid "e3bbdf08-d3fd-42e0-9c38-8184f6a9e78d",
 :com.fulcrologic.fulcro.application/active-remotes #{},
 :t_project/id {nil {}},
 :t_user/id
 {1 {:t_user/id 1,
     :t_user/title "Mr",
     :t_user/first_names "System",
     :t_user/last_name "Administrator",
     :t_user/active_projects [:t_project/id nil]}},
 :session/authenticated-user [:t_user/id 1]}
You can see the nil for t_project/id while t_user/id is populated as I expected. I’m using returning and with-target in my remote:
(defmutation login
  "Performs a login action. If the server responds successfully (200) but there
  is no token, then we have invalid credentials."
  [params]
  (action [{:keys [state]}]
          (js/console.log "Performing login action" params))
  (login [env]
         (js/console.log "Sending login action to remote" env)
         (-> env
             (m/returning User)
             (m/with-target [:session/authenticated-user])))
  (ok-action [{:keys [result] :as env}]
             (let [token (get-in result [:body 'pc4.users/login :io.jwt/token])]
               (js/console.log "response from remote: " result)
               (if token (swap! (:state env) dissoc :session/error)
                         (swap! (:state env) assoc :session/error "Invalid username or password"))
               (reset! session/authentication-token token))))
My model works with a simple merge:
(com.fulcrologic.fulcro.algorithms.merge/merge-component! @SPA User {:t_user/id              2
                                                                       :t_user/last_name       "Smith"
                                                                       :t_user/first_names     "John"
                                                                       :t_user/active_projects [{:t_project/id   1
                                                                                                 :t_project/name "Wibble"}]})
So I don’t think I have my components set-up wrong, but I’ve checked and double-checked my idents and queries! The query itself works at my backend, and works in the Fulcro Inspect query. I would be very grateful for any hints or pointers as to where I’m going wrong! Many thanks!

cjmurphy10:11:04

You could also show the query of the User component.

Mark Wardle10:11:01

Yes of course - thanks! I’m sure I’ve done something trivially stupid here, but I’m just about to try a different approach with df/load! and see what happens.

(defsc Project [this {:t_project/keys [id name title]}]
  {:ident :t_project/id
   :query [:t_project/id :t_project/name :t_project/title]}
  (dom/li "Project: " name))

(def ui-project (comp/factory Project {:keyfn :t_project/id}))

(defsc User [this {:t_user/keys [id title first_names last_name active_projects] :as user token :io.jwt/token}]
  {:query [:t_user/id :io.jwt/token :t_user/title :t_user/first_names :t_user/last_name
           {:t_user/active_projects (comp/get-query Project)}]
   :ident :t_user/id}
  (when user
    (dom/div
      (dom/p "User " id " " title " " first_names " " last_name)
      (dom/ul
        (map ui-project active_projects))
      (dom/button :.border.border-black.bg-blue-400.hover:bg-blue-200.shadow.mb-2 {:onClick #(comp/transact! @SPA [(list 'pc4.users/logout)])} "Logout"))))

Mark Wardle10:11:54

Ok… I may have found my issue. My resolvers return lists, because they do some logic like filtering etc… rather than vectors. I can reproduce my problem locally. The first version, using a list, fails. The second, using a vector, works. Does that mean my resolvers must return vectors and not lists, or is there something I’m missing?

(com.fulcrologic.fulcro.algorithms.merge/merge-component!
    @SPA
    User
    {:t_user/id              2
     :t_user/last_name       "Smith"
     :t_user/first_names     "John"
     :t_user/active_projects '({:t_project/id   1
                                :t_project/name "Wibble"})})
  (com.fulcrologic.fulcro.algorithms.merge/merge-component!
    @SPA
    User
    {:t_user/id              2
     :t_user/last_name       "Smith"
     :t_user/first_names     "John"
     :t_user/active_projects [{:t_project/id   1
                               :t_project/name "Wibble"}]})

Mark Wardle10:11:30

Indeed, wrapping the results of resolvers with (vec) seems to work for me:

(pco/defresolver user->active-projects
  [{conn :com.eldrix.rsdb/conn} {username :t_user/username}]
  {::pco/output [{:t_user/active_projects [:t_project/id]}]}
  {:t_user/active_projects (vec (filter projects/active? (users/projects conn username)))})
So it looks as if I need to go through all of my resolvers to ensure they return vectors and not lists?

👍 1
Jakub Holý (HolyJak)15:11:31

I'm not sure whether it's fulcro or #pathom question - you could ask there. Hmm, thinking more, it's actually merge that expects and handles only vectors not lists - see these 2 lines where "normalize many" only handles vectors https://github.com/fulcrologic/fulcro/blob/develop/src/main/com/fulcrologic/fulcro/algorithms/normalize.cljc#L54-L55

Jakub Holý (HolyJak)15:11:00

I guess your could hook into Transit and tell it to translate lazy seq into vectors. @tony.kay is it a hard requirement that data uses vectors and not lists (where lists seem to be produced by transit from lazy seqs)? Or should normalize be more permissive?

Mark Wardle15:11:02

Thanks Jakub - as I own the server implementation, I should be more careful to use mapv instead of map, or simply use (vec) judiciously. That said, mostly it is a nice feature of clojure that one can think in terms of sequence abstractions instead of concrete implementation, so I assumed it would work with whatever sequence I threw at it.

Jakub Holý (HolyJak)15:11:15

Understandable. I'll try to find out whether to relax the code, improve warning messages, or improve the docs.

👍 1
Mark Wardle15:11:02

I found this in the book!

👆 1
Mark Wardle15:11:49

Sorry to spam this channel today, but I have one more query. I am flattening to-one relationships at both the SQL and server-side resolver level. This works nicely - so while I could model a user as {:user/id 1 :user/name "Mark" :user/role {:role/id 1 :role/name "Beginner"}} the nested to-one relationship is flattened to`{:user/id 1 :user/name "Mark" :role/id 2 :role/name "Beginner"}.` Obviously, it’s really easy to build a component that reflects the nested approach, but how does one build a fulcro component that can in essence, reflect BOTH entities at the same level - a compound component that can query for both but then delegate part of the UI to a component that handles part of the data - e.g. the role in this example - or am I better fixing my resolvers to always nest relationships like this - or just using stateless components and living with not normalising the data properly?

Jakub Holý (HolyJak)15:11:52

Ok, so you want combine the queries [:user/id :user/name] and [:role/id :role/name] into one b/c it is just a single "data entity" (ie map) on the server side?

Mark Wardle15:11:18

Brilliant thank you. I think your section on pathom placeholders will allow me to create a nested structure from the flat structure, and presumably as I can give each a different ident, normalisation will happen as expected.

Jakub Holý (HolyJak)15:11:29

In that case ☝️ you can use the approach ☝️☝️ to be able to display the user and the role in separate components but IMO you won't be able to normalize the role. If you want to do that then I think it needs to be returned as in your first snippet.

Jakub Holý (HolyJak)15:11:14

Hm, maybe. Not sure about the normalization, need to think more about it. It might work...

Mark Wardle15:11:46

I shall try and feedback! If not, I’ll just use proper nesting server-side and keep things simpler on the client.

🙏 1
Mark Wardle15:11:16

Thanks for the help.

Jakub Holý (HolyJak)15:11:38

BTW Why do you care about normalization in this case? Do you expect to edit the role data (and want to reflect it on all other users with that role)? Or do you experience it as a performance issue if the data is duplicated for each user with the role? My point is, normalization is great but it is not always necessary.

👍 1
Mark Wardle15:11:27

Simply a contrived example. The value should update if it is changed elsewhere.

👍 1
xceno19:11:03

A little feedback: I just update our production app to all the latest fulcro deps and switched to pathom3. The only issue I had was with an old resolver of mine that returned a seq instead of a vec which pathom3 or fulcro (not sure which one) didn't handle anymore. But that's it, everything else went super smooth! 🎉 The new helper functions for customizing rad forms, interacting with the form-state and all that are also very helpful. I'll check out the new formstate debugger next. There's tons of good stuff everywhere! 🙂

❤️ 2
tony.kay19:11:11

Thanks so much for letting us know. The debugger needs to be switched to top/bottom instead of side-by-side (I think)..it’s pretty unusable on a small screen…but that’s easy enough

tony.kay20:11:03

I’ll probably release an update later today for that

Jakub Holý (HolyJak)20:11:41

> The only issue I had was with an old resolver of mine that returned a `seq` instead of a `vec` which pathom3 or fulcro (not sure which one) didn't handle anymore. according to the NOTE at the end of https://book.fulcrologic.com/#_joins , Fulcro does not normalize components in lists instead of vectors. Is that you are running into? Perhaps the older version of Pathom did turn seqs into vectors automatically?

xceno21:11:07

Yeah that sounds exactly like my issue. I suspect it was pathom2, but I also found an old message from wilker here in slack where he said that maps and vectors are the preferred return value. So I think it was a bug on my side either way

Jakub Holý (HolyJak)21:11:21

Just talked to him today and P3 supports all sequences but vectors are still preferred. Fulcro only supports vectors for normalized sub-entities.

Jakub Holý (HolyJak)21:11:15

I just tested it with P2 and if I return a lazy seq from a resolver, it is also sent as a list to Fulcro so I would expect your resolver to be just as broken in P2?!

xceno21:11:44

Hmm that's weird. Okay, I'll try this tomorrow on an old commit, but I used this resolver for months now without issues

tony.kay21:11:07

FYI, just pushed bump to RAD and rad-sui to make debugger top/bottom instead of side/side

xceno09:11:14

> FYI, just pushed bump to RAD and rad-sui to make debugger top/bottom instead of side/side Thanks Tony, I'll check it out! @holyjak I narrowed the resolver problem down: I sanity-checked my old release again and then made another branch where I upgraded step by step, first fulcro and then pathom. Returning sequences from P2 works with all versions of fulcro up to the very latest. When I return a seq, it get's converted to a vec somewhere in between. So the problem must have something to do with Pathom3. If I return a seq in P3 it's not converted, and then fulcro doesn't normalize it. However, it's not that fulcro just doesn't do anything with a seq. Instead you end up with a single item in your DB where the ID in the Ident is nil, like so: [:some-list [:some-thing/ id nil]]

Jakub Holý (HolyJak)09:11:05

Are you saying that this is only "broken" in the latest version of Fulcro, no matter what version of Pathom you use? Or only if you have the latest Fulcro and P3 (while it works on latest F. + P2 or older F. + P3)? The normalization code has not been changed in ages. Chheck the F. Inspect Network tab - do you get the same data back from P2 and P3? If so then we have narrowed the problem to Fulcro and can look what happens there. But I have P2 and the data is returned as a list from the backend, not as a vector. And it ends up as a list in the DB as well, not as a vector either. Perhaps you need to explain exactly where you have the seq?

Jakub Holý (HolyJak)09:11:12

Sorry, I have been wrong - P2 does indeed turn seq into a vector sometimes Demo - https://github.com/holyjak/minimalist-fulcro-template-backendless/tree/demo/pathom2-vectorizes-lazy-seq, details in commit https://github.com/holyjak/minimalist-fulcro-template-backendless/commit/5b8f3612da520faddbe07da9523b2c411243fcd4 If I return a lazy seq of values, it will be a list X if it is a seq of data entities and the query specifies what of the entities to return then it will become a vector. @U066U8JQJ Is it so that while Pathom 2 sometimes turned (lazy) sequences into vectors, when the query asked for stuff inside the elements of the sequence, P3 does not do that?

xceno09:11:47

F+P2 = work F+P3 = breaks Look at`:some-list` . This is with P2. (ignore :some-other, that was just another test)

xceno09:11:01

And this one is with P3

xceno09:11:28

Ahh I see! Thanks for that! Imho returning seq seems like undefined behavior and should be avoided

xceno12:11:48

Thanks for reproducing this!

🙏 1
zeitstein19:11:04

First of all, I want to thank @tony.kay for writing (and recording) such readable and in-depth materials; I think the main ideas are communicated incredibly well, and the difficulty for a beginner lies in their synthesis. Also, @holyjak for his materials and exercises. I'm evaluating Fulcro for building a UI that talks primarily to a local (in-browser and/or on-disk) Datomic-like database. I tried searching (here and Google) for a discussion on using Fulcro like this, but I haven't found anything concrete (though the topic pops-up). And while there is much more to learn (read up to chapter 5 – twice :) – and watched many tutorial videos) and think through, I would appreciate some advice / guidelines / pointers on this use case, try to short-circuit researching a bit. Specifically, having Pathom as a layer in front of a local Datomic-like is conceptually tripping me up. 1. It's quite feasible that I can feed my main views through a single pull on the database returning a ready-as-is data tree. Not sure I can write a Pathom resolver to do it in a single query on the database? If my view is composed of dozens of components, and I have Pathom composing resolvers for individual components, it seems suboptimal to have Pathom running dozens of queries against the db. I haven't dug into Pathom yet – perhaps there are ways to tailor this to my needs? 2. Since the database is local, I'm wondering whether Pathom's overhead becomes more significant? (Though it seems like this will be https://blog.wsscode.com/pathom-updates-05/). 3. Just in general, are there downsides to using Fulcro and Pathom like this I should be aware of? 4. Would looking through the RAD Datomic plugin help, assuming no prior RAD knowledge? Ultimately, I know I have to build and test myself, but I'm looking at weeks of learning before I'm able to. So, any help / discussion would be much appreciated.

Mark Wardle20:11:23

Hi. I’m only a beginner with fulcro, so can’t speak about that side of things at all, but I’ve used pathom for some time now. You can definitely write a resolver that can satisfy a complex nested request in a single go - I sometimes do that to optimise fetches to the database in what is essentially pre-fetching, and pathom will fill in the gaps only if there are gaps. You can test this out easily and prove it for yourself. Indeed, you can even write a batch resolver that takes a list of identifiers and batch resolves - limiting the number of individual fetches you need for, say, a to-many relationship for a number of entities, although I’ve not needed that very often.

Jakub Holý (HolyJak)21:11:54

You are reversing the dependency. In Fulcro, the client asks for the data it wants, the server delivers just that. With your single pull, it is suddenly the server that decides what data the client gets. Of course that is a fine solution, we did just that in our REST-based app but here it goes against the grain. You can do what Mark describes - make a single resolver that returns all the data and Pathom will fill in any gaps, if there ever appear some (and you make resolvers for them). Pathom resolvers do not need to match components / data entities - they just need to be composable to answer the queries the client sends. Regarding the overhead - I don't know but you can measure it easily with (time ..) or criterium. Just run the pull directly and then via the single resolver.

Jakub Holý (HolyJak)21:11:20

To summarise: Make a single resolver that returns all the data, just make sure to maintain it and the client query in sync. Measure the performance of the pull vs. running it through Pathom if you are worried about overhead. I am not aware of any downsides to this (other that you have to maintain the 2 to be in sync).

zeitstein21:11:59

@U013CFKNP2R, thanks! Will dig into Pathom docs. @holyjak, thanks for providing more details. One thing I'm unclear on is how Pathom decides which resolvers to run. Imagine my data model as a file system: files and folders nested in folders. On the one hand, I want to display the whole tree starting from some root – that would be my single pull. On the other hand I sometimes I would want resolvers for single folder-id or file-id. This is where I get confused, because now Pathom could, in principle, compose these resolvers to fulfil the 'single pull' query... So, I shouldn't even think about side-stepping Pathom (if that's even possible)? I guess with the 'single pull', Pathom is kind of just a wrapper around a database operation, so shouldn't be much of an overhead.

tony.kay22:11:47

Do not sidestep pathom until you see an actual performance problem. You want the arbitrary composition that it can do. There are features (such as final) that can be used for really heavy things, but the become fragile because then the UI cannot evolve without risk of breakage (say you want a new thing…with a resolver that says “I did everything” you get breakage). In terms of performance monitoring: I’d recommend tufte. It is very low overhead and gives you VERY details info. You can wrap the whole parser call in profile, and then each resolver body in p. Then add the basic-println-handler, and then every time you do a request you see an immediate log of the performance stats. Pathom has a tool for visualizing that, but then you have to return the trace as part of the query and that, to me, is a lot more trouble. I just leave the p and profile stuff in place, and optionally add/remove handlers that do reporting. Has so little performance impact that I don’t worry about it for prod even.

❤️ 1
tony.kay22:11:48

Short story: don’t bother to optimize until you have a proven problem, and then don’t start writing optimizations until you’ve measured the problem.

1
zeitstein22:11:03

Thank you, much appreciated. I figured the only way was to try the standard way and then see whether performance is an issue. But, I was looking for some reassurance that I won't get into trouble later on, and I think, implicitly, I got it 🙂 Datascript is relatively popular, but I don't see many projects using it with Fulcro. And I keep thinking I'm missing something, since Fulcro makes so much sense.

tony.kay04:11:50

Datascript can easily be used with Fulcro. I think, in fact, I used it in the book: https://github.com/fulcrologic/fulcro-developer-guide/blob/master/src/book/book/database.cljs

zeitstein07:11:12

> Datascript can easily be used with Fulcro. Right. Which is why I wonder why more projects are not using with Fulcro. Probably some of it is that Fulcro is seemingly more difficult to learn, perhaps they are using Datascript itself as the UI database, etc. > Fulcro. I think, in fact, I used it in the book: https://github.com/fulcrologic/fulcro-developer-guide/blob/master/src/book/book/database.cljs I haven't reached that part in the book. This is going to be very useful, thank you!

Jakub Holý (HolyJak)09:11:29

> why more projects are not using with Fulcro. Probably some of it is that Fulcro is seemingly more difficult to learn That is my belief as well - that's why I am trying to make it more approachable. > One thing I'm unclear on is how Pathom decides which resolvers to run. #pathom can point you to the resources that explain it. I believe it makes an execution plan and executes the cheapest one. If it sees that there is a single resolver that can return everything vs. a number of resolvers, I would believe it will call the former.

👍 1
Jakub Holý (HolyJak)09:11:18

Regarding which resolvers are used, in Pathom 3 the answer is here I guess https://pathom3.wsscode.com/docs/resolvers#prioritization

zeitstein12:11:40

Thank you, Jakub! Need to dig into Pathom. I'd start using Pathom 3 right away, but it's probably going to be easier to stick to Pathom 2 while I'm learning (finding examples, etc.). Thanks to Tony's previous link, I managed to set up client-side Pathom resolvers for #asami using mock-http-server for the remote. Now the fun starts!

hadils22:11:08

Need some ideas/suggestions on how to hold on to binary images on the front-end so I can transact them with the rest of my data. Global atom? Prop? What are others doing? Thanks in advance.

tony.kay22:11:32

See RAD. Forms support this.

tony.kay22:11:22

the Fulcro file-upload allows you to send them with a transaction, which can be ok, but in that case if you want to display them you’re going to have to generate a data url with a thumbnail in it (e.g. via a canvas draw offscreen). In that case it does a multi-part post and puts the transaction in one part and the image(s) in other parts.

tony.kay22:11:12

In terms of “holding” them…depends on what you mean. You can use component-local-state

tony.kay22:11:24

if it’s just a “hold them until I save” sort of thing

hadils22:11:04

Thanks @tony.kay. That is very helpful.

👍 1