woylie/flop

Provide a way to export/present Flop and Flop.Meta data

hgg opened this issue ยท 7 comments

hgg commented

Is your feature request related to a problem? Please describe.
I'm using Flop in a small project where the intended output is JSON, to be consumed by a FE app. When introducing pagination, filtering and ordering to the API, I have to introduce code on my end to deal with parsing the Flop.Meta structure properly so that it can be returned to the FE.

Describe the solution you'd like
I think ideally the lib would provide clear ways to present information about what kind of filters, orderings and pagination are active. I believe this can easily be achieved with functions on the Meta module.
I don't know if there are any standards for something like this.
My suggestion is a function Meta.output_config(%Meta{}) :: %{filtering: map() | nil, ordering: map() | nil, pagination: map() | nil}. With something like this, people could then take the information and adapt it to whatever their API response structure is.
I'm not sure if you would rather place this on the flop_phoenix library.

Describe alternatives you've considered
We could also go for a more phoenix approach and create a JSON view for the Meta object. This would probably make more sense in the flop_phoenix lib. The downside I see is I think the base lib should be capable of "exporting" this information.

Another, simpler, approach could be to just add a JSON encoder to the Meta struct. If it's coupled to a specific JSON lib, that could definitely be a downside. If it's not, I don't see much the difference from the initial suggestion.

Additional context
I'm happy to work on this if you'll have my help. Been looking to doing more open-source contributions and this seems like an approachable one, if it does go forward.

woylie commented

I don't quite understand what you have in mind. Can you give me a concrete example for what format you want to return to the front end?

hgg commented

Something like

%{
  filter: [%{value: "john ", op: :ilike, field: :name}],
  limit: 10,
  order: %{order_by: [:name], order_directions: [:asc]},
  pagination: %{
    current_page: 2,
    has_next_page?: false,
    has_previous_page?: true,
    next_page: nil,
    page_size: 2,
    previous_page: 1,
    total_pages: 2
  }
}

Basically something that tells the FE exactly what happened with the query on the backend in terms of filtering, sorting and pagination so that the FE can react to it. I think the best example is pagination: if I don't tell the FE something, how does it know it can ask for more pages?

EDIT: I generated this map from information inside the %Flop.Meta{} I get from running the query.

woylie commented

Well, the Meta struct has all that information, and the cast and validated Flop struct is under meta.flop. You just want it in some other arbitrary format.

If you just wanted convert the Meta struct into JSON as it is, in the easiest case, you could convert the Meta struct, the nested Flop struct and the nested Filter structs into maps with Map.from_struct/1 and pass the result to Jason.encode/1. I don't want Flop to depend on a JSON library, but you can also implement the Jason.Encode protocol for these structs using defimpl instead of using @derive (see https://hexdocs.pm/jason/Jason.Encoder.html#module-example).

hgg commented

I see your point ๐Ÿ‘๐Ÿผ
I think what made me think this would be interesting was the fact that we have ways to use Flop out-of-the-box on HTML views, so I thought JSON views could have the same kind of support. Otherwise people using this with JSON need to figure out each and every field that are relevant for their use-case.
For instance in my case, I ended up creating a module to convert the structure. But I'm not really sure I even got all the relevant fields, and it feels weird to go get data from inside the structure like this. Almost like I'm breaking the boundary of the lib. I'll paste it here just to give you context.

defmodule MyApp.MetaJSON do
  def data(%Flop.Meta{} = meta) do
    %{
      order: order_data(meta),
      filter: filter_data(meta),
      pagination: pagination_data(meta),
      limit: limit_data(meta)
    }
  end

  defp order_data(%Flop.Meta{} = meta) do
    %{order_by: meta.flop.order_by, order_directions: meta.flop.order_directions}
  end

  defp filter_data(%Flop.Meta{} = meta) do
    Enum.map(meta.flop.filters, &Map.from_struct/1)
  end

  defp pagination_data(%Flop.Meta{start_cursor: start_cursor} = meta)
       when is_binary(start_cursor) do
    Map.take(meta, [:start_cursor, :end_cursor])
  end

  defp pagination_data(%Flop.Meta{flop: %Flop{page: current_page}} = meta)
       when is_integer(current_page) do
    Map.take(meta, [
      :current_page,
      :has_next_page?,
      :has_previous_page?,
      :next_page,
      :page_size,
      :previous_page,
      :total_pages
    ])
  end

  defp pagination_data(%Flop.Meta{flop: %Flop{offset: current_offset}} = meta)
       when is_integer(current_offset) do
    Map.take(meta, [
      :current_offset,
      :next_offset,
      :previous_offset,
      :total_count
    ])
  end

  defp pagination_data(_meta), do: %{}

  defp limit_data(%Flop.Meta{} = meta) do
    meta.flop.limit ||
      meta.opts[:for]
      |> struct()
      |> Flop.Schema.default_limit()
  end
end

And this is being called from other JSON view files.

Anyways, thanks for the quick responses. Feel free to close this ๐Ÿ˜„

woylie commented

Maybe we can add a to_map function to the Meta module. But only as a convenience to get the meta and the nested structs as maps, without any transformations (maybe nil values can be removed).

hgg commented

@woylie let me know if that's something you'd like help with. I'm happy to contribute.

woylie commented

@hgg Sure, if you want to contribute, go ahead!