How to accomplish a lookup across a many to many relationship?

Hi,

Feels like a silly question but say I have the following 3 resources: Site, Page, SitePages.

SitePages being a many to many resource defining the following relationship:

  relationships do
    belongs_to :site, ContentCove.Sites.Site,
      primary_key?: true,
      allow_nil?: false

    belongs_to :page, ContentCove.Sites.Page,
      primary_key?: true,
      allow_nil?: false

    belongs_to :created_by, ContentCove.Accounts.User, allow_nil?: false
    belongs_to :updated_by, ContentCove.Accounts.User, allow_nil?: false
  end

How do I define a read action :page_for_site that would take a site_id and path string. It would find all pages where the attribute page.path=^arg(:path) and site_id =^arg(:site_id)?

E.g something along the lines of

    read :page_for_site do
      argument :site_id, :uuid
      argument :path, :string
      filter expr(path == ^arg(:path) and sites.site_id == ^arg(:site_id))

      prepare build(load: :sites)
    end

I want to return Page(s) that are found. I presume the Page resource would be the most appropriate place for this to live?

I think what you have is right except you just need site_id ==

filter expr(path == ^arg(:path) and site_id == ^arg(:site_id))

I’ve tried it unfortunately doesnt work or even show a query being executed. To make things more clear here are the 3 Resources.

Site:

defmodule ContentCove.Sites.Site do
  use Ash.Resource,
    otp_app: :content_cove,
    domain: ContentCove.Sites,
    data_layer: AshPostgres.DataLayer,
    extensions: [AshAdmin.Resource]

  postgres do
    table "sites"
    repo(ContentCove.Repo)
  end

  actions do
    defaults [:read, :destroy]
    default_accept [:domain, :name]

    create :create do
      argument :pages, {:array, :uuid} do
        allow_nil? true
      end

      primary? true
      change relate_actor(:created_by)
      change relate_actor(:updated_by)
      change manage_relationship(:pages, type: :append_and_remove)
    end

    update :update do
      require_atomic? false

      argument :pages, {:array, :uuid} do
        allow_nil? true
      end

      primary? true
      change relate_actor(:created_by)
      change relate_actor(:updated_by)
      change manage_relationship(:pages, type: :append_and_remove)
    end
  end

  attributes do
    uuid_primary_key :id

    attribute :domain, :ci_string do
      allow_nil? false
      constraints casing: :lower
    end

    attribute :name, :string, allow_nil?: false

    create_timestamp :inserted_at
    update_timestamp :updated_at
  end

  relationships do
    belongs_to :created_by, ContentCove.Accounts.User, allow_nil?: false

    belongs_to :updated_by, ContentCove.Accounts.User, allow_nil?: false

    many_to_many :pages, ContentCove.Sites.Page do
      through ContentCove.Sites.SitePage
      source_attribute_on_join_resource :site_id
      destination_attribute_on_join_resource :page_id
    end
  end

  identities do
    identity :unique_domain, [:domain]
  end
end

Page:

defmodule ContentCove.Sites.Page do
  use Ash.Resource,
    otp_app: :content_cove,
    domain: ContentCove.Sites,
    data_layer: AshPostgres.DataLayer,
    extensions: [AshAdmin.Resource]

  postgres do
    table "pages"
    repo(ContentCove.Repo)
  end

  actions do
    defaults [:read, :destroy]
    default_accept [:path, :title, :template]

    create :create do
      primary? true
      change relate_actor(:created_by)
      change relate_actor(:updated_by)
    end

    update :update do
      require_atomic? false
      primary? true
      change relate_actor(:created_by)
      change relate_actor(:updated_by)
    end

    read :page_for_site do
      argument :site_id, :uuid
      argument :path, :string
      filter expr(path == ^arg(:path) and site_id == ^arg(:site_id))

      prepare build(load: :sites)
    end
  end

  attributes do
    uuid_primary_key :id

    attribute :path, :string do
      allow_nil? false
    end

    attribute :title, :string do
      allow_nil? false
    end

    attribute :template, :ci_string do
      allow_nil? false
      constraints casing: :lower
    end

    create_timestamp :inserted_at
    update_timestamp :updated_at
  end

  relationships do
    belongs_to :created_by, ContentCove.Accounts.User, allow_nil?: false

    belongs_to :updated_by, ContentCove.Accounts.User, allow_nil?: false

    many_to_many :sites, ContentCove.Sites.Site do
      through ContentCove.Sites.SitePage
      source_attribute_on_join_resource :page_id
      destination_attribute_on_join_resource :site_id
    end
  end
end

SitePage

defmodule ContentCove.Sites.SitePage do
  use Ash.Resource,
    otp_app: :content_cove,
    domain: ContentCove.Sites,
    data_layer: AshPostgres.DataLayer,
    extensions: [AshAdmin.Resource]

  postgres do
    table "site_pages"
    repo(ContentCove.Repo)
  end

  admin do
    create_actions []
  end

  actions do
    defaults [:read, :destroy]

    create :create do
      primary? true
      change relate_actor(:created_by)
      change relate_actor(:updated_by)
    end

    update :update do
      primary? true
      change relate_actor(:created_by)
      change relate_actor(:updated_by)
    end
    
    read :page_for_site do
      argument :site_id, :uuid
      argument :path, :string
      get? true
      filter expr(path == ^arg(:path))
      filter expr(site.id == ^arg(:site_id))
    end    
  end

  relationships do
    belongs_to :site, ContentCove.Sites.Site,
      primary_key?: true,
      allow_nil?: false

    belongs_to :page, ContentCove.Sites.Page,
      primary_key?: true,
      allow_nil?: false

    belongs_to :created_by, ContentCove.Accounts.User, allow_nil?: false
    belongs_to :updated_by, ContentCove.Accounts.User, allow_nil?: false
  end
end

Just to be more clear. Im trying to filter on the path attribute of the page_id foreign key together with the site_id on the SitePage resource and want to return a page or pages instead of the many to many record directly since it isn’t of much use by itself

How are you calling the action? I’m a bit confused about the idea of it not even showing a query being run.

Oh, sorry I misunderstood your initial snippet. Page does not belongs_to :site.

filter expr(path == ^arg(:path) and sites.id == ^arg(:site_id))

Doh! thanks