Packages
ash_json_api
0.30.0-rc.2
1.7.1
1.7.0
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0-rc.2
1.6.0-rc.1
1.6.0-rc.0
1.5.1
1.5.0
1.4.45
1.4.44
1.4.43
1.4.42
1.4.41
1.4.40
1.4.39
1.4.38
1.4.37
1.4.36
1.4.35
1.4.34
1.4.33
1.4.32
1.4.31
1.4.30
1.4.29
1.4.28
1.4.27
1.4.26
1.4.25
1.4.24
1.4.23
1.4.22
1.4.21
1.4.20
1.4.19
1.4.18
1.4.17
1.4.16
1.4.15
1.4.13
1.4.12
1.4.11
1.4.10
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.8
1.3.7
1.3.6
1.3.5
1.3.4
retired
1.3.3
1.3.2
1.3.1
1.3.0
1.2.2
1.2.1
1.2.0
1.1.2
1.1.1
1.1.0
1.0.0
1.0.0-rc.6
1.0.0-rc.5
1.0.0-rc.4
1.0.0-rc.3
1.0.0-rc.2
1.0.0-rc.1
1.0.0-rc.0
0.34.2
0.34.1
0.34.0
0.33.1
0.33.0
0.32.1
0.32.0
0.31.3
0.31.2
retired
0.31.1
0.31.0
0.30.1
0.30.0-rc.4
0.30.0-rc.3
0.30.0-rc.2
0.30.0-rc.1
0.30.0-rc.0
0.29.1
0.29.0
0.28.6
0.28.5
0.28.4
0.28.3
0.28.2
0.28.1
0.28.0
0.27.6
0.27.5
0.27.1
0.27.0
0.25.0
0.24.4
0.24.2
0.24.1
0.24.0
0.23.0
0.22.0
0.21.0
0.20.0
0.19.0
0.18.0
0.17.0
0.16.0
0.15.0
0.14.0
0.13.0
0.12.0
0.11.1
0.10.0
0.9.0
0.8.0
0.6.0
0.5.0
0.4.0
0.3.0
0.2.4
0.2.3
0.2.1
0.2.0
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
The JSON:API extension for the Ash Framework.
Current section
Files
Jump to
Current section
Files
lib/ash_json_api/controllers/helpers.ex
defmodule AshJsonApi.Controllers.Helpers do
@moduledoc false
# @moduledoc """
# When we open up ash json api tooling to allow people to build custom
# behavior around it, we can use this documentation
# Tools for control flow around a request, and common controller utilities.
# While we haven't focused on supporting it yet, this will eventually be a set of tools
# that can be used to build custom controller actions, without having to write everything
# yourself.
# `chain/2` lets us pipe cleanly, only doing stateful things if no errors
# have been generated yet.
# """
alias AshJsonApi.Controllers.Response
alias AshJsonApi.{Error, Request}
alias AshJsonApi.Includes.Includer
require Ash.Query
def render_or_render_errors(request, conn, function) do
chain(request, function,
fallback: fn request ->
Response.render_errors(conn, request)
end
)
end
def fetch_includes(request) do
chain(request, fn request ->
{new_result, includes} = Includer.get_includes(request.assigns.result, request)
request
|> Request.assign(:result, new_result)
|> Request.assign(:includes, includes)
end)
end
def fetch_records(request) do
chain(request, fn request ->
request.resource
|> Ash.Query.load(request.includes_keyword)
|> Ash.Query.filter(^request.filter)
|> Ash.Query.sort(request.sort)
|> Ash.Query.load(fields(request, request.resource))
|> Ash.Query.for_read(request.action.name, request.arguments, Request.opts(request))
|> request.api.read()
|> case do
{:ok, result} ->
Request.assign(request, :result, result)
{:error, error} ->
Request.add_error(request, error, :read)
end
end)
end
def create_record(request) do
chain(request, fn %{api: api, resource: resource} ->
resource
|> Ash.Changeset.for_create(
request.action.name,
Map.merge(request.attributes, request.arguments),
Request.opts(request)
)
|> api.create()
|> api.load(fields(request, request.resource) ++ (request.includes_keyword || []))
|> case do
{:ok, record} ->
Request.assign(request, :result, record)
{:error, error} ->
Request.add_error(request, error, :create)
end
end)
end
def update_record(request) do
chain(request, fn %{api: api, assigns: %{result: result}} ->
result
|> Ash.Changeset.for_update(
request.action.name,
Map.merge(request.attributes, request.arguments),
Request.opts(request)
)
|> api.update()
|> api.load(fields(request, request.resource) ++ (request.includes_keyword || []))
|> case do
{:ok, record} ->
Request.assign(request, :result, record)
{:error, error} ->
Request.add_error(request, error, :update)
end
end)
end
def add_to_relationship(request, relationship_name) do
chain(request, fn %{api: api, assigns: %{result: result}} ->
action = Ash.Resource.Info.primary_action!(request.resource, :update).name
result
|> Ash.Changeset.new()
|> Ash.Changeset.append_to_relationship(relationship_name, request.resource_identifiers)
|> Ash.Changeset.for_update(action, %{}, Request.opts(request))
|> api.update()
|> api.load(fields(request, request.resource))
|> case do
{:ok, updated} ->
request
|> Request.assign(:record_from_path, updated)
|> Request.assign(:result, Map.get(updated, relationship_name))
{:error, error} ->
Request.add_error(request, error, :add_to_relationship)
end
end)
end
def replace_relationship(request, relationship_name) do
chain(request, fn %{api: api, assigns: %{result: result}} ->
action = Ash.Resource.Info.primary_action!(request.resource, :update).name
result
|> Ash.Changeset.new()
|> Ash.Changeset.replace_relationship(relationship_name, request.resource_identifiers)
|> Ash.Changeset.for_update(action, %{}, Request.opts(request))
|> api.update()
|> api.load(fields(request, request.resource))
|> case do
{:ok, updated} ->
request
|> Request.assign(:record_from_path, updated)
|> Request.assign(:result, Map.get(updated, relationship_name))
{:error, error} ->
Request.add_error(request, error, :replace_relationship)
end
end)
end
def delete_from_relationship(request, relationship_name) do
chain(request, fn %{api: api, assigns: %{result: result}} ->
action = Ash.Resource.Info.primary_action!(request.resource, :update).name
result
|> Ash.Changeset.new()
|> Ash.Changeset.remove_from_relationship(relationship_name, request.resource_identifiers)
|> Ash.Changeset.for_update(action, Request.opts(request))
|> api.update()
|> api.load(fields(request, request.resource))
|> case do
{:ok, updated} ->
request
|> Request.assign(:record_from_path, updated)
|> Request.assign(:result, Map.get(updated, relationship_name))
{:error, error} ->
Request.add_error(request, error, :delete_from_relationship)
end
end)
end
def destroy_record(request) do
chain(request, fn %{api: api, assigns: %{result: result}} = request ->
result
|> Ash.Changeset.for_destroy(%{}, Request.opts(request))
|> api.destroy()
|> case do
:ok ->
Request.assign(request, :result, nil)
{:error, error} ->
Request.add_error(request, error, :destroy)
end
end)
end
defp path_filter(path_params, resource) do
Enum.reduce(path_params, %{}, fn {key, value}, acc ->
case Ash.Resource.Info.public_attribute(resource, key) do
nil ->
acc
attribute ->
Map.put(acc, attribute.name, value)
end
end)
end
def fetch_record_from_path(request, through_resource \\ nil) do
chain(request, fn %{api: api, resource: request_resource} = request ->
resource = through_resource || request_resource
filter = path_filter(request.path_params, resource)
action =
if through_resource || request.action.type != :read do
Ash.Resource.Info.primary_action!(resource, :read).name
else
request.action.name
end
fields_to_load =
if through_resource do
[]
else
fields(request, request.resource)
end
resource
|> Ash.Query.filter(^filter)
|> Ash.Query.load(fields_to_load ++ (request.includes_keyword || []))
|> Ash.Query.for_read(
action,
request.arguments,
Keyword.put(Request.opts(request), :page, false)
)
|> api.read_one()
|> case do
{:ok, nil} ->
error = Error.NotFound.new(filter: filter, resource: resource)
Request.add_error(request, error, :fetch_from_path)
Request.add_error(request, error, :fetch_from_path)
{:ok, record} ->
request
|> Request.assign(:result, record)
|> Request.assign(:record_from_path, record)
{:error, error} ->
Request.add_error(request, error, :fetch_from_path)
end
end)
end
def fetch_related(request) do
request
|> chain(fn %{
api: api,
assigns: %{result: %source_resource{} = record},
relationship: relationship
} = request ->
relationship = Ash.Resource.Info.public_relationship(source_resource, relationship)
sort = request.sort || default_sort(request.resource)
load_params =
if Map.get(request.assigns, :page) do
[page: request.assigns.page]
else
[]
end
destination_query =
relationship.destination
|> Ash.Query.new(request.api)
|> Ash.Query.filter(^request.filter)
|> Ash.Query.sort(sort)
|> Ash.Query.load(request.includes_keyword)
|> Ash.Query.load(fields(request, request.resource))
|> Ash.Query.put_context(:override_api_params, load_params)
origin_query =
source_resource
|> Ash.Query.new(request.api)
|> Ash.Query.load([{relationship.name, destination_query}])
|> Ash.Query.set_tenant(request.tenant)
case api.load(
record,
origin_query,
Request.load_opts(request)
) do
{:ok, record} ->
paginated_result =
record
|> Map.get(relationship.name)
|> paginator_or_list()
request
|> Request.assign(:record_from_path, record)
|> Request.assign(:result, paginated_result)
{:error, error} ->
Request.add_error(request, error, :fetch_related)
end
end)
end
defp paginator_or_list(result) do
case result do
%{results: _} = paginator ->
paginator
other ->
List.wrap(other)
end
end
defp fields(request, resource) do
Map.get(request.fields, resource) || []
end
defp default_sort(resource) do
created_at =
Ash.Resource.Info.public_attribute(resource, :created_at) ||
Ash.Resource.Info.public_attribute(resource, :inserted_at)
if created_at do
[{created_at.name, :asc}]
else
Ash.Resource.Info.primary_key(resource)
end
end
def fetch_id_path_param(request) do
chain(request, fn request ->
case request.path_params do
%{"id" => id} ->
Request.assign(request, :id, id)
_ ->
Request.add_error(
request,
"id path parameter not present in get route: #{request.url}",
:id_path_param
)
end
end)
end
# This doesn't need to use chain, because its stateless and safe to
# do anytime. Returning multiple errors is a nice feature of JSON API
def fetch_pagination_parameters(request) do
request
|> add_pagination_parameter(:limit, :integer)
|> add_pagination_parameter(:offset, :integer)
|> add_pagination_parameter(:after, :string)
|> add_pagination_parameter(:before, :string)
|> add_pagination_parameter(:count, :boolean)
end
defp add_pagination_parameter(request, parameter, type) do
with %{"page" => page} <- request.query_params,
{:ok, value} <- Map.fetch(page, to_string(parameter)) do
case cast_pagination_parameter(value, type) do
{:ok, value} ->
Request.update_assign(
request,
:page,
[{parameter, value}],
&Keyword.put(&1, parameter, value)
)
:error ->
Request.add_error(
request,
Error.InvalidPagination.new(source_parameter: "page[#{parameter}]"),
:read
)
end
else
_ ->
request
end
end
defp cast_pagination_parameter(value, :integer) do
case Integer.parse(value) do
{integer, ""} ->
{:ok, integer}
_ ->
:error
end
end
defp cast_pagination_parameter("true", :boolean), do: {:ok, true}
defp cast_pagination_parameter("false", :boolean), do: {:ok, false}
defp cast_pagination_parameter(_, :boolean), do: :error
defp cast_pagination_parameter(value, :string) when is_binary(value) do
{:ok, value}
end
defp cast_pagination_parameter(_, _), do: :error
def chain(request, func, opts \\ []) do
case request.errors do
[] ->
func.(request)
_ ->
case Keyword.fetch(opts, :fallback) do
{:ok, fallback} ->
fallback.(request)
_ ->
request
end
end
end
# @spec with_request(
# Plug.Conn.t(),
# Ash.Resource.t(),
# Ash.action(),
# (AshJsonApi.Request.t() -> Plug.Conn.t())
# ) :: Plug.Conn.t()
# def with_request(conn, resource, action, function) do
# case AshJsonApi.Request.from(conn, resource, action) do
# %{errors: []} = request ->
# function.(request)
# %{errors: errors} = request ->
# Response.render_errors(conn, request, errors)
# end
# end
end