2021-10-30 15:40:27 +13:00
|
|
|
defmodule AshJsonApiWrapper.DataLayer do
|
2022-09-16 05:06:29 +12:00
|
|
|
@field %Spark.Dsl.Entity{
|
2021-10-30 15:40:27 +13:00
|
|
|
name: :field,
|
|
|
|
target: AshJsonApiWrapper.Field,
|
|
|
|
schema: AshJsonApiWrapper.Field.schema(),
|
|
|
|
docs: """
|
|
|
|
Configure an individual field's behavior, for example its path in the response.
|
|
|
|
""",
|
|
|
|
args: [:name]
|
|
|
|
}
|
|
|
|
|
2022-09-16 05:06:29 +12:00
|
|
|
@fields %Spark.Dsl.Section{
|
2021-10-30 15:40:27 +13:00
|
|
|
name: :fields,
|
|
|
|
describe: "Contains configuration for individual fields in the response",
|
|
|
|
entities: [
|
|
|
|
@field
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2022-09-16 05:06:29 +12:00
|
|
|
@endpoint %Spark.Dsl.Entity{
|
2021-10-30 15:40:27 +13:00
|
|
|
name: :endpoint,
|
|
|
|
target: AshJsonApiWrapper.Endpoint,
|
|
|
|
schema: AshJsonApiWrapper.Endpoint.schema(),
|
|
|
|
docs: """
|
|
|
|
Configure the endpoint that a given action will use.
|
|
|
|
|
|
|
|
Accepts overrides for fields as well.
|
|
|
|
""",
|
|
|
|
entities: [
|
2021-11-04 11:53:51 +13:00
|
|
|
fields: [@field]
|
2021-10-30 15:40:27 +13:00
|
|
|
],
|
|
|
|
args: [:action]
|
|
|
|
}
|
|
|
|
|
2022-09-16 05:06:29 +12:00
|
|
|
@get_endpoint %Spark.Dsl.Entity{
|
2021-11-04 11:53:51 +13:00
|
|
|
name: :get_endpoint,
|
|
|
|
target: AshJsonApiWrapper.Endpoint,
|
|
|
|
schema: AshJsonApiWrapper.Endpoint.get_schema(),
|
|
|
|
docs: """
|
|
|
|
Configure the endpoint that a given action will use.
|
|
|
|
|
|
|
|
Accepts overrides for fields as well.
|
|
|
|
|
|
|
|
Expresses that this endpoint is used to fetch a single item.
|
|
|
|
Doing this will make the data layer support equality filters over that field when using that action.
|
|
|
|
If "in" or "or equals" is used, then multiple requests will be made in parallel to fetch
|
|
|
|
all of those records. However, keep in mind you can't combine a filter over one of these
|
|
|
|
fields with an `or` with anything other than *more* filters on this field. For example Doing this will make the data layer support equality filters over that field.
|
|
|
|
If "in" or "or equals" is used, then multiple requests will be made in parallel to fetch
|
|
|
|
all of those records. However, keep in mind you can't combine a filter over one of these
|
|
|
|
fields with an `or` with anything other than *more* filters on this field. For example,
|
|
|
|
`filter(resource, id == 1 or foo == true)`, since we wouldn't be able to turn this into
|
|
|
|
multiple requests to the get endpoint for `id`. If other filters are supported, they can be used
|
|
|
|
with `and`, e.g `filter(resource, id == 1 or id == 2 and other_supported_filter == true)`, since those
|
|
|
|
filters will be applied to each request.
|
|
|
|
|
2022-05-20 08:10:09 +12:00
|
|
|
Expects the field to be available in the path template, e.g with `get_for` of `:id`, path should contain `:id`, e.g
|
2021-11-04 11:53:51 +13:00
|
|
|
`/get/:id` or `/:id`,
|
|
|
|
`filter(resource, id == 1 or foo == true)`, since we wouldn't be able to turn this into
|
|
|
|
multiple requests to the get endpoint for `id`. If other filters are supported, they can be used
|
|
|
|
with `and`, e.g `filter(resource, id == 1 or id == 2 and other_supported_filter == true)`, since those
|
|
|
|
filters will be applied to each request.
|
|
|
|
|
2022-05-20 08:10:09 +12:00
|
|
|
Expects the field to be available in the path template, e.g with `get_for` of `:id`, path should contain `:id`, e.g
|
2021-11-04 11:53:51 +13:00
|
|
|
`/get/:id` or `/:id`
|
|
|
|
""",
|
|
|
|
entities: [
|
|
|
|
fields: [@field]
|
|
|
|
],
|
|
|
|
args: [:action, :get_for]
|
|
|
|
}
|
|
|
|
|
2022-09-16 05:06:29 +12:00
|
|
|
@endpoints %Spark.Dsl.Section{
|
2021-10-30 15:40:27 +13:00
|
|
|
name: :endpoints,
|
|
|
|
describe: "Contains the configuration for the endpoints used in each action",
|
|
|
|
schema: [
|
|
|
|
base: [
|
|
|
|
type: :string,
|
|
|
|
doc: "The base endpoint to which all relative urls provided will be appended."
|
|
|
|
]
|
|
|
|
],
|
|
|
|
entities: [
|
2021-11-04 11:53:51 +13:00
|
|
|
@endpoint,
|
|
|
|
@get_endpoint
|
2021-10-30 15:40:27 +13:00
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2022-09-16 05:06:29 +12:00
|
|
|
@json_api_wrapper %Spark.Dsl.Section{
|
2021-10-30 15:40:27 +13:00
|
|
|
name: :json_api_wrapper,
|
|
|
|
describe: "Contains the configuration for the json_api_wrapper data layer",
|
|
|
|
sections: [
|
|
|
|
@fields,
|
|
|
|
@endpoints
|
|
|
|
],
|
2023-01-15 19:07:41 +13:00
|
|
|
imports: [
|
|
|
|
AshJsonApiWrapper.Paginator.Builtins
|
|
|
|
],
|
2021-10-30 15:40:27 +13:00
|
|
|
schema: [
|
2023-01-15 07:28:21 +13:00
|
|
|
base_entity_path: [
|
|
|
|
type: :string,
|
|
|
|
doc: """
|
|
|
|
Where in the response to find resulting entities. Can be overridden per endpoint.
|
|
|
|
"""
|
|
|
|
],
|
2023-01-15 19:07:41 +13:00
|
|
|
base_paginator: [
|
|
|
|
type:
|
|
|
|
{:spark_behaviour, AshJsonApiWrapper.Paginator, AshJsonApiWrapper.Paginator.Builtins},
|
|
|
|
doc: """
|
|
|
|
A module implementing the `AshJSonApiWrapper.Paginator` behaviour, to allow scanning pages when reading.
|
|
|
|
"""
|
|
|
|
],
|
2023-07-01 07:21:45 +12:00
|
|
|
tesla: [
|
2021-10-30 15:40:27 +13:00
|
|
|
type: :atom,
|
2023-07-01 07:21:45 +12:00
|
|
|
default: AshJsonApiWrapper.DefaultTesla,
|
2021-10-30 15:40:27 +13:00
|
|
|
doc: """
|
2023-07-01 07:21:45 +12:00
|
|
|
The Tesla module to use.
|
2021-10-30 15:40:27 +13:00
|
|
|
"""
|
|
|
|
]
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2021-11-05 07:08:11 +13:00
|
|
|
require Logger
|
2023-01-15 07:28:21 +13:00
|
|
|
|
|
|
|
use Spark.Dsl.Extension,
|
|
|
|
sections: [@json_api_wrapper],
|
|
|
|
transformers: [AshJsonApiWrapper.DataLayer.Transformers.SetEndpointDefaults]
|
2021-10-30 15:40:27 +13:00
|
|
|
|
|
|
|
defmodule Query do
|
2021-11-04 11:53:51 +13:00
|
|
|
defstruct [
|
2023-01-15 19:07:41 +13:00
|
|
|
:api,
|
2023-01-15 07:28:21 +13:00
|
|
|
:context,
|
|
|
|
:headers,
|
2021-11-04 11:53:51 +13:00
|
|
|
:action,
|
|
|
|
:limit,
|
|
|
|
:offset,
|
|
|
|
:filter,
|
2023-01-15 19:07:41 +13:00
|
|
|
:runtime_filter,
|
2023-07-01 07:21:45 +12:00
|
|
|
:path,
|
|
|
|
:query_params,
|
|
|
|
:body,
|
2022-02-12 10:29:14 +13:00
|
|
|
:sort,
|
2021-11-04 11:53:51 +13:00
|
|
|
:endpoint,
|
|
|
|
:templates,
|
|
|
|
:override_results
|
|
|
|
]
|
2021-10-30 15:40:27 +13:00
|
|
|
end
|
|
|
|
|
|
|
|
@behaviour Ash.DataLayer
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
def can?(_, :create), do: true
|
2021-11-04 11:53:51 +13:00
|
|
|
def can?(_, :boolean_filter), do: true
|
|
|
|
def can?(_, :filter), do: true
|
|
|
|
def can?(_, :limit), do: true
|
|
|
|
def can?(_, :offset), do: true
|
2021-11-05 07:08:11 +13:00
|
|
|
|
|
|
|
def can?(
|
|
|
|
_,
|
|
|
|
{:filter_expr,
|
|
|
|
%Ash.Query.Operator.Eq{
|
|
|
|
left: %Ash.Query.Operator.Eq{left: %Ash.Query.Ref{}, right: %Ash.Query.Ref{}}
|
|
|
|
}}
|
|
|
|
),
|
|
|
|
do: false
|
|
|
|
|
|
|
|
def can?(
|
|
|
|
_,
|
|
|
|
{:filter_expr, %Ash.Query.Operator.Eq{right: %Ash.Query.Ref{}}}
|
|
|
|
),
|
|
|
|
do: true
|
|
|
|
|
|
|
|
def can?(
|
|
|
|
_,
|
|
|
|
{:filter_expr, %Ash.Query.Operator.Eq{left: %Ash.Query.Ref{}}}
|
|
|
|
),
|
|
|
|
do: true
|
|
|
|
|
2021-11-05 18:13:33 +13:00
|
|
|
def can?(
|
|
|
|
_,
|
|
|
|
{:filter_expr, %Ash.Query.Operator.In{right: %Ash.Query.Ref{}}}
|
|
|
|
),
|
|
|
|
do: false
|
|
|
|
|
|
|
|
def can?(
|
|
|
|
_,
|
|
|
|
{:filter_expr, %Ash.Query.Operator.In{left: %Ash.Query.Ref{}, right: %Ash.Query.Ref{}}}
|
|
|
|
),
|
|
|
|
do: false
|
|
|
|
|
|
|
|
def can?(
|
|
|
|
_,
|
|
|
|
{:filter_expr, %Ash.Query.Operator.In{left: %Ash.Query.Ref{}}}
|
|
|
|
),
|
|
|
|
do: true
|
|
|
|
|
2022-02-12 10:29:14 +13:00
|
|
|
def can?(_, :sort), do: true
|
|
|
|
def can?(_, {:sort, _}), do: true
|
2021-10-30 15:40:27 +13:00
|
|
|
def can?(_, _), do: false
|
|
|
|
|
|
|
|
@impl true
|
2023-07-01 07:21:45 +12:00
|
|
|
def resource_to_query(resource, api \\ nil) do
|
|
|
|
%Query{path: AshJsonApiWrapper.DataLayer.Info.endpoint_base(resource), api: api}
|
2021-10-30 15:40:27 +13:00
|
|
|
end
|
|
|
|
|
2021-11-04 11:53:51 +13:00
|
|
|
@impl true
|
|
|
|
def filter(query, filter, resource) do
|
2021-11-05 18:13:33 +13:00
|
|
|
if filter == false || match?(%Ash.Filter{expression: false}, filter) do
|
|
|
|
%{query | override_results: []}
|
2021-11-04 11:53:51 +13:00
|
|
|
else
|
2021-11-05 18:13:33 +13:00
|
|
|
if filter == nil || filter == true || match?(%Ash.Filter{expression: nil}, filter) do
|
|
|
|
{:ok, %{query | filter: filter}}
|
|
|
|
else
|
|
|
|
if query.action do
|
|
|
|
case validate_filter(filter, resource, query.action) do
|
2023-01-15 19:07:41 +13:00
|
|
|
{:ok, {endpoint, templates, instructions}, remaining_filter} ->
|
2023-07-01 07:21:45 +12:00
|
|
|
{templates, instructions} =
|
2023-01-15 19:07:41 +13:00
|
|
|
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
|
|
|
|
|
2023-07-01 07:21:45 +12:00
|
|
|
{templates, instructions}
|
2023-01-15 19:07:41 +13:00
|
|
|
end
|
|
|
|
|
2021-11-05 18:13:33 +13:00
|
|
|
new_query_params =
|
2023-07-01 07:21:45 +12:00
|
|
|
Enum.reduce(instructions || [], query.query_params || %{}, fn
|
2023-01-15 19:07:41 +13:00
|
|
|
{:set, field, value}, query ->
|
|
|
|
field =
|
|
|
|
field
|
|
|
|
|> List.wrap()
|
|
|
|
|> Enum.map(&to_string/1)
|
|
|
|
|
|
|
|
AshJsonApiWrapper.Helpers.put_at_path(query, field, value)
|
2021-11-05 18:13:33 +13:00
|
|
|
|
|
|
|
{:place_in_list, path, value}, query ->
|
|
|
|
update_in!(query, path, [], &[value | &1])
|
|
|
|
end)
|
|
|
|
|
|
|
|
{:ok,
|
|
|
|
%{
|
|
|
|
query
|
|
|
|
| endpoint: endpoint,
|
|
|
|
templates: templates,
|
2023-01-15 19:07:41 +13:00
|
|
|
runtime_filter: remaining_filter,
|
2023-07-01 07:21:45 +12:00
|
|
|
query_params: new_query_params
|
2021-11-05 18:13:33 +13:00
|
|
|
}}
|
|
|
|
|
|
|
|
{:error, error} ->
|
|
|
|
{:error, error}
|
|
|
|
end
|
|
|
|
else
|
|
|
|
{:ok, %{query | filter: filter}}
|
|
|
|
end
|
|
|
|
end
|
2021-11-04 11:53:51 +13:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-02-12 10:29:14 +13:00
|
|
|
@impl true
|
|
|
|
def sort(query, sort, _resource) when sort in [nil, []] do
|
|
|
|
{:ok, query}
|
|
|
|
end
|
|
|
|
|
|
|
|
def sort(query, sort, _resource) do
|
|
|
|
endpoint = query.endpoint
|
|
|
|
|
|
|
|
if endpoint.runtime_sort? do
|
|
|
|
{:ok, %{query | sort: sort}}
|
|
|
|
else
|
|
|
|
{:error, "Sorting is not supported"}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-10-30 15:40:27 +13:00
|
|
|
@impl true
|
|
|
|
def set_context(_resource, query, context) do
|
2023-01-15 07:28:21 +13:00
|
|
|
params = context[:data_layer][:query_params] || %{}
|
|
|
|
headers = Map.to_list(context[:data_layer][:headers] || %{})
|
2021-10-30 15:40:27 +13:00
|
|
|
|
2021-11-04 11:53:51 +13:00
|
|
|
action = context[:action]
|
|
|
|
|
2023-01-15 07:28:21 +13:00
|
|
|
{:ok,
|
|
|
|
%{
|
|
|
|
query
|
2023-07-01 07:21:45 +12:00
|
|
|
| query_params: params,
|
|
|
|
headers: headers,
|
2023-01-15 19:07:41 +13:00
|
|
|
api: query.api,
|
2023-01-15 07:28:21 +13:00
|
|
|
action: action,
|
|
|
|
context: context
|
|
|
|
}}
|
2021-10-30 15:40:27 +13:00
|
|
|
end
|
|
|
|
|
2021-11-04 11:53:51 +13:00
|
|
|
defp validate_filter(filter, resource, action) when filter in [nil, true] do
|
2023-01-15 19:07:41 +13:00
|
|
|
{:ok, {AshJsonApiWrapper.DataLayer.Info.endpoint(resource, action.name), nil, []}, filter}
|
2021-11-04 11:53:51 +13:00
|
|
|
end
|
|
|
|
|
|
|
|
defp validate_filter(filter, resource, action) do
|
2021-11-05 18:13:33 +13:00
|
|
|
case AshJsonApiWrapper.Filter.find_filter_that_uses_get_endpoint(filter, resource, action) do
|
2021-11-05 07:08:11 +13:00
|
|
|
{:ok, {remaining_filter, get_endpoint, templates}} ->
|
2023-01-15 19:07:41 +13:00
|
|
|
{:ok, {get_endpoint, templates, []}, remaining_filter}
|
2021-11-05 07:08:11 +13:00
|
|
|
|
|
|
|
{:ok, nil} ->
|
2022-09-16 05:06:29 +12:00
|
|
|
endpoint = AshJsonApiWrapper.DataLayer.Info.endpoint(resource, action.name)
|
2021-11-05 07:08:11 +13:00
|
|
|
|
2021-11-05 18:13:33 +13:00
|
|
|
case filter_instructions(filter, resource, endpoint) do
|
2023-01-15 19:07:41 +13:00
|
|
|
{:ok, instructions, remaining_filter} ->
|
|
|
|
{:ok, {endpoint, nil, instructions}, remaining_filter}
|
2021-11-05 07:08:11 +13:00
|
|
|
|
|
|
|
{:error, error} ->
|
|
|
|
{:error, error}
|
|
|
|
end
|
|
|
|
|
|
|
|
{:error, error} ->
|
|
|
|
{:error, error}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-01-15 19:07:41 +13:00
|
|
|
defp filter_instructions(filter, _resource, endpoint) do
|
2021-11-05 18:13:33 +13:00
|
|
|
fields =
|
|
|
|
endpoint.fields
|
2023-01-15 19:07:41 +13:00
|
|
|
|> List.wrap()
|
2021-11-05 18:13:33 +13:00
|
|
|
|> Enum.filter(& &1.filter_handler)
|
|
|
|
|
|
|
|
Enum.reduce_while(fields, {:ok, [], filter}, fn field, {:ok, instructions, filter} ->
|
|
|
|
result =
|
|
|
|
case field.filter_handler do
|
|
|
|
:simple ->
|
2023-01-15 19:07:41 +13:00
|
|
|
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
|
2021-11-05 18:13:33 +13:00
|
|
|
|
|
|
|
{:place_in_list, path} ->
|
|
|
|
AshJsonApiWrapper.Filter.find_place_in_list_filter(
|
|
|
|
filter,
|
|
|
|
field.name,
|
|
|
|
path
|
|
|
|
)
|
|
|
|
end
|
2021-11-05 07:08:11 +13:00
|
|
|
|
2021-11-05 18:13:33 +13:00
|
|
|
case result do
|
|
|
|
{:ok, nil} ->
|
|
|
|
{:cont, {:ok, instructions, filter}}
|
2021-11-05 07:08:11 +13:00
|
|
|
|
2021-11-05 18:13:33 +13:00
|
|
|
{:ok, {remaining_filter, new_instructions}} ->
|
|
|
|
{:cont, {:ok, new_instructions ++ instructions, remaining_filter}}
|
|
|
|
end
|
|
|
|
end)
|
|
|
|
|> case do
|
|
|
|
{:ok, instructions, nil} ->
|
2023-01-15 19:07:41 +13:00
|
|
|
{:ok, instructions, nil}
|
2021-11-05 18:13:33 +13:00
|
|
|
|
2023-01-15 19:07:41 +13:00
|
|
|
{:ok, instructions, remaining_filter} ->
|
|
|
|
{:ok, instructions, remaining_filter}
|
2021-11-05 18:13:33 +13:00
|
|
|
|
|
|
|
{:error, error} ->
|
|
|
|
{:error, error}
|
2021-11-05 07:08:11 +13:00
|
|
|
end
|
2021-11-04 11:53:51 +13:00
|
|
|
end
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
def limit(query, limit, _resource) do
|
|
|
|
{:ok, %{query | limit: limit}}
|
|
|
|
end
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
def offset(query, offset, _resource) do
|
|
|
|
{:ok, %{query | offset: offset}}
|
|
|
|
end
|
|
|
|
|
2021-10-30 15:40:27 +13:00
|
|
|
@impl true
|
|
|
|
def create(resource, changeset) do
|
2022-09-16 05:06:29 +12:00
|
|
|
endpoint = AshJsonApiWrapper.DataLayer.Info.endpoint(resource, changeset.action.name)
|
2021-10-30 15:40:27 +13:00
|
|
|
|
|
|
|
base =
|
2021-11-04 11:53:51 +13:00
|
|
|
case endpoint.fields_in || :body do
|
2021-10-30 15:40:27 +13:00
|
|
|
:body ->
|
|
|
|
changeset.context[:data_layer][:body] || %{}
|
|
|
|
|
|
|
|
:params ->
|
|
|
|
changeset.context[:data_layer][:query_params] || %{}
|
|
|
|
end
|
|
|
|
|
|
|
|
{:ok, with_attrs} =
|
|
|
|
changeset.attributes
|
|
|
|
|> Kernel.||(%{})
|
|
|
|
|> Enum.reduce_while({:ok, base}, fn {key, value}, {:ok, acc} ->
|
|
|
|
attribute = Ash.Resource.Info.attribute(resource, key)
|
2022-09-16 05:06:29 +12:00
|
|
|
field = AshJsonApiWrapper.DataLayer.Info.field(resource, attribute.name)
|
2021-10-30 15:40:27 +13:00
|
|
|
|
|
|
|
case Ash.Type.dump_to_embedded(
|
|
|
|
attribute.type,
|
|
|
|
value,
|
|
|
|
attribute.constraints
|
|
|
|
) do
|
|
|
|
{:ok, dumped} ->
|
|
|
|
path =
|
|
|
|
if field && field.write_path do
|
|
|
|
field.write_path
|
|
|
|
else
|
|
|
|
[to_string(attribute.name)]
|
|
|
|
end
|
|
|
|
|
|
|
|
path =
|
|
|
|
if endpoint.write_entity_path do
|
|
|
|
endpoint.write_entity_path ++ path
|
|
|
|
else
|
|
|
|
path
|
|
|
|
end
|
|
|
|
|
|
|
|
{:cont, {:ok, put_in!(acc, path, dumped)}}
|
|
|
|
|
|
|
|
:error ->
|
|
|
|
{:halt,
|
|
|
|
{:error,
|
|
|
|
Ash.Error.Changes.InvalidAttribute.exception(
|
|
|
|
field: attribute.name,
|
|
|
|
message: "Could not be dumped to embedded"
|
|
|
|
)}}
|
|
|
|
end
|
|
|
|
end)
|
|
|
|
|
|
|
|
{body, params} =
|
|
|
|
case endpoint.fields_in do
|
|
|
|
:params ->
|
|
|
|
{changeset.context[:data_layer][:body] || %{}, with_attrs}
|
|
|
|
|
|
|
|
:body ->
|
|
|
|
{with_attrs, changeset.context[:data_layer][:query_params] || %{}}
|
|
|
|
end
|
|
|
|
|
2023-07-01 07:21:45 +12:00
|
|
|
path = endpoint.path || AshJsonApiWrapper.DataLayer.Info.endpoint_base(resource)
|
|
|
|
headers = [{"Content-Type", "application/json"}, {"Accept", "application/json"}]
|
2021-11-04 11:53:51 +13:00
|
|
|
|
2023-07-01 07:21:45 +12:00
|
|
|
with {:ok, %{status: status} = response} when status >= 200 and status < 300 <-
|
|
|
|
AshJsonApiWrapper.DataLayer.Info.tesla(resource).get(path,
|
|
|
|
body: body,
|
|
|
|
query: params,
|
|
|
|
headers: headers
|
|
|
|
),
|
2021-11-04 11:53:51 +13:00
|
|
|
{:ok, body} <- Jason.decode(response.body),
|
2023-01-15 19:07:41 +13:00
|
|
|
{:ok, entities} <- get_entities(body, endpoint, resource),
|
|
|
|
{:ok, processed} <-
|
|
|
|
process_entities(entities, resource, endpoint) do
|
2021-11-04 11:53:51 +13:00
|
|
|
{:ok, Enum.at(processed, 0)}
|
|
|
|
else
|
|
|
|
{:ok, %{status: status} = response} ->
|
2023-07-01 07:21:45 +12:00
|
|
|
# TODO: add method/query params
|
2021-11-04 11:53:51 +13:00
|
|
|
{:error,
|
2023-07-01 07:21:45 +12:00
|
|
|
"Received status code #{status} from GET #{path}. Response: #{inspect(response)}"}
|
2021-10-30 15:40:27 +13:00
|
|
|
|
2021-11-04 11:53:51 +13:00
|
|
|
other ->
|
|
|
|
other
|
|
|
|
end
|
2021-10-30 15:40:27 +13:00
|
|
|
end
|
|
|
|
|
|
|
|
defp put_in!(body, [key], value) do
|
2021-11-05 18:13:33 +13:00
|
|
|
Map.put(body || %{}, key, value)
|
2021-10-30 15:40:27 +13:00
|
|
|
end
|
|
|
|
|
|
|
|
defp put_in!(body, [first | rest], value) do
|
|
|
|
body
|
|
|
|
|> Map.put_new(first, %{})
|
|
|
|
|> Map.update!(first, &put_in!(&1, rest, value))
|
|
|
|
end
|
|
|
|
|
2021-11-05 18:13:33 +13:00
|
|
|
defp update_in!(body, [key], default, func) do
|
|
|
|
body
|
|
|
|
|> Kernel.||(%{})
|
|
|
|
|> Map.put_new(key, default)
|
|
|
|
|> Map.update!(key, func)
|
|
|
|
end
|
|
|
|
|
|
|
|
defp update_in!(body, [first | rest], default, func) do
|
|
|
|
body
|
|
|
|
|> Map.put_new(first, %{})
|
|
|
|
|> Map.update!(first, &update_in!(&1, rest, default, func))
|
|
|
|
end
|
|
|
|
|
2021-10-30 15:40:27 +13:00
|
|
|
@impl true
|
2022-05-20 08:10:09 +12:00
|
|
|
def run_query(query, resource, overridden? \\ false)
|
|
|
|
|
|
|
|
def run_query(%{override_results: results} = query, _resource, _overriden)
|
|
|
|
when not is_nil(results) do
|
2022-02-12 10:29:14 +13:00
|
|
|
do_sort({:ok, results}, query)
|
2021-11-04 11:53:51 +13:00
|
|
|
end
|
|
|
|
|
2022-05-20 08:10:09 +12:00
|
|
|
def run_query(query, resource, overridden?) do
|
2023-07-01 07:21:45 +12:00
|
|
|
endpoint =
|
|
|
|
query.endpoint || AshJsonApiWrapper.DataLayer.Info.endpoint(resource, query.action.name)
|
|
|
|
|
|
|
|
query =
|
|
|
|
if overridden? do
|
|
|
|
query
|
|
|
|
else
|
|
|
|
%{query | path: endpoint.path}
|
|
|
|
end
|
|
|
|
|
2021-11-05 07:08:11 +13:00
|
|
|
if query.templates do
|
|
|
|
query.templates
|
2023-01-15 19:07:41 +13:00
|
|
|
|> Enum.uniq()
|
2021-11-05 07:08:11 +13:00
|
|
|
|> Task.async_stream(
|
|
|
|
fn template ->
|
2023-07-01 07:21:45 +12:00
|
|
|
query
|
|
|
|
|> fill_template(template)
|
|
|
|
|> Map.put(:templates, nil)
|
|
|
|
|> run_query(resource, true)
|
2021-11-05 07:08:11 +13:00
|
|
|
end,
|
|
|
|
timeout: :infinity
|
|
|
|
)
|
|
|
|
|> Enum.reduce_while(
|
|
|
|
{:ok, []},
|
|
|
|
fn
|
|
|
|
{:ok, {:ok, results}}, {:ok, all_results} ->
|
|
|
|
{:cont, {:ok, results ++ all_results}}
|
2021-10-30 15:40:27 +13:00
|
|
|
|
2021-11-05 07:08:11 +13:00
|
|
|
{:ok, {:error, error}}, _ ->
|
|
|
|
{:halt, {:error, error}}
|
|
|
|
|
|
|
|
{:exit, reason}, _ ->
|
|
|
|
{:error, "Request process exited with #{inspect(reason)}"}
|
|
|
|
end
|
|
|
|
)
|
2021-10-30 15:40:27 +13:00
|
|
|
else
|
2022-09-16 05:06:29 +12:00
|
|
|
query =
|
|
|
|
if query.limit do
|
|
|
|
if query.offset && query.offset != 0 do
|
2023-07-01 07:21:45 +12:00
|
|
|
Logger.warning(
|
2023-01-15 07:28:21 +13:00
|
|
|
"ash_json_api_wrapper does not support limits with offsets yet, and so they will both be applied after."
|
|
|
|
)
|
|
|
|
|
2022-09-16 05:06:29 +12:00
|
|
|
query
|
|
|
|
else
|
|
|
|
case endpoint.limit_with do
|
|
|
|
{:param, param} ->
|
|
|
|
%{
|
|
|
|
query
|
2023-07-01 07:21:45 +12:00
|
|
|
| query_params: Map.put(query.query_params || %{}, param, query.limit)
|
2022-09-16 05:06:29 +12:00
|
|
|
}
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
query
|
|
|
|
end
|
|
|
|
end
|
|
|
|
else
|
|
|
|
query
|
|
|
|
end
|
|
|
|
|
2023-07-01 07:21:45 +12:00
|
|
|
with {:ok, %{status: status} = response} when status >= 200 and status < 300 <-
|
|
|
|
make_request(resource, query),
|
2021-11-05 07:08:11 +13:00
|
|
|
{:ok, body} <- Jason.decode(response.body),
|
2023-07-01 07:21:45 +12:00
|
|
|
{:ok, entities} <- get_entities(body, endpoint, resource, paginate_with: query) do
|
2021-11-05 07:08:11 +13:00
|
|
|
entities
|
|
|
|
|> limit_offset(query)
|
2021-11-05 18:13:33 +13:00
|
|
|
|> process_entities(resource, endpoint)
|
2021-11-05 07:08:11 +13:00
|
|
|
else
|
|
|
|
{:ok, %{status: status} = response} ->
|
2023-07-01 07:21:45 +12:00
|
|
|
# TODO: more info here
|
2021-11-05 07:08:11 +13:00
|
|
|
{:error,
|
2023-07-01 07:21:45 +12:00
|
|
|
"Received status code #{status} from #{query.path}. Response: #{inspect(response)}"}
|
2021-10-30 15:40:27 +13:00
|
|
|
|
2021-11-05 07:08:11 +13:00
|
|
|
other ->
|
|
|
|
other
|
|
|
|
end
|
2021-10-30 15:40:27 +13:00
|
|
|
end
|
2022-02-12 10:29:14 +13:00
|
|
|
|> do_sort(query)
|
2023-01-15 19:07:41 +13:00
|
|
|
|> 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
|
2021-10-30 15:40:27 +13:00
|
|
|
end
|
|
|
|
|
2022-02-12 10:29:14 +13:00
|
|
|
defp do_sort({:ok, results}, %{sort: sort}) when sort not in [nil, []] do
|
|
|
|
Ash.Sort.runtime_sort(results, sort)
|
|
|
|
end
|
|
|
|
|
|
|
|
defp do_sort(other, _), do: other
|
|
|
|
|
2023-07-01 07:21:45 +12:00
|
|
|
defp fill_template(query, template) do
|
2021-11-05 07:08:11 +13:00
|
|
|
template
|
|
|
|
|> List.wrap()
|
2023-07-01 07:21:45 +12:00
|
|
|
|> Enum.reduce(query, fn
|
|
|
|
{key, replacement}, query ->
|
2023-01-15 19:07:41 +13:00
|
|
|
%{
|
2023-07-01 07:21:45 +12:00
|
|
|
query
|
|
|
|
| path: String.replace(query.path, ":#{key}", to_string(replacement))
|
2023-01-15 19:07:41 +13:00
|
|
|
}
|
|
|
|
|
2023-07-01 07:21:45 +12:00
|
|
|
{:set, key, value}, query ->
|
2023-01-15 19:07:41 +13:00
|
|
|
key =
|
|
|
|
key
|
|
|
|
|> List.wrap()
|
|
|
|
|> Enum.map(&to_string/1)
|
|
|
|
|
2023-07-01 07:21:45 +12:00
|
|
|
%{
|
|
|
|
query
|
|
|
|
| query_params: AshJsonApiWrapper.Helpers.put_at_path(query.query_params, key, value)
|
|
|
|
}
|
2021-11-05 07:08:11 +13:00
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
2021-11-04 11:53:51 +13:00
|
|
|
defp limit_offset(results, %Query{limit: limit, offset: offset}) do
|
|
|
|
results =
|
|
|
|
if offset do
|
|
|
|
Enum.drop(results, offset)
|
|
|
|
else
|
|
|
|
results
|
|
|
|
end
|
|
|
|
|
|
|
|
if limit do
|
|
|
|
Enum.take(results, limit)
|
|
|
|
else
|
|
|
|
results
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-07-01 07:21:45 +12:00
|
|
|
defp make_request(resource, query) do
|
|
|
|
# log_send(path, query)
|
|
|
|
AshJsonApiWrapper.DataLayer.Info.tesla(resource).get(query.path,
|
|
|
|
body: query.body,
|
|
|
|
query: query.query_params
|
|
|
|
)
|
2021-11-05 18:13:33 +13:00
|
|
|
|
2023-07-01 07:21:45 +12:00
|
|
|
# |> log_resp(path, query)
|
2021-11-05 18:13:33 +13:00
|
|
|
end
|
|
|
|
|
2023-07-01 07:21:45 +12:00
|
|
|
# defp log_send(request) do
|
|
|
|
# Logger.debug("Sending request: #{inspect(request)}")
|
|
|
|
# request
|
|
|
|
# end
|
2021-10-30 15:40:27 +13:00
|
|
|
|
2023-07-01 07:21:45 +12:00
|
|
|
# defp log_resp(response) do
|
|
|
|
# Logger.debug("Received response: #{inspect(response)}")
|
|
|
|
# response
|
|
|
|
# end
|
2021-10-30 15:40:27 +13:00
|
|
|
|
2021-11-05 18:13:33 +13:00
|
|
|
defp process_entities(entities, resource, endpoint) do
|
2021-10-30 15:40:27 +13:00
|
|
|
Enum.reduce_while(entities, {:ok, []}, fn entity, {:ok, entities} ->
|
2021-11-05 18:13:33 +13:00
|
|
|
case process_entity(entity, resource, endpoint) do
|
2021-10-30 15:40:27 +13:00
|
|
|
{:ok, entity} -> {:cont, {:ok, [entity | entities]}}
|
|
|
|
{:error, error} -> {:halt, {:error, error}}
|
|
|
|
end
|
|
|
|
end)
|
|
|
|
|> case do
|
|
|
|
{:ok, entities} -> {:ok, Enum.reverse(entities)}
|
|
|
|
{:error, error} -> {:error, error}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-11-05 18:13:33 +13:00
|
|
|
defp process_entity(entity, resource, endpoint) do
|
2021-10-30 15:40:27 +13:00
|
|
|
resource
|
|
|
|
|> Ash.Resource.Info.attributes()
|
|
|
|
|> Enum.reduce_while(
|
|
|
|
{:ok,
|
|
|
|
struct(resource,
|
|
|
|
__meta__: %Ecto.Schema.Metadata{
|
|
|
|
state: :loaded
|
|
|
|
}
|
|
|
|
)},
|
|
|
|
fn attr, {:ok, record} ->
|
2021-11-05 18:13:33 +13:00
|
|
|
case get_field(entity, attr, resource, endpoint) do
|
2021-10-30 15:40:27 +13:00
|
|
|
{:ok, value} ->
|
|
|
|
{:cont, {:ok, Map.put(record, attr.name, value)}}
|
|
|
|
|
|
|
|
{:error, error} ->
|
|
|
|
{:halt, {:error, error}}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2021-11-05 18:13:33 +13:00
|
|
|
defp get_field(entity, attr, resource, endpoint) do
|
|
|
|
raw_value = get_raw_value(entity, attr, resource, endpoint)
|
2021-10-30 15:40:27 +13:00
|
|
|
|
|
|
|
case Ash.Type.cast_stored(attr.type, raw_value, attr.constraints) do
|
|
|
|
{:ok, value} ->
|
|
|
|
{:ok, value}
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
{:error,
|
|
|
|
AshJsonApiWrapper.Errors.InvalidData.exception(field: attr.name, value: raw_value)}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-11-05 18:13:33 +13:00
|
|
|
defp get_raw_value(entity, attr, resource, endpoint) do
|
|
|
|
case get_field(resource, endpoint, attr.name) do
|
2022-05-20 08:10:09 +12:00
|
|
|
%{path: ""} ->
|
|
|
|
entity
|
|
|
|
|
2021-10-30 15:40:27 +13:00
|
|
|
%{path: path} when not is_nil(path) ->
|
|
|
|
case ExJSONPath.eval(entity, path) do
|
|
|
|
{:ok, [value | _]} ->
|
|
|
|
value
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
Map.get(entity, to_string(attr.name))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-01-15 19:07:41 +13:00
|
|
|
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)}
|
2021-10-30 15:40:27 +13:00
|
|
|
|
2023-01-15 19:07:41 +13:00
|
|
|
path ->
|
|
|
|
case ExJSONPath.eval(body, path) do
|
|
|
|
{:ok, [entities | _]} ->
|
|
|
|
{:ok, List.wrap(entities)}
|
2021-10-30 15:40:27 +13:00
|
|
|
|
2023-01-15 19:07:41 +13:00
|
|
|
{:ok, _} ->
|
|
|
|
{:ok, []}
|
|
|
|
|
|
|
|
{:error, error} ->
|
|
|
|
{:error, error}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp get_all_bodies(
|
|
|
|
body,
|
|
|
|
%{paginator: {module, opts}} = endpoint,
|
|
|
|
resource,
|
2023-07-01 07:21:45 +12:00
|
|
|
paginate_with,
|
2023-01-15 19:07:41 +13:00
|
|
|
entity_callback,
|
|
|
|
bodies
|
|
|
|
) do
|
2023-07-01 07:21:45 +12:00
|
|
|
case module.continue(body, Enum.at(bodies, 0), opts) do
|
2023-01-15 19:07:41 +13:00
|
|
|
:halt ->
|
|
|
|
{:ok, bodies}
|
|
|
|
|
|
|
|
{:ok, instructions} ->
|
2023-07-01 07:21:45 +12:00
|
|
|
query = apply_instructions(paginate_with, instructions)
|
2023-01-15 19:07:41 +13:00
|
|
|
|
2023-07-01 07:21:45 +12:00
|
|
|
case make_request(resource, query) do
|
2023-01-15 19:07:41 +13:00
|
|
|
{: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
|
2023-07-01 07:21:45 +12:00
|
|
|
get_all_bodies(new_body, endpoint, resource, paginate_with, entity_callback, [
|
2023-01-15 19:07:41 +13:00
|
|
|
entities | bodies
|
|
|
|
])
|
|
|
|
end
|
|
|
|
|
|
|
|
{:ok, %{status: status} = response} ->
|
2023-07-01 07:21:45 +12:00
|
|
|
# TODO: more info
|
2023-01-15 19:07:41 +13:00
|
|
|
{:error,
|
2023-07-01 07:21:45 +12:00
|
|
|
"Received status code #{status} in #{query.path}. Response: #{inspect(response)}"}
|
2021-10-30 15:40:27 +13:00
|
|
|
|
|
|
|
{:error, error} ->
|
|
|
|
{:error, error}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2021-11-05 18:13:33 +13:00
|
|
|
|
2023-07-01 07:21:45 +12:00
|
|
|
defp apply_instructions(query, instructions) do
|
|
|
|
query
|
2023-01-15 19:07:41 +13:00
|
|
|
|> apply_params(instructions)
|
|
|
|
|> apply_headers(instructions)
|
|
|
|
end
|
|
|
|
|
2023-07-01 07:21:45 +12:00
|
|
|
defp apply_params(query, %{params: params}) when is_map(params) do
|
|
|
|
%{query | query_params: Ash.Helpers.deep_merge_maps(query.query_params || %{}, params)}
|
2023-01-15 19:07:41 +13:00
|
|
|
end
|
|
|
|
|
2023-07-01 07:21:45 +12:00
|
|
|
defp apply_params(query, _), do: query
|
2023-01-15 19:07:41 +13:00
|
|
|
|
2023-07-01 07:21:45 +12:00
|
|
|
defp apply_headers(query, %{headers: headers}) when is_map(headers) do
|
2023-01-15 19:07:41 +13:00
|
|
|
%{
|
2023-07-01 07:21:45 +12:00
|
|
|
query
|
2023-01-15 19:07:41 +13:00
|
|
|
| headers:
|
2023-07-01 07:21:45 +12:00
|
|
|
query.headers
|
2023-01-15 19:07:41 +13:00
|
|
|
|> Kernel.||(%{})
|
|
|
|
|> Map.new()
|
|
|
|
|> Map.merge(headers)
|
|
|
|
|> Map.to_list()
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2023-07-01 07:21:45 +12:00
|
|
|
defp apply_headers(query, _), do: query
|
2023-01-15 19:07:41 +13:00
|
|
|
|
2021-11-05 18:13:33 +13:00
|
|
|
defp get_field(resource, endpoint, field) do
|
2022-01-20 10:34:45 +13:00
|
|
|
Enum.find(endpoint.fields || [], &(&1.name == field)) ||
|
2022-09-16 05:06:29 +12:00
|
|
|
Enum.find(AshJsonApiWrapper.DataLayer.Info.fields(resource), &(&1.name == field))
|
2021-11-05 18:13:33 +13:00
|
|
|
end
|
2021-10-30 15:40:27 +13:00
|
|
|
end
|