pathom

thheller 2023-08-11T06:24:45.213509Z

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"}})

thheller 2023-08-11T07:01:51.639069Z

I'll see about setting up a proper example. thing and other are a bit abstract šŸ˜›

caleb.macdonaldblack 2023-08-11T07:16:19.093529Z

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.

thheller 2023-08-11T07:21:25.995489Z

(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])

thheller 2023-08-11T07:22:27.191049Z

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

thheller 2023-08-11T07:22:53.846999Z

by having the first type of defresolver pathom happily returns {:title "Amazing Thing", :price "cheap!"} here

thheller 2023-08-11T07:23:36.170529Z

on obvious fix is to kill the :title attribute and use :product/title and :merchant/title or something

thheller 2023-08-11T07:24:24.967879Z

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

caleb.macdonaldblack 2023-08-11T07:32:34.393299Z

Maybe it’s opinionated, but namespaces attributes are absolutely best practice here

caleb.macdonaldblack 2023-08-11T07:33:05.216079Z

It solves this exact problem.

caleb.macdonaldblack 2023-08-11T07:36:37.338969Z

You could also model an offer, such that it is an entity that has a relationship to a merchant and another to a product.

caleb.macdonaldblack 2023-08-11T07:40:56.127509Z

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.

thheller 2023-08-11T07:43:45.774689Z

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

thheller 2023-08-11T07:44:17.101159Z

the reason being that I like generic keywords since they make certain function very easy to re-use

thheller 2023-08-11T07:45:21.017289Z

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

thheller 2023-08-11T07:49:29.031719Z

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 šŸ˜›

caleb.macdonaldblack 2023-08-11T08:24:21.854579Z

caleb.macdonaldblack 2023-08-11T08:24:45.925249Z

caleb.macdonaldblack 2023-08-11T08:24:48.337239Z

@thheller Here are some more concrete examples

thheller 2023-08-11T08:28:16.294459Z

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.

caleb.macdonaldblack 2023-08-11T08:35:17.938189Z

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.

caleb.macdonaldblack 2023-08-11T08:39:31.818849Z

How do the examples look to you otherwise? Without the aliasing, does that work for you?

thheller 2023-08-11T08:43:53.519099Z

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

thheller 2023-08-11T08:44:27.755019Z

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

thheller 2023-08-11T08:45:10.618919Z

thats why I used the same 1 as id for everything in my example

thheller 2023-08-11T08:48:28.154009Z

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

caleb.macdonaldblack 2023-08-11T08:49:54.524019Z

@thheller What about this?

thheller 2023-08-11T08:51:47.540399Z

well you skipped the offer, which was essential for showing the problem. just lookup by id worked fine before

caleb.macdonaldblack 2023-08-11T08:52:26.091649Z

The offer down the bottom? or I could be misunderstanding.

caleb.macdonaldblack 2023-08-11T08:52:56.972779Z

Offer is implicit I suppose, at this stage, no resolver really needed.

thheller 2023-08-11T08:54:09.526779Z

well what I mean is the offer not being normalized and pathom being made to "guess"

thheller 2023-08-11T08:55:22.809509Z

{:com.example.product/id 1 :com.example.merchant/id 1} as the entity input I guess

thheller 2023-08-11T08:55:42.034759Z

sure if the entity already has the join itself there is nothing to do

thheller 2023-08-11T08:55:59.602919Z

but I guess thats another way to solve it when reading from the db, just reading it in the normalized way

caleb.macdonaldblack 2023-08-11T08:56:11.134999Z

thheller 2023-08-11T08:58:01.350239Z

same problem, but as I said it might not be bad to read sql data that way

thheller 2023-08-11T08:58:56.903739Z

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}}

thheller 2023-08-11T09:00:05.353149Z

so the join is already setup and doesn't need to infer that we are joining from :product-id or :merchant-id

thheller 2023-08-11T09:01:23.743699Z

actually I like that. can't think of a case where this doesn't work

caleb.macdonaldblack 2023-08-11T09:02:12.772909Z

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"}

thheller 2023-08-11T09:03:06.975239Z

I'm oldskool and have int8 as keys šŸ˜‰

caleb.macdonaldblack 2023-08-11T09:03:24.238129Z

Yeah, just an example.

caleb.macdonaldblack 2023-08-11T09:04:59.151899Z

int8 works two haha

thheller 2023-08-11T09:05:29.946059Z

yeah not really relevant here, not what I meant.

thheller 2023-08-11T09:06:45.539849Z

the core problem being that you shouldn't give pathom ambiguous queries where it has two paths to resolve an attribute

thheller 2023-08-11T09:07:40.079289Z

and now there is a third way to address this I didn't think off

caleb.macdonaldblack 2023-08-11T09:07:57.554129Z

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]

caleb.macdonaldblack 2023-08-11T09:09:03.009859Z

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

thheller 2023-08-11T09:09:33.251369Z

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

caleb.macdonaldblack 2023-08-11T09:10:00.493449Z

But the reference value needs both the attr and the value: ie: [:com.example/product 1] is not really enough.

caleb.macdonaldblack 2023-08-11T09:10:30.652499Z

Even in sql, the column is aware of the value of the reference, and the table it’s referencing

thheller 2023-08-11T09:10:32.335079Z

I guess I don't follow what you are talking about now anymore?

thheller 2023-08-11T09:11:00.218279Z

{:offer-id 1 :product-id 1 :merchant-id 1} is the raw table output I get, no transformation or anything

thheller 2023-08-11T09:11:34.500139Z

you know like create table offers (id serial, product_id int8 not null, merchant_id int8 not null) or whatever

thheller 2023-08-11T09:11:58.147849Z

yes, I can absolutely do some transformation when reading from the db, but the db itself is flat

caleb.macdonaldblack 2023-08-11T09:12:17.650429Z

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}}

thheller 2023-08-11T09:12:27.986369Z

yes, that is what I said. do it directly when reading from the db, instead of having pathom do it for me

caleb.macdonaldblack 2023-08-11T09:13:45.816619Z

Yeah, that.

thheller 2023-08-11T09:14:12.708059Z

thanks for the idea, didn't think of that

caleb.macdonaldblack 2023-08-11T09:15:02.130189Z

All g. Sorry if I was confusing you haha

caleb.macdonaldblack 2023-08-11T09:22:35.438519Z

@thheller This example is more tailored to data you might expect from an SQL database

caleb.macdonaldblack 2023-08-11T09:25:21.287659Z

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

thheller 2023-08-11T09:27:34.003359Z

yeah, thats making pathom do it again. I think I prefer the other route though

caleb.macdonaldblack 2023-08-11T09:28:00.717369Z

I do too. I think both are good solutions though.

caleb.macdonaldblack 2023-08-11T09:28:10.628499Z

But prefer the first.

thheller 2023-08-11T09:29:32.647759Z

still has the problem of making "invalid queries" possible but I guess thats fine

caleb.macdonaldblack 2023-08-11T09:29:42.924249Z

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

caleb.macdonaldblack 2023-08-11T09:32:12.280469Z

Solution 2 is pretty much your 2nd option you originally suggested I suppose

thheller 2023-08-11T09:32:20.688269Z

like from {:offer/id 1} resolving [:generic/title] gives you either the product or merchant title, don't know what decides what you get šŸ˜›

caleb.macdonaldblack 2023-08-11T09:32:38.386829Z

Nah because it’s nested.

thheller 2023-08-11T09:32:38.652369Z

not quite, since it has that ambiguity removed

caleb.macdonaldblack 2023-08-11T09:33:19.130379Z

:offer/id gives you :offer/product that is a map containing the generic title.

caleb.macdonaldblack 2023-08-11T09:33:38.347779Z

So no ambiguity

caleb.macdonaldblack 2023-08-11T09:34:00.444559Z

Basically referring to the offer test in my last example.

thheller 2023-08-11T09:34:57.722729Z

(deftest offer-test
  (is (= ?
         (p.eql/process
           com.company.example1/pathom-env
           {:offer/id 1}
           [:generic/title]))))

thheller 2023-08-11T09:35:00.258189Z

is what I mean

caleb.macdonaldblack 2023-08-11T09:36:08.298199Z

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.

thheller 2023-08-11T09:36:44.225449Z

thats what I meant. it is probably ok that this works, since the user just needs to be more explicit in the query.

caleb.macdonaldblack 2023-08-11T09:37:27.828799Z

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

caleb.macdonaldblack 2023-08-11T09:38:44.185539Z

All in all it’s more so a data modeling problem than pathom.

thheller 2023-08-11T09:57:23.875449Z

well its still a pathom question in what resolvers to write and how

favila 2023-08-11T13:54:06.573879Z

> 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

favila 2023-08-11T13:56:14.426579Z

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

favila 2023-08-11T13:58:48.395589Z

Making this pleasant and effective though means namespacing everything

souenzzo 2023-08-14T09:07:03.089609Z

oh my 100+ replies btw: https://github.com/souenzzo/eql-style-guide#resolve-it-flat

wilkerlucio 2023-08-14T12:48:37.153309Z

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

thheller 2023-08-11T06:27:30.617949Z

couldn't find anything in the docs the specifically recommend one or the other?

caleb.macdonaldblack 2023-08-11T06:38:33.956479Z

The first is good

caleb.macdonaldblack 2023-08-11T06:38:57.525449Z

Second probably not as good unless there is a specific reason for it.

caleb.macdonaldblack 2023-08-11T06:41:53.059339Z

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.

thheller 2023-08-11T06:42:33.242459Z

but the first creates issues when entities share attributes, since pathom will then know multiple ways to get a certain attribute?

thheller 2023-08-11T06:43:13.161059Z

(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"})

caleb.macdonaldblack 2023-08-11T06:43:15.142399Z

And since they’re all merged into the same map. It’s structured like you would expect.

thheller 2023-08-11T06:43:28.290659Z

imagine :attr-a being a :title or some other generic attribute

thheller 2023-08-11T06:43:52.712799Z

so if a query now asks for :title, how does pathom decide which to get?

caleb.macdonaldblack 2023-08-11T06:44:15.647069Z

One map/level represents only one entity.

thheller 2023-08-11T06:45:48.947639Z

hmm k, so the problem is somewhere else if :attr-a is suddenly returning something from a different entity

thheller 2023-08-11T06:46:15.260209Z

(trying to debug a problem a friend is seeing, not my own setup/code so a lot of guessing involved here)

caleb.macdonaldblack 2023-08-11T06:46:47.511939Z

No worries. I’m on my phone. I can can explain it better when I’m at my computer.

thheller 2023-08-11T06:48:41.166489Z

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 šŸ˜›

caleb.macdonaldblack 2023-08-11T06:51:10.904189Z

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.

thheller 2023-08-11T06:55:00.560079Z

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?

thheller 2023-08-11T06:57:21.847919Z

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?

caleb.macdonaldblack 2023-08-11T06:57:39.231569Z

You can just use a single resolver and return nested data if want.

caleb.macdonaldblack 2023-08-11T06:57:53.038829Z

It’s just less flexible. But it’s fine if that’s all you need

thheller 2023-08-11T06:59:42.265559Z

that is not really the answer I'm looking for. asking for the best practice here. do whatever is not best practice. šŸ˜›