mirror of
https://github.com/ash-project/ash_json_api_wrapper.git
synced 2024-09-19 21:02:50 +12:00
improvement: paginators & better filter support
improvement: new spark_function_behaviour for `before_request`
This commit is contained in:
parent
92cf4fd8bf
commit
a9d94bdd9b
15 changed files with 409 additions and 147 deletions
|
@ -1,6 +1,7 @@
|
|||
spark_locals_without_parens = [
|
||||
base: 1,
|
||||
base_entity_path: 1,
|
||||
base_paginator: 1,
|
||||
before_request: 1,
|
||||
endpoint: 1,
|
||||
endpoint: 2,
|
||||
|
@ -13,6 +14,7 @@ spark_locals_without_parens = [
|
|||
get_endpoint: 2,
|
||||
get_endpoint: 3,
|
||||
limit_with: 1,
|
||||
paginator: 1,
|
||||
path: 1,
|
||||
runtime_sort?: 1,
|
||||
write_entity_path: 1,
|
||||
|
|
|
@ -92,6 +92,9 @@ defmodule AshJsonApiWrapper.DataLayer do
|
|||
@fields,
|
||||
@endpoints
|
||||
],
|
||||
imports: [
|
||||
AshJsonApiWrapper.Paginator.Builtins
|
||||
],
|
||||
schema: [
|
||||
before_request: [
|
||||
type:
|
||||
|
@ -108,6 +111,13 @@ defmodule AshJsonApiWrapper.DataLayer do
|
|||
Where in the response to find resulting entities. Can be overridden per endpoint.
|
||||
"""
|
||||
],
|
||||
base_paginator: [
|
||||
type:
|
||||
{:spark_behaviour, AshJsonApiWrapper.Paginator, AshJsonApiWrapper.Paginator.Builtins},
|
||||
doc: """
|
||||
A module implementing the `AshJSonApiWrapper.Paginator` behaviour, to allow scanning pages when reading.
|
||||
"""
|
||||
],
|
||||
finch: [
|
||||
type: :atom,
|
||||
required: true,
|
||||
|
@ -132,6 +142,7 @@ defmodule AshJsonApiWrapper.DataLayer do
|
|||
|
||||
defmodule Query do
|
||||
defstruct [
|
||||
:api,
|
||||
:request,
|
||||
:context,
|
||||
:headers,
|
||||
|
@ -139,6 +150,7 @@ defmodule AshJsonApiWrapper.DataLayer do
|
|||
:limit,
|
||||
:offset,
|
||||
:filter,
|
||||
:runtime_filter,
|
||||
:sort,
|
||||
:endpoint,
|
||||
:templates,
|
||||
|
@ -213,11 +225,73 @@ defmodule AshJsonApiWrapper.DataLayer do
|
|||
else
|
||||
if query.action do
|
||||
case validate_filter(filter, resource, query.action) do
|
||||
{:ok, {endpoint, templates, instructions}} ->
|
||||
{:ok, {endpoint, templates, instructions}, remaining_filter} ->
|
||||
{instructions, templates} =
|
||||
if templates && !Enum.empty?(templates) do
|
||||
{templates, instructions}
|
||||
else
|
||||
instructions =
|
||||
instructions
|
||||
|> Enum.reduce([], fn
|
||||
{:expand_set, field, values} = instruction, new_instructions ->
|
||||
if Enum.any?(new_instructions, fn
|
||||
{:expand_set, ^field, _other_values} ->
|
||||
true
|
||||
|
||||
_ ->
|
||||
false
|
||||
end) do
|
||||
Enum.map(new_instructions, fn
|
||||
{:expand_set, ^field, other_values} ->
|
||||
{:expand_set, field,
|
||||
other_values
|
||||
|> MapSet.new()
|
||||
|> MapSet.intersection(MapSet.new(values))
|
||||
|> MapSet.to_list()}
|
||||
|
||||
other ->
|
||||
other
|
||||
end)
|
||||
else
|
||||
[instruction | new_instructions]
|
||||
end
|
||||
|
||||
instruction, new_instructions ->
|
||||
[instruction | new_instructions]
|
||||
end)
|
||||
|
||||
{expand_set, instructions} =
|
||||
Enum.split_with(instructions || [], fn
|
||||
{:expand_set, _, _} ->
|
||||
true
|
||||
|
||||
_ ->
|
||||
false
|
||||
end)
|
||||
|
||||
templates =
|
||||
expand_set
|
||||
|> Enum.at(0)
|
||||
|> case do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
{:expand_set, field, values} ->
|
||||
Enum.map(values, &{:set, field, &1})
|
||||
end
|
||||
|
||||
{instructions, templates}
|
||||
end
|
||||
|
||||
new_query_params =
|
||||
Enum.reduce(instructions, query.request.query || %{}, fn
|
||||
{:simple, field, value}, query ->
|
||||
Map.put(query, to_string(field), value)
|
||||
Enum.reduce(instructions || [], query.request.query || %{}, fn
|
||||
{:set, field, value}, query ->
|
||||
field =
|
||||
field
|
||||
|> List.wrap()
|
||||
|> Enum.map(&to_string/1)
|
||||
|
||||
AshJsonApiWrapper.Helpers.put_at_path(query, field, value)
|
||||
|
||||
{:place_in_list, path, value}, query ->
|
||||
update_in!(query, path, [], &[value | &1])
|
||||
|
@ -228,6 +302,7 @@ defmodule AshJsonApiWrapper.DataLayer do
|
|||
query
|
||||
| endpoint: endpoint,
|
||||
templates: templates,
|
||||
runtime_filter: remaining_filter,
|
||||
request: %{query.request | query: new_query_params}
|
||||
}}
|
||||
|
||||
|
@ -267,6 +342,7 @@ defmodule AshJsonApiWrapper.DataLayer do
|
|||
%{
|
||||
query
|
||||
| request: %{query.request | query: params, headers: headers},
|
||||
api: query.api,
|
||||
action: action,
|
||||
headers: headers,
|
||||
context: context
|
||||
|
@ -274,26 +350,20 @@ defmodule AshJsonApiWrapper.DataLayer do
|
|||
end
|
||||
|
||||
defp validate_filter(filter, resource, action) when filter in [nil, true] do
|
||||
{:ok, {AshJsonApiWrapper.DataLayer.Info.endpoint(resource, action.name), nil, []}}
|
||||
{:ok, {AshJsonApiWrapper.DataLayer.Info.endpoint(resource, action.name), nil, []}, filter}
|
||||
end
|
||||
|
||||
defp validate_filter(filter, resource, action) do
|
||||
case AshJsonApiWrapper.Filter.find_filter_that_uses_get_endpoint(filter, resource, action) do
|
||||
{:ok, {remaining_filter, get_endpoint, templates}} ->
|
||||
case filter_instructions(remaining_filter, resource, get_endpoint) do
|
||||
{:ok, instructions} ->
|
||||
{:ok, {get_endpoint, templates, instructions}}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
{:ok, {get_endpoint, templates, []}, remaining_filter}
|
||||
|
||||
{:ok, nil} ->
|
||||
endpoint = AshJsonApiWrapper.DataLayer.Info.endpoint(resource, action.name)
|
||||
|
||||
case filter_instructions(filter, resource, endpoint) do
|
||||
{:ok, instructions} ->
|
||||
{:ok, {endpoint, nil, instructions}}
|
||||
{:ok, instructions, remaining_filter} ->
|
||||
{:ok, {endpoint, nil, instructions}, remaining_filter}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
@ -304,25 +374,40 @@ defmodule AshJsonApiWrapper.DataLayer do
|
|||
end
|
||||
end
|
||||
|
||||
defp filter_instructions(filter, resource, endpoint) do
|
||||
base_fields =
|
||||
resource
|
||||
|> AshJsonApiWrapper.DataLayer.Info.fields()
|
||||
|> Map.new(&{&1.name, &1})
|
||||
|
||||
defp filter_instructions(filter, _resource, endpoint) do
|
||||
fields =
|
||||
endpoint.fields
|
||||
|> Enum.reduce(base_fields, fn field, acc ->
|
||||
Map.put(acc, field.name, field)
|
||||
end)
|
||||
|> Map.values()
|
||||
|> List.wrap()
|
||||
|> Enum.filter(& &1.filter_handler)
|
||||
|
||||
Enum.reduce_while(fields, {:ok, [], filter}, fn field, {:ok, instructions, filter} ->
|
||||
result =
|
||||
case field.filter_handler do
|
||||
:simple ->
|
||||
AshJsonApiWrapper.Filter.find_simple_filter(filter, field)
|
||||
AshJsonApiWrapper.Filter.find_simple_filter(filter, field.name)
|
||||
|
||||
{:simple, path} ->
|
||||
case AshJsonApiWrapper.Filter.find_simple_filter(filter, field.name) do
|
||||
{:ok, {remaining_filter, new_instructions}} ->
|
||||
field_name = field.name
|
||||
|
||||
{:ok,
|
||||
{remaining_filter,
|
||||
Enum.map(new_instructions, fn
|
||||
{:set, ^field_name, value} ->
|
||||
{:set, path, value}
|
||||
|
||||
{:expand_set, ^field_name, values} ->
|
||||
{:expand_set, path, values}
|
||||
|
||||
other ->
|
||||
# don't think this is possible
|
||||
other
|
||||
end)}}
|
||||
|
||||
other ->
|
||||
other
|
||||
end
|
||||
|
||||
{:place_in_list, path} ->
|
||||
AshJsonApiWrapper.Filter.find_place_in_list_filter(
|
||||
|
@ -338,18 +423,14 @@ defmodule AshJsonApiWrapper.DataLayer do
|
|||
|
||||
{:ok, {remaining_filter, new_instructions}} ->
|
||||
{:cont, {:ok, new_instructions ++ instructions, remaining_filter}}
|
||||
|
||||
{:error, error} ->
|
||||
{:halt, {:error, error}}
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
{:ok, instructions, nil} ->
|
||||
{:ok, instructions}
|
||||
{:ok, instructions, nil}
|
||||
|
||||
{:ok, _instructions, remaining_filter} ->
|
||||
{:error,
|
||||
"Some part of the provided filter statement was not processes: #{inspect(remaining_filter)}"}
|
||||
{:ok, instructions, remaining_filter} ->
|
||||
{:ok, instructions, remaining_filter}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
@ -436,11 +517,13 @@ defmodule AshJsonApiWrapper.DataLayer do
|
|||
)
|
||||
|> Map.put(:query, params)
|
||||
|
||||
with {:ok, %{status: status} = response} when status >= 200 and status < 300 <-
|
||||
request(request, changeset, resource, endpoint.path),
|
||||
with request <- request(request, changeset, resource, endpoint.path),
|
||||
{:ok, %{status: status} = response} when status >= 200 and status < 300 <-
|
||||
do_request(request, resource),
|
||||
{:ok, body} <- Jason.decode(response.body),
|
||||
{:ok, entities} <- get_entities(body, endpoint),
|
||||
{:ok, processed} <- process_entities(entities, resource, endpoint) do
|
||||
{:ok, entities} <- get_entities(body, endpoint, resource),
|
||||
{:ok, processed} <-
|
||||
process_entities(entities, resource, endpoint) do
|
||||
{:ok, Enum.at(processed, 0)}
|
||||
else
|
||||
{:ok, %{status: status} = response} ->
|
||||
|
@ -486,11 +569,12 @@ defmodule AshJsonApiWrapper.DataLayer do
|
|||
def run_query(query, resource, overridden?) do
|
||||
if query.templates do
|
||||
query.templates
|
||||
|> Enum.uniq()
|
||||
|> Task.async_stream(
|
||||
fn template ->
|
||||
query = %{
|
||||
query
|
||||
| request: Finch.build(:get, fill_template(query.endpoint.path, template)),
|
||||
| request: fill_template(query.request, template),
|
||||
templates: nil
|
||||
}
|
||||
|
||||
|
@ -549,10 +633,11 @@ defmodule AshJsonApiWrapper.DataLayer do
|
|||
query
|
||||
end
|
||||
|
||||
with {:ok, %{status: status} = response} when status >= 200 and status < 300 <-
|
||||
request(query.request, query.context, resource, path),
|
||||
with request <- request(query.request, query.context, resource, path),
|
||||
{:ok, %{status: status} = response} when status >= 200 and status < 300 <-
|
||||
do_request(request, resource),
|
||||
{:ok, body} <- Jason.decode(response.body),
|
||||
{:ok, entities} <- get_entities(body, endpoint) do
|
||||
{:ok, entities} <- get_entities(body, endpoint, resource, paginate_with: request) do
|
||||
entities
|
||||
|> limit_offset(query)
|
||||
|> process_entities(resource, endpoint)
|
||||
|
@ -566,6 +651,19 @@ defmodule AshJsonApiWrapper.DataLayer do
|
|||
end
|
||||
end
|
||||
|> do_sort(query)
|
||||
|> runtime_filter(query)
|
||||
end
|
||||
|
||||
defp runtime_filter({:ok, results}, query) do
|
||||
if not is_nil(query.runtime_filter) do
|
||||
Ash.Filter.Runtime.filter_matches(query.api, results, query.runtime_filter)
|
||||
else
|
||||
{:ok, results}
|
||||
end
|
||||
end
|
||||
|
||||
defp runtime_filter(other, _) do
|
||||
other
|
||||
end
|
||||
|
||||
defp do_sort({:ok, results}, %{sort: sort}) when sort not in [nil, []] do
|
||||
|
@ -574,11 +672,23 @@ defmodule AshJsonApiWrapper.DataLayer do
|
|||
|
||||
defp do_sort(other, _), do: other
|
||||
|
||||
defp fill_template(string, template) do
|
||||
defp fill_template(request, template) do
|
||||
template
|
||||
|> List.wrap()
|
||||
|> Enum.reduce(string, fn {key, replacement}, acc ->
|
||||
String.replace(acc, ":#{key}", to_string(replacement))
|
||||
|> Enum.reduce(request, fn
|
||||
{key, replacement}, request ->
|
||||
%{
|
||||
request
|
||||
| path: String.replace(request.path, ":#{key}", to_string(replacement))
|
||||
}
|
||||
|
||||
{:set, key, value}, request ->
|
||||
key =
|
||||
key
|
||||
|> List.wrap()
|
||||
|> Enum.map(&to_string/1)
|
||||
|
||||
%{request | query: AshJsonApiWrapper.Helpers.put_at_path(request.query, key, value)}
|
||||
end)
|
||||
end
|
||||
|
||||
|
@ -600,27 +710,24 @@ defmodule AshJsonApiWrapper.DataLayer do
|
|||
defp request(request, query_or_changeset, resource, path) do
|
||||
case AshJsonApiWrapper.DataLayer.Info.before_request(resource) do
|
||||
{module, opts} ->
|
||||
request
|
||||
|> Map.put(:path, path)
|
||||
|> module.(query_or_changeset, opts)
|
||||
|> encode_query()
|
||||
|> encode_body()
|
||||
|> log_send()
|
||||
|> make_request(AshJsonApiWrapper.DataLayer.Info.finch(resource))
|
||||
|> log_resp()
|
||||
module.call(Map.put(request, :path, path), query_or_changeset, opts)
|
||||
|
||||
nil ->
|
||||
request
|
||||
|> Map.put(:path, path)
|
||||
|> encode_query()
|
||||
|> encode_body()
|
||||
|> log_send()
|
||||
|> make_request(AshJsonApiWrapper.DataLayer.Info.finch(resource))
|
||||
|> log_resp()
|
||||
end
|
||||
end
|
||||
|
||||
def make_request(request, finch) do
|
||||
defp do_request(request, resource) do
|
||||
request
|
||||
|> encode_query()
|
||||
|> encode_body()
|
||||
|> log_send()
|
||||
|> make_request(AshJsonApiWrapper.DataLayer.Info.finch(resource))
|
||||
|> log_resp()
|
||||
end
|
||||
|
||||
defp make_request(request, finch) do
|
||||
case Finch.request(request, finch) do
|
||||
{:ok, %{status: code, headers: headers} = response} when code >= 300 and code < 400 ->
|
||||
headers
|
||||
|
@ -770,18 +877,68 @@ defmodule AshJsonApiWrapper.DataLayer do
|
|||
end
|
||||
end
|
||||
|
||||
defp get_entities(body, endpoint) do
|
||||
case endpoint.entity_path do
|
||||
nil ->
|
||||
{:ok, List.wrap(body)}
|
||||
defp get_entities(body, endpoint, resource, opts \\ []) do
|
||||
if opts[:paginate_with] && endpoint.paginator do
|
||||
with {:ok, entities} <-
|
||||
get_entities(body, endpoint, resource, Keyword.delete(opts, :paginate_with)),
|
||||
{:ok, bodies} <-
|
||||
get_all_bodies(
|
||||
body,
|
||||
endpoint,
|
||||
resource,
|
||||
opts[:paginate_with],
|
||||
&get_entities(&1, endpoint, resource, Keyword.delete(opts, :paginate_with)),
|
||||
[entities]
|
||||
) do
|
||||
{:ok, bodies |> Enum.reverse() |> List.flatten()}
|
||||
end
|
||||
else
|
||||
case endpoint.entity_path do
|
||||
nil ->
|
||||
{:ok, List.wrap(body)}
|
||||
|
||||
path ->
|
||||
case ExJSONPath.eval(body, path) do
|
||||
{:ok, [entities | _]} ->
|
||||
{:ok, List.wrap(entities)}
|
||||
path ->
|
||||
case ExJSONPath.eval(body, path) do
|
||||
{:ok, [entities | _]} ->
|
||||
{:ok, List.wrap(entities)}
|
||||
|
||||
{:ok, _} ->
|
||||
{:ok, []}
|
||||
{:ok, _} ->
|
||||
{:ok, []}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp get_all_bodies(
|
||||
body,
|
||||
%{paginator: {module, opts}} = endpoint,
|
||||
resource,
|
||||
request,
|
||||
entity_callback,
|
||||
bodies
|
||||
) do
|
||||
case module.continue(body, Enum.at(bodies, 0), request, opts) do
|
||||
:halt ->
|
||||
{:ok, bodies}
|
||||
|
||||
{:ok, instructions} ->
|
||||
request = apply_instructions(request, instructions)
|
||||
|
||||
case do_request(request, resource) do
|
||||
{:ok, %{status: status} = response} when status >= 200 and status < 300 ->
|
||||
with {:ok, new_body} <- Jason.decode(response.body),
|
||||
{:ok, entities} <- entity_callback.(new_body) do
|
||||
get_all_bodies(new_body, endpoint, resource, request, entity_callback, [
|
||||
entities | bodies
|
||||
])
|
||||
end
|
||||
|
||||
{:ok, %{status: status} = response} ->
|
||||
{:error,
|
||||
"Received status code #{status} in request #{inspect(request)}. Response: #{inspect(response)}"}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
@ -789,6 +946,32 @@ defmodule AshJsonApiWrapper.DataLayer do
|
|||
end
|
||||
end
|
||||
|
||||
defp apply_instructions(request, instructions) do
|
||||
request
|
||||
|> apply_params(instructions)
|
||||
|> apply_headers(instructions)
|
||||
end
|
||||
|
||||
defp apply_params(request, %{params: params}) when is_map(params) do
|
||||
%{request | query: Ash.Helpers.deep_merge_maps(request.query || %{}, params)}
|
||||
end
|
||||
|
||||
defp apply_params(request, _), do: request
|
||||
|
||||
defp apply_headers(request, %{headers: headers}) when is_map(headers) do
|
||||
%{
|
||||
request
|
||||
| headers:
|
||||
request.headers
|
||||
|> Kernel.||(%{})
|
||||
|> Map.new()
|
||||
|> Map.merge(headers)
|
||||
|> Map.to_list()
|
||||
}
|
||||
end
|
||||
|
||||
defp apply_headers(request, _), do: request
|
||||
|
||||
defp get_field(resource, endpoint, field) do
|
||||
Enum.find(endpoint.fields || [], &(&1.name == field)) ||
|
||||
Enum.find(AshJsonApiWrapper.DataLayer.Info.fields(resource), &(&1.name == field))
|
||||
|
|
|
@ -3,39 +3,44 @@ defmodule AshJsonApiWrapper.DataLayer.Info do
|
|||
|
||||
alias Spark.Dsl.Extension
|
||||
|
||||
@spec endpoint_base(Ash.Resource.t()) :: String.t() | nil
|
||||
@spec endpoint_base(map | Ash.Resource.t()) :: String.t() | nil
|
||||
def endpoint_base(resource) do
|
||||
Extension.get_opt(resource, [:json_api_wrapper, :endpoints], :base, nil, false)
|
||||
end
|
||||
|
||||
@spec finch(Ash.Resource.t()) :: module | nil
|
||||
@spec finch(map | Ash.Resource.t()) :: module | nil
|
||||
def finch(resource) do
|
||||
Extension.get_opt(resource, [:json_api_wrapper], :finch, nil, false)
|
||||
end
|
||||
|
||||
@spec base_entity_path(Ash.Resource.t()) :: String.t() | nil
|
||||
@spec base_entity_path(map | Ash.Resource.t()) :: String.t() | nil
|
||||
def base_entity_path(resource) do
|
||||
Extension.get_opt(resource, [:json_api_wrapper], :base_entity_path, nil, false)
|
||||
end
|
||||
|
||||
@spec before_request(Ash.Resource.t()) :: AshJsonApiWrapper.Finch.Plug.ref() | nil
|
||||
@spec base_paginator(map | Ash.Resource.t()) :: AshJsonApiWrapper.Paginator.ref()
|
||||
def base_paginator(resource) do
|
||||
Extension.get_opt(resource, [:json_api_wrapper], :base_paginator, nil, false)
|
||||
end
|
||||
|
||||
@spec before_request(map | Ash.Resource.t()) :: AshJsonApiWrapper.Finch.Plug.ref() | nil
|
||||
def before_request(resource) do
|
||||
Extension.get_opt(resource, [:json_api_wrapper], :before_request, nil)
|
||||
end
|
||||
|
||||
@spec field(Ash.Resource.t(), atom) :: AshJsonApiWrapper.Field.t() | nil
|
||||
@spec field(map | Ash.Resource.t(), atom) :: AshJsonApiWrapper.Field.t() | nil
|
||||
def field(resource, name) do
|
||||
resource
|
||||
|> fields()
|
||||
|> Enum.find(&(&1.name == name))
|
||||
end
|
||||
|
||||
@spec fields(Ash.Resource.t()) :: list(AshJsonApiWrapper.Field.t())
|
||||
@spec fields(map | Ash.Resource.t()) :: list(AshJsonApiWrapper.Field.t())
|
||||
def fields(resource) do
|
||||
Extension.get_entities(resource, [:json_api_wrapper, :fields])
|
||||
end
|
||||
|
||||
@spec endpoint(Ash.Resource.t(), atom) :: AshJsonApiWrapper.Endpoint.t() | nil
|
||||
@spec endpoint(map | Ash.Resource.t(), atom) :: AshJsonApiWrapper.Endpoint.t() | nil
|
||||
def endpoint(resource, action) do
|
||||
default_endpoint = AshJsonApiWrapper.Endpoint.default(resource)
|
||||
|
||||
|
@ -56,7 +61,7 @@ defmodule AshJsonApiWrapper.DataLayer.Info do
|
|||
end
|
||||
end
|
||||
|
||||
@spec get_endpoint(Ash.Resource.t(), atom, atom) :: AshJsonApiWrapper.Endpoint.t() | nil
|
||||
@spec get_endpoint(map | Ash.Resource.t(), atom, atom) :: AshJsonApiWrapper.Endpoint.t() | nil
|
||||
def get_endpoint(resource, action, get_for) do
|
||||
default_endpoint = AshJsonApiWrapper.Endpoint.default(resource)
|
||||
|
||||
|
@ -78,7 +83,7 @@ defmodule AshJsonApiWrapper.DataLayer.Info do
|
|||
end
|
||||
end
|
||||
|
||||
@spec endpoints(Ash.Resource.t()) :: list(AshJsonApiWrapper.Endpoint.t())
|
||||
@spec endpoints(map | Ash.Resource.t()) :: list(AshJsonApiWrapper.Endpoint.t())
|
||||
def endpoints(resource) do
|
||||
Extension.get_entities(resource, [:json_api_wrapper, :endpoints])
|
||||
end
|
||||
|
|
|
@ -3,26 +3,30 @@ defmodule AshJsonApiWrapper.DataLayer.Transformers.SetEndpointDefaults do
|
|||
|
||||
alias Spark.Dsl.Transformer
|
||||
|
||||
@impl Spark.Dsl.Transformer
|
||||
def transform(dsl) do
|
||||
base_entity_path = AshJsonApiWrapper.DataLayer.Info.base_entity_path(dsl) || nil
|
||||
base_entity_path = AshJsonApiWrapper.DataLayer.Info.base_entity_path(dsl)
|
||||
base_paginator = AshJsonApiWrapper.DataLayer.Info.base_paginator(dsl)
|
||||
base_fields = AshJsonApiWrapper.DataLayer.Info.fields(dsl)
|
||||
|
||||
dsl
|
||||
|> AshJsonApiWrapper.DataLayer.Info.endpoints()
|
||||
|> Enum.reduce({:ok, dsl}, fn endpoint, {:ok, dsl} ->
|
||||
if endpoint.entity_path || is_nil(base_entity_path) do
|
||||
{:ok, dsl}
|
||||
else
|
||||
{:ok,
|
||||
Transformer.replace_entity(
|
||||
dsl,
|
||||
[:ash_json_api_wrapper, :endpoint],
|
||||
%{
|
||||
endpoint
|
||||
| entity_path: base_entity_path
|
||||
},
|
||||
&(&1.action == endpoint.action)
|
||||
)}
|
||||
end
|
||||
endpoint_field_names = Enum.map(endpoint.fields, & &1.name)
|
||||
|
||||
{:ok,
|
||||
Transformer.replace_entity(
|
||||
dsl,
|
||||
[:ash_json_api_wrapper, :endpoint],
|
||||
%{
|
||||
endpoint
|
||||
| entity_path: endpoint.entity_path || base_entity_path,
|
||||
paginator: endpoint.paginator || base_paginator,
|
||||
fields:
|
||||
Enum.reject(base_fields, &(&1.name in endpoint_field_names)) ++ endpoint.fields
|
||||
},
|
||||
&(&1.action == endpoint.action)
|
||||
)}
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,7 +8,8 @@ defmodule AshJsonApiWrapper.Endpoint do
|
|||
:write_entity_path,
|
||||
:get_for,
|
||||
:runtime_sort?,
|
||||
:limit_with
|
||||
:limit_with,
|
||||
:paginator
|
||||
]
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
@ -48,6 +49,12 @@ defmodule AshJsonApiWrapper.Endpoint do
|
|||
default: false,
|
||||
doc:
|
||||
"Whether or not this endpoint should support sorting at runtime after the data has been received."
|
||||
],
|
||||
paginator: [
|
||||
type:
|
||||
{:spark_behaviour, AshJsonApiWrapper.Paginator, AshJsonApiWrapper.Paginator.Builtins},
|
||||
doc:
|
||||
"A module implementing the `AshJSonApiWrapper.Paginator` behaviour, to allow scanning pages when reading."
|
||||
]
|
||||
]
|
||||
end
|
||||
|
@ -70,6 +77,8 @@ defmodule AshJsonApiWrapper.Endpoint do
|
|||
%__MODULE__{
|
||||
path: AshJsonApiWrapper.DataLayer.Info.endpoint_base(resource),
|
||||
entity_path: AshJsonApiWrapper.DataLayer.Info.base_entity_path(resource),
|
||||
paginator: AshJsonApiWrapper.DataLayer.Info.base_paginator(resource),
|
||||
fields: AshJsonApiWrapper.DataLayer.Info.fields(resource),
|
||||
fields_in: :body
|
||||
}
|
||||
end
|
||||
|
|
|
@ -24,7 +24,8 @@ defmodule AshJsonApiWrapper.Field do
|
|||
Specification for how the field is handled when used in filters. This is relatively limited at the moment.
|
||||
|
||||
Supports the following:
|
||||
* `:simple` - Sets the value directly into the query params. Does not support `or equals` or `in` filters.
|
||||
* `:simple` - Sets the value directly into the query params.
|
||||
* `{:simple, "key" | ["path", "to", "key"]}` - Sets the value directly into the query params using the provided key.
|
||||
* `{:place_in_list, ["path", "to", "list"]}` - Supports `or equals` and `in` filters over the given field, by placing their values in the provided list.
|
||||
"""
|
||||
]
|
||||
|
|
|
@ -1,64 +1,51 @@
|
|||
defmodule AshJsonApiWrapper.Filter do
|
||||
@moduledoc false
|
||||
|
||||
def find_simple_filter(
|
||||
filter,
|
||||
field,
|
||||
context \\ %{in_an_or?: false, other_branch_instructions: nil}
|
||||
)
|
||||
|
||||
def find_simple_filter(%Ash.Filter{expression: expression}, field, context) do
|
||||
find_simple_filter(expression, field, context)
|
||||
def find_simple_filter(%Ash.Filter{expression: expression}, field) do
|
||||
find_simple_filter(expression, field)
|
||||
end
|
||||
|
||||
def find_simple_filter(
|
||||
%Ash.Query.BooleanExpression{op: op, left: left, right: right} = expr,
|
||||
field,
|
||||
context
|
||||
%Ash.Query.BooleanExpression{op: :and, left: left, right: right} = expr,
|
||||
field
|
||||
) do
|
||||
case find_simple_filter(left, field, context) do
|
||||
case find_simple_filter(left, field) do
|
||||
{:ok, nil} ->
|
||||
case find_simple_filter(right, field, context) do
|
||||
case find_simple_filter(right, field) do
|
||||
{:ok, nil} ->
|
||||
{:ok, expr, []}
|
||||
|
||||
{:ok, {right_remaining, right_instructions}} ->
|
||||
{:ok, Ash.Query.BooleanExpression.new(op, left, right_remaining), right_instructions}
|
||||
{:ok, Ash.Query.BooleanExpression.new(:and, left, right_remaining),
|
||||
right_instructions}
|
||||
end
|
||||
|
||||
{:ok, {left_remaining, left_instructions}} ->
|
||||
case find_simple_filter(right, field, %{
|
||||
context
|
||||
| other_branch_instructions: left_instructions
|
||||
}) do
|
||||
case find_simple_filter(right, field) do
|
||||
{:ok, nil} ->
|
||||
{:ok, {Ash.Query.BooleanExpression.new(op, left_remaining, right), left_instructions}}
|
||||
{:ok,
|
||||
{Ash.Query.BooleanExpression.new(:and, left_remaining, right), left_instructions}}
|
||||
|
||||
{:ok, {right_remaining, right_instructions}} ->
|
||||
{:ok,
|
||||
{Ash.Query.BooleanExpression.new(op, left_remaining, right_remaining),
|
||||
{Ash.Query.BooleanExpression.new(:and, left_remaining, right_remaining),
|
||||
left_instructions ++ right_instructions}}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def find_simple_filter(
|
||||
%Ash.Query.Operator.Eq{left: left, right: %Ash.Query.Ref{} = right} = op,
|
||||
field,
|
||||
context
|
||||
field
|
||||
) do
|
||||
find_simple_filter(%{op | right: left, left: right}, field, context)
|
||||
find_simple_filter(%{op | right: left, left: right}, field)
|
||||
end
|
||||
|
||||
def find_simple_filter(
|
||||
%Ash.Query.Operator.Eq{
|
||||
left: %Ash.Query.Ref{relationship_path: [], attribute: %{name: name}}
|
||||
},
|
||||
field,
|
||||
_context
|
||||
field
|
||||
)
|
||||
when name != field do
|
||||
{:ok, nil}
|
||||
|
@ -69,20 +56,23 @@ defmodule AshJsonApiWrapper.Filter do
|
|||
left: %Ash.Query.Ref{relationship_path: [], attribute: %{name: field}},
|
||||
right: value
|
||||
},
|
||||
field,
|
||||
context
|
||||
field
|
||||
) do
|
||||
if Enum.any?(context.other_branch_instructions, fn
|
||||
{:simple, other_field, other_value} ->
|
||||
other_field == field && other_value != value
|
||||
{:ok, {nil, [{:set, field, value}]}}
|
||||
end
|
||||
|
||||
_ ->
|
||||
false
|
||||
end) do
|
||||
{:error, "Would set a simple filter for #{field} with two different values"}
|
||||
else
|
||||
{:ok, {nil, [{:set, field, value}]}}
|
||||
end
|
||||
def find_simple_filter(
|
||||
%Ash.Query.Operator.In{
|
||||
left: %Ash.Query.Ref{relationship_path: [], attribute: %{name: field}},
|
||||
right: values
|
||||
},
|
||||
field
|
||||
) do
|
||||
{:ok, {nil, [{:expand_set, field, values}]}}
|
||||
end
|
||||
|
||||
def find_simple_filter(_, _) do
|
||||
{:ok, nil}
|
||||
end
|
||||
|
||||
def find_place_in_list_filter(
|
||||
|
@ -303,15 +293,14 @@ defmodule AshJsonApiWrapper.Filter do
|
|||
end
|
||||
|
||||
def find_filter_that_uses_get_endpoint(
|
||||
%Ash.Query.Operator.Eq{left: %Ash.Query.Ref{}, right: %Ash.Query.Ref{}},
|
||||
%Ash.Query.Operator.Eq{left: %Ash.Query.Ref{}, right: %Ash.Query.Ref{}} = expr,
|
||||
_,
|
||||
_,
|
||||
_,
|
||||
_,
|
||||
_
|
||||
uses_endpoint
|
||||
) do
|
||||
{:error,
|
||||
"References on both sides of operators not supported in ash_json_api_wrapper currently"}
|
||||
{:ok, {expr, uses_endpoint, nil}}
|
||||
end
|
||||
|
||||
def find_filter_that_uses_get_endpoint(
|
||||
|
|
|
@ -5,6 +5,8 @@ defmodule AshJsonApiWrapper.Finch.Plug do
|
|||
end
|
||||
end
|
||||
|
||||
@type ref :: {module, Keyword.t()}
|
||||
|
||||
@callback call(
|
||||
request :: Finch.Request.t(),
|
||||
query_or_changeset :: Ash.Changeset.t() | Ash.Query.t(),
|
||||
|
|
14
lib/helpers.ex
Normal file
14
lib/helpers.ex
Normal file
|
@ -0,0 +1,14 @@
|
|||
defmodule AshJsonApiWrapper.Helpers do
|
||||
@moduledoc false
|
||||
def put_at_path(_, [], value), do: value
|
||||
|
||||
def put_at_path(nil, [key | rest], value) do
|
||||
%{key => put_at_path(nil, rest, value)}
|
||||
end
|
||||
|
||||
def put_at_path(map, [key | rest], value) when is_map(map) do
|
||||
map
|
||||
|> Map.put_new(key, %{})
|
||||
|> Map.update!(key, &put_at_path(&1, rest, value))
|
||||
end
|
||||
end
|
9
lib/paginator/builtins.ex
Normal file
9
lib/paginator/builtins.ex
Normal file
|
@ -0,0 +1,9 @@
|
|||
defmodule AshJsonApiWrapper.Paginator.Builtins do
|
||||
@moduledoc "Builtin paginators"
|
||||
|
||||
@spec continuation_property(String.t(), opts :: Keyword.t()) ::
|
||||
AshJsonApiWrapper.Paginator.ref()
|
||||
def continuation_property(get, opts) do
|
||||
{AshJsonApiWrapper.Paginator.ContinuationProperty, Keyword.put(opts, :get, get)}
|
||||
end
|
||||
end
|
24
lib/paginator/continuation_property.ex
Normal file
24
lib/paginator/continuation_property.ex
Normal file
|
@ -0,0 +1,24 @@
|
|||
defmodule AshJsonApiWrapper.Paginator.ContinuationProperty do
|
||||
use AshJsonApiWrapper.Paginator
|
||||
|
||||
def continue(_response, [], _, _), do: :halt
|
||||
|
||||
def continue(response, _entities, _request, opts) do
|
||||
case ExJSONPath.eval(response, opts[:get]) do
|
||||
{:ok, [value | _]} when not is_nil(value) ->
|
||||
if opts[:header] do
|
||||
{:ok, %{headers: %{opts[:header] => value}}}
|
||||
else
|
||||
if opts[:param] do
|
||||
{:ok,
|
||||
%{params: AshJsonApiWrapper.Helpers.put_at_path(%{}, List.wrap(opts[:param]), value)}}
|
||||
else
|
||||
:halt
|
||||
end
|
||||
end
|
||||
|
||||
_ ->
|
||||
:halt
|
||||
end
|
||||
end
|
||||
end
|
20
lib/paginator/paginator.ex
Normal file
20
lib/paginator/paginator.ex
Normal file
|
@ -0,0 +1,20 @@
|
|||
defmodule AshJsonApiWrapper.Paginator do
|
||||
@moduledoc """
|
||||
Behavior for scanning pages of a paginated endpoint.
|
||||
"""
|
||||
|
||||
@type ref :: {module, Keyword.t()}
|
||||
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
@behaviour AshJsonApiWrapper.Paginator
|
||||
end
|
||||
end
|
||||
|
||||
@callback continue(
|
||||
response :: term,
|
||||
entities :: [Ash.Resource.record()],
|
||||
request :: Finch.Request.t(),
|
||||
opts :: Keyword.t()
|
||||
) :: {:ok, %{optional(:params) => map, optional(:headers) => map}} | :halt
|
||||
end
|
2
mix.exs
2
mix.exs
|
@ -37,7 +37,7 @@ defmodule AshJsonApiWrapper.MixProject do
|
|||
{:credo, ">= 0.0.0", only: :dev, runtime: false},
|
||||
{:dialyxir, ">= 0.0.0", only: :dev, runtime: false},
|
||||
{:sobelow, ">= 0.0.0", only: :dev, runtime: false},
|
||||
{:git_ops, "~> 2.4.4", only: :dev},
|
||||
{:git_ops, "~> 2.5", only: :dev},
|
||||
{:excoveralls, "~> 0.13.0", only: [:dev, :test]},
|
||||
{:mix_test_watch, "~> 1.0", only: :dev, runtime: false},
|
||||
{:parse_trans, "3.3.0", only: [:dev, :test], override: true}
|
||||
|
|
8
mix.lock
8
mix.lock
|
@ -1,11 +1,11 @@
|
|||
%{
|
||||
"ash": {:hex, :ash, "2.5.2", "018dfaeb78b97810dfbc12d13842dd3fe4b8a797deebd4b9fbe32a9cd4471238", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: true]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: false]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:spark, ">= 0.3.0", [hex: :spark, repo: "hexpm", optional: false]}, {:stream_data, "~> 0.5.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69df569a557f55bb0479cf270ecaf80ef58780fa607e118e2889af1ae4448b45"},
|
||||
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
|
||||
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
|
||||
"castore": {:hex, :castore, "0.1.22", "4127549e411bedd012ca3a308dede574f43819fe9394254ca55ab4895abfa1a2", [:mix], [], "hexpm", "c17576df47eb5aa1ee40cc4134316a99f5cad3e215d5c77b8dd3cfef12a22cac"},
|
||||
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
|
||||
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
||||
"comparable": {:hex, :comparable, "1.0.0", "bb669e91cedd14ae9937053e5bcbc3c52bb2f22422611f43b6e38367d94a495f", [:mix], [{:typable, "~> 0.1", [hex: :typable, repo: "hexpm", optional: false]}], "hexpm", "277c11eeb1cd726e7cd41c6c199e7e52fa16ee6830b45ad4cdc62e51f62eb60c"},
|
||||
"credo": {:hex, :credo, "1.5.6", "e04cc0fdc236fefbb578e0c04bd01a471081616e741d386909e527ac146016c6", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "4b52a3e558bd64e30de62a648518a5ea2b6e3e5d2b164ef5296244753fc7eb17"},
|
||||
"credo": {:hex, :credo, "1.6.7", "323f5734350fd23a456f2688b9430e7d517afb313fbd38671b8a4449798a7854", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "41e110bfb007f7eda7f897c10bf019ceab9a0b269ce79f015d54b0dcf4fc7dd3"},
|
||||
"decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
|
||||
"dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"},
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.16", "607709303e1d4e3e02f1444df0c821529af1c03b8578dfc81bb9cf64553d02b9", [:mix], [], "hexpm", "69fcf696168f5a274dd012e3e305027010658b2d1630cef68421d6baaeaccead"},
|
||||
|
@ -21,7 +21,7 @@
|
|||
"finch": {:hex, :finch, "0.14.0", "619bfdee18fc135190bf590356c4bf5d5f71f916adb12aec94caa3fa9267a4bc", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5459acaf18c4fdb47a8c22fb3baff5d8173106217c8e56c5ba0b93e66501a8dd"},
|
||||
"gettext": {:hex, :gettext, "0.19.1", "564953fd21f29358e68b91634799d9d26989f8d039d7512622efb3c3b1c97892", [:mix], [], "hexpm", "10c656c0912b8299adba9b061c06947511e3f109ab0d18b44a866a4498e77222"},
|
||||
"git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"},
|
||||
"git_ops": {:hex, :git_ops, "2.4.5", "185a724dfde3745edd22f7571d59c47a835cf54ded67e9ccbc951920b7eec4c2", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e323a5b01ad53bc8c19c3a444be3e61ed7803ecd2e95530446ae9327d0143ecc"},
|
||||
"git_ops": {:hex, :git_ops, "2.5.4", "1f303c9952eccfc183631b7c3cceeb6604cb641a40dd29269bcd622416395de9", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "85d1fe718cacad67a7ca1e9e809a6cbb9771c2e9238c96e9aebbb286114f64e0"},
|
||||
"hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"},
|
||||
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
|
||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||
|
@ -35,7 +35,7 @@
|
|||
"mint": {:hex, :mint, "1.4.2", "50330223429a6e1260b2ca5415f69b0ab086141bc76dc2fbf34d7c389a6675b2", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "ce75a5bbcc59b4d7d8d70f8b2fc284b1751ffb35c7b6a6302b5192f8ab4ddd80"},
|
||||
"mix_test_watch": {:hex, :mix_test_watch, "1.1.0", "330bb91c8ed271fe408c42d07e0773340a7938d8a0d281d57a14243eae9dc8c3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "52b6b1c476cbb70fd899ca5394506482f12e5f6b0d6acff9df95c7f1e0812ec3"},
|
||||
"nimble_options": {:hex, :nimble_options, "0.5.2", "42703307b924880f8c08d97719da7472673391905f528259915782bb346e0a1b", [:mix], [], "hexpm", "4da7f904b915fd71db549bcdc25f8d56f378ef7ae07dc1d372cbe72ba950dce0"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"},
|
||||
"nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
|
||||
"picosat_elixir": {:hex, :picosat_elixir, "0.2.3", "bf326d0f179fbb3b706bb2c15fbc367dacfa2517157d090fdfc32edae004c597", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f76c9db2dec9d2561ffaa9be35f65403d53e984e8cd99c832383b7ab78c16c66"},
|
||||
|
|
|
@ -159,11 +159,11 @@ defmodule AshJsonApiWrapper.Hackernews.Test do
|
|||
Finch.start_link(name: MyApp.Finch)
|
||||
|
||||
assert [top_story] =
|
||||
TopStory
|
||||
|> Ash.Query.limit(1)
|
||||
|> Ash.Query.load(story: :user)
|
||||
|> Api.read!()
|
||||
|> Enum.map(& &1.story)
|
||||
TopStory
|
||||
|> Ash.Query.limit(1)
|
||||
|> Ash.Query.load(story: :user)
|
||||
|> Api.read!()
|
||||
|> Enum.map(& &1.story)
|
||||
|
||||
assert is_binary(top_story.url)
|
||||
assert is_binary(top_story.title)
|
||||
|
|
Loading…
Reference in a new issue