dmitriid

dmitriid

  1. First, you don’t get Ash.
  2. Then you hate it’s verbose low-level API
  3. Then you atempt to write everyting declaratively in resource actions
  4. Then you discover
action :name, :return_type do
   run fn input, context ->
       .... whatever you want, including direct Ecto queries etc. ....
       {:ok, result}
   end
end
  1. And then you start noticing things in documentation that tell you: you can use Ash.Query here

I’m building a blog engine (how original). One of the main things that a blog has is: pages and tags. So, my question was: given a tag, load all of its pages, given a specific offset and a limit.

For example you have many pages tagged with ash, and a user requests page 2 from those pages. And their pages can additionally have their own tags, and calculations, and aggregations, and things that you may want to load. And you want a total count of those pages etc.

My first attempt

My first attempt was: why not use Ecto?

action :get_pages_for_tag, :map do
   argument :tag_id, :uuid
   argument :limit, :integer
   argument :offset, :integer

   run fn input, _context ->
     query = from tag in Tags,
        where: tag.id = ^input.arguments.tag_id,
        join: page_tags in PageTags,
        on: page_tags.tag_id == tag.id,
        join: page in Pages,
        on: page.id,
        select: page
    ....
  end
end

This poses a few issues: it’s a bit tedious to write out, you still have to add code to load related data (like al the related tags for the loaded pages) etc. There were some issues with haveing macros inside macros inside macros which would confuse the compiler (I think).

Second attempt and epiphany

I decided to play with Ash.Query in the console while skimming through docs. And these things simultaneously struck me:

  • Ash.Query is a slightly higher level of abstraction than pure Ecto
  • You can load related data by simply asking Ash to load it
  • Documentation for Ash.Query.load is this:
  load(query, options)
  @spec load(
      t() | Ash.Resource.t(),
      atom
      | Ash.Query.Calculation.t()
      | list(atom | Ash.Query.Calculation.t())
      | list({atom | Ash.Query.Calculation.t(), term})
  ) :: t()

Yes. It accepts a resource or a query as the first parameter!

So, without getting into the details of the actual resources involved, I give you this beauty:

query =
  MyApp.Blog.Tag
  # we don't need other fields, just id
  |> Ash.Query.select([:id])
  # load a calculaiton that counts no of pages in this tag
  |> Ash.Query.load(:no_of_pages)
  # load pages related to this tag, with limit and offset
  |> Ash.Query.load(
    pages: 
      MyApp.Blog.Page
      |> Ash.Query.load([:tags, :external_scripts])
      |> Ash.Query.limit(paging[:limit])
      |> Ash.Query.offset(paging[:offset])
  )
  # regular filters on the :pages relationship. 
  # these could be added directly in the `load(pages: ...)` query
  |> Ash.Query.filter(
    Ash.Query.expr(
      pages.is_published == true and pages.is_tag == false and
        pages.published_at <= ^NaiveDateTime.utc_now()
    )
  )
  |> Ash.Query.filter(Ash.Query.expr(id == ^input.arguments[:tag_id]))

{:ok, result} = query |> MyApp.Blog.read_one()

Boom. One srather straightforward piece of code to do what I need.

Yes, those can be used in declarative actions, too

  read :pages_for_tag do
     load(pages: 
       MyApp.Blog.Page 
       # only select specific fields
       |> Ash.Query.Select([:id, :title])
       # load relations for pages
       |> Ash.Query.load([:tags, :external_scripts]) ....)

     filter(... your filters ...)
  end

Caveats

Don’t blindly trust code. The query above generates three separate queries that are fetched from the database. This is totally fine in my case, but might not be fine for you.

Showing Posts 1 to 10

zachdaniel

zachdaniel

Creator of Ash

Great stuff! A couple tips for you:

  1. if you want the tags and then for that given tag the pages that match that criteria, you’d want to do the filtering in the load query. The query you have is more like “tags where they have a published page that matches these criteria”, as opposed to “the tag, loading the pages that match these criteria”

  2. You don’t need to use Ash.Query.expr with Ash.Query.filter. It already wraps it for you.

pages_query =
  MyApp.Blog.Page
  |> Ash.Query.load([:tags, :external_scripts])
  |> Ash.Query.limit(paging[:limit])
  |> Ash.Query.offset(paging[:offset])
  |> Ash.Query.filter(
      pages.is_published == true and pages.is_tag == false and
        pages.published_at <= ^NaiveDateTime.utc_now()
  )

query =
  MyApp.Blog.Tag
  |> Ash.Query.select([:id])
  |> Ash.Query.load(:no_of_pages)
  |> Ash.Query.load(pages: pages_query)
  |> Ash.Query.filter(id == ^input.arguments[:tag_id])
absowoot

absowoot

There’s also the Ash Realworld git that has some good example code for posts and tags.

dmitriid

dmitriid OP

you’d want to do the filtering in the load query.

That’s what I did in the code right after I posted this :slight_smile: Unfortunately, can’t update the post anymore…

You don’t need to use Ash.Query.expr with Ash.Query.filter. It already wraps it for you.

Ah. I would sometimes run into function x is undefined when running without the expr, but can’t remember when. So now I tend to just stick expr everywhere :slight_smile:

dmitriid

dmitriid OP

Thank you! I took a quick look and… so that’s how you use prepare! :slight_smile:

zachdaniel

zachdaniel

Creator of Ash

This happens when you’ve not done require Ash.Query typically.

dmitriid

dmitriid OP

Somehow I’ve had issues even after require’ing Ash.Query :thinking:

zachdaniel

zachdaniel

Creator of Ash

:thinking: would need to see some specific example, but typically that is what causes that issue.

deviprsd

deviprsd

I’m in step no. 3, the realworld git is definitely a big help

CYMKD

CYMKD

Amazing stuff.
Im in step 3 as well, literally did a lot generic actions with run() and now fighting understanding the Resource Actions in detail (hoping to definitely contribute to DX docs once I feel like im not just blindly making things work)

deviprsd

deviprsd

Maybe we should have a thread of all the docs pain points somewhere so we can gather them and be able clean them up.

Last night I had to dig through discord to find out how to actually use Ash.PubSub but it is made using pubsubs easy-peasy. Took a solid minute there to get the instructions.

Edit: I kind of made a github discussion thread Documentation Pain Points Thread · ash-project/ash · Discussion #776 · GitHub to help out

Where Next? Top

Trending in Discussions Top

AstonJ
As the title says, please share what you’ve been up to with Elixir. Whether that’s been learning it, looking into it, making stuff with i...
2977 92995 915
New
AstonJ
The obligatory hello world thread! Who are you and where are you from? :stuck_out_tongue:
4616 55835 594
New
caslu
I want to open this thread for you all to discuss and help those who really like Ash but are still hesitant to use it in a real project. ...
New
arcanemachine
I was working on an Ecto migration and I needed a timestamp. So, for the nth time, I looked up the different data types for timestamps, a...
New
alexslade
Fly’s CEO posted this recently - Turn And Face The Strange · The Fly Blog It says that Fly is going all-in on sprites, which is a worry ...
New
Herve37
We’re evaluating API mocking tools for OpenAPI-based projects and would love to hear what other teams are using. We’re particularly inte...
New
matt-savvy
Is there a word for the ~> symbol used in Version strings? Do you also just call it a Squiggle Arrow™ ?!
New

Other Trending Topics Top

garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
wintermeyer
There are three potential reasons for members of this forum to have a look at https://vutuv.de You are tired or annoyed of LinkedIn. Yo...
New
aseigo
ICal is a library for interacting with iCalendar data. It parses iCalendars into typed Elixir structs via ICal.from_ics, and can prepare ...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews