I want the date to be the key and I want to return the resources in an array, should I use an action to make this possible in Ash?

Hello,

...
attributes do
    uuid_primary_key :id

    attribute :title, :string do
      allow_nil? false
    end

    attribute :date_of_record, :date do
      allow_nil? false
    end

    attribute :date_of_visited, :date
    attribute :pain_of_scale, :integer, constraints: [min: 1, max: 5]
    attribute :memo, :string
    attribute :doctor_opinion, :string

    create_timestamp :created_at do
      private? false
 end
...

I have the above resources defined and I want to group by date_of_visited and get back an array of resources with date as key value. How can I do this?
Additionally, weโ€™d like to see pagination applied as well.

expected:

{
  "2024-01-02": [{resource}, {resource}, ....],
  "2024-03-02": [{resource}, {resource}, ...]
}

Youโ€™ll probably want to combine a generic action with an optional paginated read action. You can pass in your pagination parameters into the read action and then transform those results into the desired format.

2 Likes

Thanks for letting us know.
Iโ€™ll give it a try and get back to you.

%{
  ~D[2024-03-11] => [
    %{
      id: "697757c9-fe23-43ce-9c51-747d25224967",
      user: %{type: :relationship, __struct__: Ash.NotLoaded, field: :user},
      title: "์˜ค๋Š˜ ๊ธฐ๋กํ•œ ๋‚ด์šฉ",
      __struct__: Dentallog.DentalDiary.Diary,
      date_of_visited: ~D[2024-03-11],
      calculations: %{},
      aggregates: %{},
      __metadata__: %{
        selected: [:archived_at, :id, :title, :date_of_record, :date_of_visited, :pain_of_scale,
         :memo, :doctor_opinion, :created_at, :updated_at, :user_id],
        keyset: "g2o="
      },
      archived_at: nil,
      __lateral_join_source__: nil,
      __order__: nil,
      created_at: ~U[2024-03-24 00:31:44.315150Z],
      updated_at: ~U[2024-03-24 00:31:44.315150Z],
      paper_trail_versions: %{
        type: :relationship,
        __struct__: Ash.NotLoaded,
        field: :paper_trail_versions
      },
      __meta__: %{
        state: :loaded,
        context: nil,
        prefix: nil,
        source: "dental_diaries",
        __struct__: Ecto.Schema.Metadata,
        schema: Dentallog.DentalDiary.Diary
      },
      user_id: "83d1da63-39d3-4c27-bc93-3a72f70435fc",
      date_of_record: ~D[2024-03-11],
      doctor_opinion: "์˜์‚ฌ์Œค์ด ๋‹ค์Œ์ฃผ์— ๋‚ด์›ํ•˜๋ผ๊ณ  ํ•˜์‹ฌ",
      memo: "์˜ค๋Š˜ ๊ฐ”๋Š”๋ฐ ์ถฉ์น˜๊ฐ€ ๋งŽ์•˜์Œ",
      pain_of_scale: 5,
      photos: %{type: :relationship, __struct__: Ash.NotLoaded, field: :photos}
    },
    %{
      id: "a59f58cb-2348-44d0-9669-a805e7b97a74",
      user: %{type: :relationship, __struct__: Ash.NotLoaded, field: :user},
      title: "์˜ค๋Š˜ ๊ธฐ๋กํ•œ ๋‚ด์šฉ",
      __struct__: Dentallog.DentalDiary.Diary,
      date_of_visited: ~D[2024-03-11],
      calculations: %{},
      aggregates: %{},
      __metadata__: %{
        selected: [:archived_at, :id, :title, :date_of_record, :date_of_visited, :pain_of_scale,
         :memo, :doctor_opinion, :created_at, :updated_at, :user_id],
        keyset: "g2o="
      },
      archived_at: nil,
      __lateral_join_source__: nil,
      __order__: nil,
      created_at: ~U[2024-03-24 00:31:44.651395Z],
      updated_at: ~U[2024-03-24 00:31:44.651395Z],
      paper_trail_versions: %{
        type: :relationship,
        __struct__: Ash.NotLoaded,
        field: :paper_trail_versions
      },
      __meta__: %{
        state: :loaded,
        context: nil,
        prefix: nil,
        source: "dental_diaries",
        __struct__: Ecto.Schema.Metadata,
        schema: Dentallog.DentalDiary.Diary
      },
      user_id: "83d1da63-39d3-4c27-bc93-3a72f70435fc",
      date_of_record: ~D[2024-03-11],
      doctor_opinion: "์˜์‚ฌ์Œค์ด ๋‹ค์Œ์ฃผ์— ๋‚ด์›ํ•˜๋ผ๊ณ  ํ•˜์‹ฌ",
      memo: "์˜ค๋Š˜ ๊ฐ”๋Š”๋ฐ ์ถฉ์น˜๊ฐ€ ๋งŽ์•˜์Œ",
      pain_of_scale: 5,
      photos: %{type: :relationship, __struct__: Ash.NotLoaded, field: :photos}
    }
  ],
  ~D[2024-03-12] => [
    %{
      id: "95eecd00-3a72-4f41-b958-050bf6793528",
      user: %{type: :relationship, __struct__: Ash.NotLoaded, field: :user},
      title: "์˜ค๋Š˜ ๊ธฐ๋กํ•œ ๋‚ด์šฉ",
      __struct__: Dentallog.DentalDiary.Diary,
      date_of_visited: ~D[2024-03-12],
      calculations: %{},
      aggregates: %{},
      __metadata__: %{
        selected: [:archived_at, :id, :title, :date_of_record, :date_of_visited, :pain_of_scale,
         :memo, :doctor_opinion, :created_at, :updated_at, :user_id],
        keyset: "g2o="
      },
      archived_at: nil,
      __lateral_join_source__: nil,
      __order__: nil,
      created_at: ~U[2024-03-24 00:31:44.881270Z],
      updated_at: ~U[2024-03-24 00:31:44.881270Z],
      paper_trail_versions: %{
        type: :relationship,
        __struct__: Ash.NotLoaded,
        field: :paper_trail_versions
      },
      __meta__: %{
        state: :loaded,
        context: nil,
        prefix: nil,
        source: "dental_diaries",
        __struct__: Ecto.Schema.Metadata,
        schema: Dentallog.DentalDiary.Diary
      },
      user_id: "83d1da63-39d3-4c27-bc93-3a72f70435fc",
      date_of_record: ~D[2024-03-11],
      doctor_opinion: "์˜์‚ฌ์Œค์ด ๋‹ค์Œ์ฃผ์— ๋‚ด์›ํ•˜๋ผ๊ณ  ํ•˜์‹ฌ",
      memo: "์˜ค๋Š˜ ๊ฐ”๋Š”๋ฐ ์ถฉ์น˜๊ฐ€ ๋งŽ์•˜์Œ",
      pain_of_scale: 5,
      photos: %{type: :relationship, __struct__: Ash.NotLoaded, field: :photos}
    },
    %{
      id: "7a2951dc-4d58-49a1-aa44-ea18618d8a88",
      user: %{type: :relationship, __struct__: Ash.NotLoaded, field: :user},
      title: "์˜ค๋Š˜ ๊ธฐ๋กํ•œ ๋‚ด์šฉ",
      __struct__: Dentallog.DentalDiary.Diary,
      date_of_visited: ~D[2024-03-12],
      calculations: %{},
      aggregates: %{},
      __metadata__: %{
        selected: [:archived_at, :id, :title, :date_of_record, :date_of_visited, :pain_of_scale,
         :memo, :doctor_opinion, :created_at, :updated_at, :user_id],
        keyset: "g2o="
      },
      archived_at: nil,
      __lateral_join_source__: nil,
      __order__: nil,
      created_at: ~U[2024-03-24 00:31:45.128320Z],
      updated_at: ~U[2024-03-24 00:31:45.128320Z],
      paper_trail_versions: %{
        type: :relationship,
        __struct__: Ash.NotLoaded,
        field: :paper_trail_versions
      },
      __meta__: %{
        state: :loaded,
        context: nil,
        prefix: nil,
        source: "dental_diaries",
        __struct__: Ecto.Schema.Metadata,
        schema: Dentallog.DentalDiary.Diary
      },
      user_id: "83d1da63-39d3-4c27-bc93-3a72f70435fc",
      date_of_record: ~D[2024-03-11],
      doctor_opinion: "์˜์‚ฌ์Œค์ด ๋‹ค์Œ์ฃผ์— ๋‚ด์›ํ•˜๋ผ๊ณ  ํ•˜์‹ฌ",
      memo: "์˜ค๋Š˜ ๊ฐ”๋Š”๋ฐ ์ถฉ์น˜๊ฐ€ ๋งŽ์•˜์Œ",
      pain_of_scale: 5,
      photos: %{type: :relationship, __struct__: Ash.NotLoaded, field: :photos}
    }
  ]
}

As far as grouping by date, I was successful.
and I donโ€™t think I need pagination, now that I think about it.
because read the entire data when doing a groupby anyway.

The problem is when I return it to AshGraphql.

queries do
      action :list_diary_group_by_visited_date, :group_by_date_of_visited
 end
...
action :group_by_date_of_visited, {:array, :map} do
      run fn input, context ->
        diary_list = Dentallog.DentalDiary.Diary
                        |> Dentallog.DentalDiary.read!()
                        |> Enum.group_by(&(&1.date_of_visited))
        diary_list = Jason.encode!(diary_list)
        {:ok, diary_list}
      end
    end

The return value is in the form of a [JsonString!] and I would like to return it as a structure of my choice.
Do I need to have knowledge of Absinte to do this?

Hmmโ€ฆI think we will need to add something for you to make that work without using absinthe. But otherwise yes you will need to define that type in absinthe.

schema.ex

query do
    field :get_dental_diaries, list_of(:dental_diary_group_by_date) do
      resolve(&Resolvers.DentalDiary.diary/3)
    end
  end

object :dental_diary_group_by_date do
    field :visited_date, :string
    field :diaries, list_of(:dental_diary)
  end

resource.ex

def group_by_date_of_visited do
      Diary
      |> Dentallog.DentalDiary.read!()
      |> Enum.group_by(& &1.date_of_visited)
      |> Enum.map(fn {date, diary_list} ->
        %{
          visited_date: date,
          diaries: diary_list
        }
      end)

    action :group_by_date_of_visited, :struct do
      constraints instance_of: :dental_diary_group_by_date

      run fn input, _context ->
        {:ok, group_by_date_of_visited()}
      end
    end

Iโ€™ve had success using an absinthe object return type.
But, I want to use the ash generic action rather than absinthe, cuz I want to use ash_graphql.
But I donโ€™t know how to specify the dental_diary_group_by_date return type in generic action.
Do you know how to do this?

You can use Ash.Type.NewType to create a new type of map, and then you can use def graphql_type(_), do: :your_type

1 Like

Iโ€™m sorry, but Iโ€™ve been looking at the documentation for a long time and donโ€™t understand it.

group_by_date_diary_type.ex

defmodule Dentallog.Types.GroupByDateDiary do
  use Ash.Type.NewType,
    subtype_of: :map

  def graphql_type, do: :group_by_date_diary
end

diary.ex

...
action :group_by_date_of_visited, :group_by_date_of_visited do
      run fn input, _context ->
        {:ok, group_by_date_of_visited()}
      end
 end
...

I think I need a more detailed example.
If you donโ€™t mind, could you elaborate a bit more? :smiling_face_with_tear:

Yes, sorry about that :slight_smile: I was at ElixirConf EU, so didnโ€™t have as much time to expand on my forum post answers.

Given this type:

defmodule Dentallog.Types.GroupByDateDiary do
  use Ash.Type.NewType,
    subtype_of: :map

  use AshGraphql.Type

  @impl AshGraphql.Type
  def graphql_type(_), do: :group_by_date_diary
end

you could then use it in your action

action :group_by_date_of_visited, Dentallog.Types.GroupByDateDiary do
    run fn input, _context ->
      {:ok, group_by_date_of_visited()}
    end
end
1 Like

It works! :grinning:
Youโ€™re right, I did think youโ€™d be busy with ElixirConf EU 2024.
Thanks as always for your hard work.

1 Like