Fork me on GitHub
#pathom
<
2022-04-08
>
sheluchin18:04:54

If you have Items and Sales of those individual items, how would you model something like "total items sold in the past month" using attribute modelling? I see a few alternatives to doing it: • don't mention anything about the date range: {:item/id 1 :item/sales-count 42} • mention the implied range past-month in the context of the Item: {:item/id 1 :item/total-sold-past-month 42} • mention the implied range in the context of Sales: {:sale/item 1 :sale/past-month-count 42} • combine contexts and be explicit about the beginning of the range with since: {:item/id 1 :sale/total-count 42 :sale/since 2022-03-08} • combine contexts and be explicit about both ends of the range: {:item/id 1 :sale/total-count 42 :sale/since 2022-03-08 :sale/until 2022-04-09} • exclude the :item/id in the output since it's in the input: {:sale/total-count 42 :sale/since 2022-03-08 :sale/until 2022-04-09} • could also be more like the first one like but with no :item/id: {:sale/total-count 42} And I'm sure there are many other conceivable ways to model it. My thinking is that it would be best not to hide any information, such as the actual date range, by being explicit by both the beginning and the end of it. This would allow the requirements may grow in the future as I could end up needing counts from the past day, week, year, or any arbitrary date range. I'm having a bit of a hard time arriving at a clear model, particularly when spanning multiple relations and filters + aggregations are involved. Can anyone offer some advice?

2
wilkerlucio19:04:27

this is a really open place I think, and the answer will depend on the variations your system needs. as a rule of thumb I suggest starting with more specific names that fulfill some specific purpose of the application, but if you find yourself having to create too many of those, then its a good time to think about using more explicit params, that could be in the form of inputs (as you demonstrated) or as params

sheluchin19:04:26

Is there much value in including all of the relevant information in the response? Like, if you're using params for a date range, does it make sense to reflect the parameter data in the response? As a novice with Pathom, it sounds like a good idea, but maybe it has limited practical value. I'm getting a bit of analysis paralysis with all the options and understanding what is helpful in the longer term. In the short term I've made a bunch of specific names and my system does what I need, but it feels like I have an explosion of names and it will not scale well with requirements. I'm at the revision stage here.

2
Björn Ebbinghaus19:04:00

:item/total-sold-past-month is very explicit. (As long as you don't hold on to that data for long) Do you want to display it in your UI? Then that attribute is enough. You can add other names later. It is not a breaking change. === Calling it :sale/total-count would be a lie, wouldn't it? It is not the total count. It is a count in a range. {:item/id 1 :item/sales-in-range #:sale-range{:no-of-sales 42, :since ..., :until ...}} If someone is reading your code and finds :sale-range/no-of-sales they instantly knows that this is a number of sales in a given range, no need of explicit documentation. Else you have to write documentation like this: > :sale/total-count denotes the total number of sales of something, EXCEPT when it is in a map together with the keys :sale/since. === Being explicit with names allows you to trust your names and that helps a lot with preventing bugs. Example: Instead of: {:user/email "", :user/email-validated? false} use: {:user/validated-email ""} An unvalidated email is not the same as a validated email, is it not? If you write a function like this:

(defn reset-password [user]
  (send-new-password (:user/email user)))
You have to think: "Who is responsible for enforcing a rule like "only send new passwords to validated emails"?" "Should I do that? Maybe the caller already validated that?" If you write instead:
(defn reset-password [user]
  (send-new-password (:user/validated-email user)))
You can trust the name. You expect a validated mail. It is the callers job to provide that. Your job is to send that mail! The caller only provided a {:user/email ""} ? You blow up instead of violating an important security rule.

🙌 2
2
Björn Ebbinghaus19:04:40

The great thing with Graph APIs like pathom is, that you can provide as much data as you like. It won't impact anything, as long as you aren't explicitly asking for it.

sheluchin11:04:18

Thank you for such a detailed response @U4VT24ZM3. Your post has given me much direction and still more to think about. Taking your advice into account, the main things I'm wondering about now are: • avoiding nesting: how much is pragmatic? • introducing namespaces (:sale-range) without matching files: is this okay or should I maintain that each ns has a file? • adding dimensions (:sales-person):

{:item/id 1
 ;; Alternate ways of identifying sales by sales-person:

 ;; also adding alias from :item/sales-person -> :sales-person/id
 :item/sales-person 100
 ;; or just embedding the subtree
 :item/sales-person #:sales-person{:id 100}
 :item/sales-in-range #:sale-range{:no-of-sales 42
                                   :since <date>
                                   :until <date>
                                   ;; same representation options here as above
                                   :sales-person 100}}
 
The embedding the sales-person reference seems to lead to the same documentation requirement as you noted above. I'd have to add documentation like this: > :item/sales-in-range denotes total number of sales of something, EXCEPT when it is in a map together with the keys :item/sales-person And the alternative seems to be instead using :item/sales-in-range-by-salesman, but this is getting quite verbose and I'm not sure if embedding the entire search criteria in the name like this is practical. Perhaps replacing :item/sales-in-range #:sale-range{} with :item/filtered-sales #:filtered-sales{} would be the best alternative here? Then the search criteria could grow due more easily due to its generic parent.

Björn Ebbinghaus13:04:23

@UPWHQK562 I wouldn't provide:

{:item/id 1,
 :item/sales-person 100}
By doing this, you essentially merge the two entities. For example, if you are using a Fulcro client, this would make it impossible to normalize:
: not normalized
{:all-sales-persons {:sales-person/id 100, :sales-person/name "Björn"}
 :sold-items {:item/id 1, :item/sales-person 100, :sales-person/name "Björn"}}

; would normalize to:
{:sales-person/id {100 {:sales-person/id 100, :sales-person/name "Björn"}}
 :item/id {1 {:item/id 1, :sales-person/name "Björn"}} ;; <-- You have a duplicate!
Which is inconvenient if, e.g., you want to change the name of the sales-person.
; Better:
{:sales-person/id {100 {:sales-person/id 100, :sales-person/name "Björn"}} ;; <-- Single place where it is recorded that the name of the sales person with id=100 is "Björn"
 :item/id {1 {:item/id 1, :item/seller [:sales-person/id 100]}} 
You can of course provide a shortcut, when the client doesn't have these requirements. {:item/id 1, :item.seller/name "Björn"} Nested inputs in pathom3 are very useful for these shortcuts:
(defresolver item->seller-name [{:keys [item/sale-person]}]
  {::pco/input [{:item/sale-person [:sales-person/name]}]}
  {:item.seller/name (:sales-person/name sale-person)})
But having said all that. In the end, it's really at your discretion. What are your requirements?
; Maybe overkill? Do we really need to consider the currency as a seperate entity? 
{:item/id 42, :item/price {:price/value 100, :price/currency {:currency/id :currency/dollar, :currency/symbol "$"}}}

; Maybe this is enough?
{:item/id 42, :item/price {:price/value 100, :currency/symbol "$"}

; Or even:
{:item/id 42, :item/price-tag-dollar "$100"}

Björn Ebbinghaus13:04:35

Regarding your long names: When these things grow, the names will get less precise. It is good to avoid it, but you can't always do it. Are you really in the scope of an item any more? Maybe something like this is more suitable for you:

#:sale-statistic{:item #:item{:id 1, :name „Car"}
                 :seller #:sales-person{:id 100, :name „Björn"}
                 :volume 42
                 :since #inst 2020, 
                 :until #inst 2021}
Statistic report for sales by Item:
{:item/id 1
 :item/name "Car"
 {:item/sell-statistics 
  [#:sale-statistic{:seller {:sales-person/id 100}
                    :volume 42
                    :since #inst 2020, 
                    :until #inst 2021}]}}
Statistic report for sales by seller:
{:sales-person/id 100
 :sales-person/name "Björn"
 {:sales-person/sell-statistics
  [#:sale-statistic{:item {:item/id 1}
                    :volume 42
                    :since #inst 2020, 
                    :until #inst 2021}]}}
And many more:

❤️ 2
Björn Ebbinghaus13:04:51

To summarize: Names are hard.

sheluchin15:04:21

@U4VT24ZM3 that is extremely valuable advice. Thanks for taking the time to think it through and give your input here. I think you've provided enough of a framework for me to use throughout this iteration of my application. Should I miss something or arrive at something sub-optimal, it can get addressed in the next round. This round will advance my thinking nonetheless. By the way, are there any resources that can help learn this approach, or is it basically just accumulated through experience? I've gone through the Pathom and Fulcro tutorials, and those definitely helped... I just think that more examples with explanations thereof will help re-contextualize the practice in more ways and go a long way toward cementing the habit. I search around on GitHub for example usages often enough, but without the added explanation, doing all the code interpretation and trying to fit it into a mental framework of best practices is kind of hard. > To summarize: Names are hard. Yep, sure are. I feel like Pathom brings the hard part of programming - naming things - to the surface and makes you confront the difficult part early in the process. Mostly off-topic now, but the https://leanpub.com/elementsofclojure/read_sample book has a whole chapter on naming things, and mentions natural vs. synthetic names, where synthetic names are totally random have no intuitive mapping to the thing being named. I wonder if anyone has tried using synthetic names with Pathom 😄

Björn Ebbinghaus17:04:30

Recently I got some knowledge from "Domain Modelling Made Functional" by Scott Wlaschin. Here is a talk about it from him: https://www.youtube.com/watch?v=2JB1_e5wZmU He is using F#, which is a strongly typed language, but the ideas translate to Clojure as well. You can use names and specs instead of types. In the video, he talks about getting rid of flags (like :user/email-verified?) at the 39 minute. He does it by wrapping the type EmailAddress with a Type VerifiedEmailAddress. In Clojure, you would do something similar with names and specs.

(s/def :user/email (s/and string? #(contains? % \@))) ; Add some Regex here
(s/def :user/verified-email :user/email)

sheluchin20:04:05

@U4VT24ZM3 Thanks for the video. Pretty good talk indeed. I think there is some limitation to the "just add another type" approach. From Email -> VerifiedEmail it's pretty straight forward, but sometimes entities have a whole bag of flags and it seems like this approach won't get you far in a case like that. You'll just end up with a bunch of flags encoded as separate names and I don't know if that adds much clarity in the long run. Makes me want to check out more DDD content though.

Björn Ebbinghaus11:04:48

That's an argument against static typing. You can't possibly name every type. @U066U8JQJ talks about that in his talk https://www.youtube.com/watch?v=YaHiff2vZ_o

{:user/id 42} ; "IdOnlyUser"
{:user/id 42, :user/email ""} ; "UnverifiedUser"
{:user/id 42, :user/email "", :user/name "Björn"} ; UnverifiedNamedUser 
...
Madness! But since we don't have explicit types, we can just look at the keys of an entity, to determine what it is. > Sometimes entities have a whole bag of flags Can you give an example? Are there really situations, where you are better off giving things a lot of flags instead of other attributes? Flags don't really lessen the problem of naming, do they? Instead of type names, you now have flags. Even more: Now your attribute names don't have meaning on their own. They have different meaning based on the value of another name!