hey there. asking for a friend. what is best practice in pathom for resolvers that are supposed to look up stuff by id, i.e. ident lookups. assume that thing here is an entity in the system, coming out of a db or something. which style is the "correct" best practice here?
;; this is bad right?
(pco/defresolver retrieve-thing-by-id
[env {::whatever/keys [thing-id]}]
{::pco/output
[::whatever/attr-a
::whatever/attr-b]}
{::whatever/attr-a "foo"
::whatever/attr-b "bar"})
;; and should be this?
(pco/defresolver retrieve-thing-by-id
[env {::whatever/keys [thing-id]}]
{::pco/output
[{::whatever/thing
[::whatever/attr-a
::whatever/attr-b]}]}
{::whatever/thing
{::whatever/attr-a "foo"
::whatever/attr-b "bar"}})I'll see about setting up a proper example. thing and other are a bit abstract š
Splitting it up into two resolvers is just an abstraction that allows both to be used and tested independently. But imo there is nothing wrong with one single resolver that returns nested data if thatās fine for the use case. No point in over complicating things. Itās easy enough to split them out into two when/if you need to.
(def products
{1 {:title "Amazing Thing"}})
(def merchants
{1 {:title "ACME Inc."}})
(def offers
{1 {:product-id 1 :merchant-id 1 :price "cheap!"}})
(pco/defresolver product [{:keys [product-id]}]
{::pco/output
[:title]}
(get products product-id))
(pco/defresolver merchant [{:keys [merchant-id]}]
{::pco/output
[:title]}
(get merchants merchant-id))
(def env
(pci/register [product merchant]))
;; just starting from offer 1
(p.eql/process env (get offers 1) [:title :price])so, this is sort of a self-made shot in the foot. there is this ambiguous :title attribute that both product and merchant share, and the query being deliberately vague about which it wants
by having the first type of defresolver pathom happily returns {:title "Amazing Thing", :price "cheap!"} here
on obvious fix is to kill the :title attribute and use :product/title and :merchant/title or something
the other less obvious fix is to use the second style resolver and having to be explicit about the joins so that the query needs you to use [:price {:product [:title]}] since it doesn't really know what title is otherwise
Maybe itās opinionated, but namespaces attributes are absolutely best practice here
It solves this exact problem.
You could also model an offer, such that it is an entity that has a relationship to a merchant and another to a product.
Pathom will also help you swap between specific and generic namespaces with aliasing. So if you need a generic title attribute you can use an Aliaās to convert them to one.
I used non-namespaced keywords for this example, they are namespaced in the actual code. just using a generic ::foo/title in the project, instead of a per-entity :product/title or so
the reason being that I like generic keywords since they make certain function very easy to re-use
like (render-ui-title product) works just as well as (render-ui-title manufacturer) and not having to either duplicate that function, doing a conditional in that function, or having to pass which title attribute it should use
I'm guessing the best practice I'm asking for here doesn't really exist since this is a self-created problem that most people probably don't have š
@thheller Here are some more concrete examples
aliasing somewhat defeats the purpose of sharing in the first place, since you just have to write the code to do the translation in a different place, but you are still writing that code. with the shared attribute you are not writing that code.
The alias really just solves the data modeling problem. If we want to flatting into a single map we need to namespace them. Otherwise without the aliasing, i think the nested data works just fine.
How do the examples look to you otherwise? Without the aliasing, does that work for you?
not really. you are cheating a little bit by using product1 and merchant1 as the ids and relying on the fact that the resolver returns ::pco/unknown-value I guess
in practice the ids are integers and we can hardly rely on the existence check, and we can assume our data is sound so that we are only looking up ids that actually exist
thats why I used the same 1 as id for everything in my example
also the id attribute is not generic, it is specific to which thing you are referencing. so ::foo/product-id and ::foo/merchant-id. not ::foo/id
@thheller What about this?
well you skipped the offer, which was essential for showing the problem. just lookup by id worked fine before
The offer down the bottom? or I could be misunderstanding.
Offer is implicit I suppose, at this stage, no resolver really needed.
well what I mean is the offer not being normalized and pathom being made to "guess"
{:com.example.product/id 1 :com.example.merchant/id 1} as the entity input I guess
sure if the entity already has the join itself there is nothing to do
but I guess thats another way to solve it when reading from the db, just reading it in the normalized way
same problem, but as I said it might not be bad to read sql data that way
so a row of {:offer-id 1 :product-id 1 :merchant-id 1} is just returned as {:offer-id :product {:product-id 1} :merchant {:merchant-id 1}}
so the join is already setup and doesn't need to infer that we are joining from :product-id or :merchant-id
actually I like that. can't think of a case where this doesn't work
The attribute value is really just a reference.
And SQL or even datomic, the reference includes the table/attribute and the value.
So in SQL your data for a PRODUCT_ID column, really is: UUID references PRODUCTS . So the information you have on hand is: product + #uuid"0530bb97-7f2a-4863-a8dc-6e4fc8b3fad0"
So [product #uuid"0530bb97-7f2a-4863-a8dc-6e4fc8b3fad0"]
or: [:product/id #uuid"0530bb97-7f2a-4863-a8dc-6e4fc8b3fad0]`
or: [:com.example.product/id #uuid"0530bb97-7f2a-4863-a8dc-6e4fc8b3fad0"]
or: {:com.example.product/id #uuid"0530bb97-7f2a-4863-a8dc-6e4fc8b3fad0"}
I'm oldskool and have int8 as keys š
Yeah, just an example.
int8 works two haha
yeah not really relevant here, not what I meant.
the core problem being that you shouldn't give pathom ambiguous queries where it has two paths to resolve an attribute
and now there is a third way to address this I didn't think off
But consider this partial datalog:
[?offer :com.example/product ?product]
[?product :com.example.product/id ?product-id]
:com.example/product is an attribute belonging to the offer entity.
And the value of that reference needs both the lookup attribute and the value youāre looking for.
So either:
[:db/id 1]
or maybe even:
[:com.example.product/id 1]So it wouldnāt really make sense to put com.example.product/id attribute in the map representing the offering. Because that attribute doesnāt belong to that entity. A reference however com.example/product is perfectly fine
well this is a SQL based system. the attribute is there in the table š and I just get it like that. didn't think of converting it properly
But the reference value needs both the attr and the value:
ie: [:com.example/product 1] is not really enough.
Even in sql, the column is aware of the value of the reference, and the table itās referencing
I guess I don't follow what you are talking about now anymore?
{:offer-id 1 :product-id 1 :merchant-id 1} is the raw table output I get, no transformation or anything
you know like create table offers (id serial, product_id int8 not null, merchant_id int8 not null) or whatever
yes, I can absolutely do some transformation when reading from the db, but the db itself is flat
Basically:
;; Don't do this:
{:com.example/kind :com.example.kind/offer
:com.example.product/id 1
:com.example.merchant/id 1}
;; Instead, do this:
{:com.example/kind :com.example.kind/offer
:com.example/product {:com.example.product/id 1}
:com.example/merchant {:com.example.merchant/id 1}}yes, that is what I said. do it directly when reading from the db, instead of having pathom do it for me
Yeah, that.
thanks for the idea, didn't think of that
All g. Sorry if I was confusing you haha
@thheller This example is more tailored to data you might expect from an SQL database
Iām understanding the confusion is probably around the idea that :product/id and :offer/product_id are different attributes:
:product/id is an int8
:offer/product_id is a reference/foreign-key
yeah, thats making pathom do it again. I think I prefer the other route though
I do too. I think both are good solutions though.
But prefer the first.
still has the problem of making "invalid queries" possible but I guess thats fine
I suppose with solution 2. You can let your titles be ā:product/titleā and then alias them to generics. The db lib will probably namespace them with the table anyway
Solution 2 is pretty much your 2nd option you originally suggested I suppose
like from {:offer/id 1} resolving [:generic/title] gives you either the product or merchant title, don't know what decides what you get š
Nah because itās nested.
not quite, since it has that ambiguity removed
:offer/id gives you :offer/product that is a map containing the generic title.
So no ambiguity
Basically referring to the offer test in my last example.
(deftest offer-test
(is (= ?
(p.eql/process
com.company.example1/pathom-env
{:offer/id 1}
[:generic/title]))))is what I mean
Yeah you probably donāt want that. Unless the offer can also have a title. Or your deriving the title for the offer from product or merchant.
thats what I meant. it is probably ok that this works, since the user just needs to be more explicit in the query.
But if you want offer to have itās own title, or derive from product and/or merchant that works fine. And you can still get both the merchant and product titles through the nested entities via the reference attributes
All in all itās more so a data modeling problem than pathom.
well its still a pathom question in what resolvers to write and how
> like (render-ui-title product) works just as well as (render-ui-title manufacturer) and not having to either duplicate that function, doing a conditional in that function, or having to pass which title attribute it should use
ime you will have the happiest time in pathom by making these functions resolvers that return namespaces attributes and querying for that attribute within pathom itself. Eg your :generic/title pattern, which can give you a āui title labelā and which ui code would consume directly. I do this even for eg the āsameā field in datomic vs in a json endpoint, which will have two different namespaces. The resolvers take care of the conversion between worlds and the query only talks about the form it wants to see. The prep for a json payload is just filtering keys and stripping namespaces off
Pathom becomes the map-and-attribute-based āuniversal data modelā of multiple domains, and resolvers the bridge between them and between the data models and their data sources
Making this pleasant and effective though means namespacing everything
oh my 100+ replies
btw:
https://github.com/souenzzo/eql-style-guide#resolve-it-flat
hello, sorry getting late to the party here, but everything folks said about the Pathom modeling looks correct to me, and that's how its intended to be used. my extra cent is that it helps if you keep in mind that every entity is fully generic (kinda like entities on datomic, that can always have any attribute), this makes the namespaces really important because they your units of disambiguation. then instead of trying to understand what "type" are you in, its really about "what data I start with?" that will dictate the reach you can do
couldn't find anything in the docs the specifically recommend one or the other?
The first is good
Second probably not as good unless there is a specific reason for it.
Imagine a map representing an entity. It would have an ID attribute and its other attributes at the same level, in the same map. So you can have a map as an input with only the id attribute. Then using the first example, pathom will take that input, and merge the output map with the input (kinda). It will all be available at the same level.
but the first creates issues when entities share attributes, since pathom will then know multiple ways to get a certain attribute?
(pco/defresolver retrieve-other-by-id
[env {::whatever/keys [other-id]}]
{::pco/output
[::whatever/attr-a
::whatever/attr-c]}
{::whatever/attr-a "foo"
::whatever/attr-c "baz"})And since theyāre all merged into the same map. Itās structured like you would expect.
imagine :attr-a being a :title or some other generic attribute
so if a query now asks for :title, how does pathom decide which to get?
One map/level represents only one entity.
hmm k, so the problem is somewhere else if :attr-a is suddenly returning something from a different entity
(trying to debug a problem a friend is seeing, not my own setup/code so a lot of guessing involved here)
No worries. Iām on my phone. I can can explain it better when Iām at my computer.
mostly just wanted to check if I'm giving bad advice or not. both resolver types make sense to me, the second seems a bit cleaner but requires a lot more unwrapping of stuff later š
The structure of the output of your second example, would be better suited as the input when dealing with relationships between two different entities You have some parent entity, and a child as a level of nesting. When you fetch the parent, you can also fetch the child with just the id/ref attribute. And have a separate resolver like your first example to merge in the child attributes. Now you can use pull syntax to either fetch the hydrated parent & child entity together. But also you can just hydrate the child or parent separately if you have their ids.
yes, this is also useful for joins but it seems weird to have both resolvers. thats why I asked basically. do you use the second form just because you get joins out of it automatically?
especially since this is a sql based system and the the things table might have :other-id, so just being able to [{[:thing-id 1] [:attr-a {:other [:attr-a]}]}] seems more natural?
You can just use a single resolver and return nested data if want.
Itās just less flexible. But itās fine if thatās all you need
that is not really the answer I'm looking for. asking for the best practice here. do whatever is not best practice. š