woylie / flop

Filtering, ordering and pagination for Ecto
MIT License
678 stars 36 forks source link

Suport operators/variables in custom sorting operations. #511

Open mattmatters opened 2 weeks ago

mattmatters commented 2 weeks ago

Hey folks, thanks for creating such a fantastic library. We are using it in production and it's been absolutely fantastic.

A recent issue/feature request I've come across is I need to sort a list of items by an arbitrary variable.

Example: Show tasks assigned to a specific user first before everything else.

This can obviously be handled via a fragment in the order by. I am currently considering just doing it in the query passed to flop. However it would be nice to formalize this and expose it in the same api the client currently uses.

My thought would be we could introduce another parameter alongside order_directions and order_by called order_variables (or whatever name people like). Then extend the custom field section to have something like a custom sorter. In the example above, I would pass the user id as a variable along with the sort direction.

For example, imagine the custom function was an mfa (and that the fragment was actually valid)

@derive {
  Flop.Schema,
  filterable: [:creator],
  adapter_opts: [
    custom_fields: [
      creator: [
        sorter: fn query, %Flop.Sort{variable: var, direction: dir} ->
          order_by(query, [p], [{dir, fragment("(CASE WHEN ? = ? THEN 1 ELSE 0 END) ?", p.user_id, ^var})]
        end,
        ecto_type: :integer,
        operators: [:<=, :>=]
      ]
    ]
  ]
}

Would love to hear your thoughts, more than happy to contribute a pr if it's something you would be interested in adding to your library. I'm also down if you have any suggestions for alternatives. My goal is just to have a nice consistent api for whatever frontend clients are calling this.

woylie commented 1 week ago

Hi @mattmatters,

if you select the condition you order by in the same query, you can use an alias field: https://hexdocs.pm/flop/Flop.Schema.html#module-alias-fields. Would that work for you in this case?

We can definitely add support for custom order fields as well. I don't think we'd need an additional parameter, though. We can just extend the existing custom field options, as proposed. I'd go with an MFA tuple for consistency.

@derive {
  Flop.Schema,
  filterable: [:my_custom_field],
  sortable: [:my_custom_field],
  adapter_opts: [
    custom_fields: [
      my_custom_field: [
        filter: {MyModule, :filter_by_custom_field, []},
        sorter: {MyModule, :order_by_custom_field, []},
        ecto_type: :date,
        operators: [:<=, :>=]
      ]
    ]
  ]
}

I don't think we need a new struct for this. The sorter function can have a signature like sorter(Ecto.Query.t(), Flop.order_direction(), keyword) :: Ecto.Query.t(). The third argument would be the compile time options as defined in the schema definition above, plus runtime options passed as extra_opts (same as already done with the filter function).

For compile-time validation:

Would you be up for a PR?

mattmatters commented 5 days ago

What a great idea! Custom fields make perfect sense. Yeah I would love to give a crack on it over the week!