defmodule JSONAPIPlug.QueryParser.Ecto.Include do
@moduledoc """
JSON:API 'include' query parameter parser implementation for Ecto
Expects `include` parameter to be in the [JSON:API include](https://jsonapi.org/format/#fetching-includes)
format and converts them to Ecto `preload` optio to `Ecto.Repo` functions.
"""
alias JSONAPIPlug.{Exceptions.InvalidQuery, QueryParser, Resource}
@behaviour QueryParser
@impl QueryParser
def parse(_jsonapi, nil), do: []
def parse(%JSONAPIPlug{resource: resource}, include) when is_binary(include) do
include
|> String.split(",", trim: true)
|> Enum.map(fn include ->
include
|> JSONAPIPlug.recase(:underscore)
|> String.split(".", trim: true)
end)
|> valid_includes(resource)
end
def parse(%JSONAPIPlug{resource: resource}, include) do
raise InvalidQuery, type: resource.type(), param: "include", value: include
end
defp valid_includes(includes, resource) do
relationships = resource.relationships()
valid_relationships_includes = Enum.map(relationships, &to_string(Resource.field_name(&1)))
Enum.reduce(
relationships,
[],
&process_relationship_include(resource, &1, includes, &2, valid_relationships_includes)
)
|> Keyword.merge([], fn _k, a, b -> Keyword.merge(a, b) end)
end
defp process_relationship_include(
resource,
relationship,
includes,
valid_includes,
valid_relationships_includes
) do
name = Resource.field_option(relationship, :name) || Resource.field_name(relationship)
include_name = to_string(name)
Enum.reduce(includes, [], fn
[^include_name], relationship_includes ->
update_in(
relationship_includes,
[name],
&Keyword.merge(&1 || [], [], fn _k, a, b -> Keyword.merge(a, b) end)
)
[^include_name | rest], relationship_includes ->
case Resource.field_option(relationship, :resource) do
nil ->
relationship_includes
related_resource ->
update_in(
relationship_includes,
[name],
&Keyword.merge(&1 || [], valid_includes([rest], related_resource))
)
end
[include_name | _] = path, relationship_includes ->
if include_name in valid_relationships_includes do
relationship_includes
else
raise InvalidQuery, type: resource.type(), param: "include", value: Enum.join(path, ".")
end
end)
|> Keyword.merge(valid_includes, fn _k, a, b -> Keyword.merge(a, b) end)
end
end