Packages
ash_json_api
0.27.5
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/resource/resource.ex
defmodule AshJsonApi.Resource do
@route_schema [
route: [
type: :string,
required: true,
doc: "The path of the route"
],
action: [
type: :atom,
required: true,
doc: "The action to call when this route is hit"
],
primary?: [
type: :boolean,
default: false,
doc:
"Whether or not this is the route that should be linked to by default when rendering links to this type of route"
]
]
@get %Ash.Dsl.Entity{
name: :get,
args: [:action],
describe: "A GET route to retrieve a single record",
examples: [
"get :read"
],
schema:
@route_schema
|> Ash.OptionsHelpers.set_default!(:route, "/:id"),
target: AshJsonApi.Resource.Route,
auto_set_fields: [
method: :get,
controller: AshJsonApi.Controllers.Get,
action_type: :read,
type: :get
]
}
@index %Ash.Dsl.Entity{
name: :index,
args: [:action],
describe: "A GET route to retrieve a list of records",
examples: [
"index :read"
],
schema:
@route_schema
|> Ash.OptionsHelpers.set_default!(:route, "/")
|> Keyword.put(:paginate?, type: :boolean, default: true),
target: AshJsonApi.Resource.Route,
auto_set_fields: [
method: :get,
controller: AshJsonApi.Controllers.Index,
action_type: :read,
type: :index
]
}
@post %Ash.Dsl.Entity{
name: :post,
args: [:action],
describe: "A POST route to create a record",
examples: [
"post :create"
],
schema:
@route_schema
|> Ash.OptionsHelpers.set_default!(:route, "/"),
target: AshJsonApi.Resource.Route,
auto_set_fields: [
method: :post,
controller: AshJsonApi.Controllers.Post,
action_type: :create,
type: :post
]
}
@patch %Ash.Dsl.Entity{
name: :patch,
args: [:action],
describe: "A PATCH route to update a record",
examples: [
"patch :update"
],
schema:
@route_schema
|> Ash.OptionsHelpers.set_default!(:route, "/:id"),
target: AshJsonApi.Resource.Route,
auto_set_fields: [
method: :patch,
controller: AshJsonApi.Controllers.Patch,
action_type: :update,
type: :patch
]
}
@delete %Ash.Dsl.Entity{
name: :delete,
args: [:action],
describe: "A DELETE route to destroy a record",
examples: [
"delete :destroy"
],
schema:
@route_schema
|> Ash.OptionsHelpers.set_default!(:route, "/:id"),
target: AshJsonApi.Resource.Route,
auto_set_fields: [
method: :delete,
controller: AshJsonApi.Controllers.Delete,
action_type: :destroy,
type: :delete
]
}
@related %Ash.Dsl.Entity{
name: :related,
args: [:relationship, :action],
describe: "A GET route to read the related resources of a relationship",
examples: [
"related :comments, :read"
],
schema:
@route_schema
|> Ash.OptionsHelpers.make_optional!(:route)
|> Ash.OptionsHelpers.append_doc!(:route, "Defaults to /:id/[relationship_name]")
|> Keyword.put(:relationship,
type: :atom,
required: true
),
transform: {__MODULE__, :set_related_route, []},
target: AshJsonApi.Resource.Route,
auto_set_fields: [
method: :get,
controller: AshJsonApi.Controllers.GetRelated
]
}
@relationship %Ash.Dsl.Entity{
name: :relationship,
args: [:relationship, :action],
describe: "A READ route to read the relationship, returns resource identifiers.",
examples: [
"relationship :comments, :read"
],
schema:
@route_schema
|> Ash.OptionsHelpers.make_optional!(:route)
|> Ash.OptionsHelpers.append_doc!(
:route,
" Defaults to /:id/relationships/[relationship_name]"
)
|> Keyword.put(:relationship,
type: :atom,
required: true
),
transform: {__MODULE__, :set_relationship_route, []},
target: AshJsonApi.Resource.Route,
auto_set_fields: [
method: :get,
controller: AshJsonApi.Controllers.GetRelationship
]
}
@post_to_relationship %Ash.Dsl.Entity{
name: :post_to_relationship,
args: [:relationship],
describe: "A POST route to create related entities using resource identifiers",
examples: [
"post_to_relationship :comments"
],
schema:
@route_schema
|> Ash.OptionsHelpers.make_optional!(:route)
|> Ash.OptionsHelpers.append_doc!(
:route,
" Defaults to /:id/relationships/[relationship_name]"
)
|> Keyword.put(:relationship,
type: :atom,
required: true
)
|> Keyword.delete(:action),
transform: {__MODULE__, :set_relationship_route, []},
target: AshJsonApi.Resource.Route,
auto_set_fields: [
method: :post,
type: :post_to_relationship,
controller: AshJsonApi.Controllers.PostToRelationship
]
}
@patch_relationship %Ash.Dsl.Entity{
name: :patch_relationship,
args: [:relationship],
describe: "A PATCH route to update a relationship using resource identifiers",
examples: [
"patch_relationship :comments"
],
schema:
@route_schema
|> Ash.OptionsHelpers.make_optional!(:route)
|> Ash.OptionsHelpers.append_doc!(
:route,
" Defaults to /:id/relationships/[relationship_name]"
)
|> Keyword.put(:relationship,
type: :atom,
required: true
)
|> Keyword.delete(:action),
transform: {__MODULE__, :set_relationship_route, []},
target: AshJsonApi.Resource.Route,
auto_set_fields: [
method: :patch,
type: :patch_relationship,
controller: AshJsonApi.Controllers.PatchRelationship
]
}
@delete_from_relationship %Ash.Dsl.Entity{
name: :delete_from_relationship,
args: [:relationship],
describe: "A DELETE route to remove related entities using resource identifiers",
examples: [
"delete_from_relationship :comments"
],
schema:
@route_schema
|> Ash.OptionsHelpers.make_optional!(:route)
|> Ash.OptionsHelpers.append_doc!(
:route,
" Defaults to /:id/relationships/[relationship_name]"
)
|> Keyword.put(:relationship,
type: :atom,
required: true
)
|> Keyword.delete(:action),
transform: {__MODULE__, :set_relationship_route, []},
target: AshJsonApi.Resource.Route,
auto_set_fields: [
method: :delete,
type: :delete_from_relationship,
controller: AshJsonApi.Controllers.DeleteFromRelationship
]
}
@routes %Ash.Dsl.Section{
name: :routes,
describe: "Configure the routes that will be exposed via the JSON:API",
schema: [
base: [
type: :string,
required: true,
doc: "The base route for the resource, e.g `\"/users\"`"
]
],
examples: [
"""
routes do
base_route "/posts"
get :read
get :me, route: "/me"
index :read
post :confirm_name, route: "/confirm_name"
patch :update
related :comments, :read
relationship :comments, :read
post_to_relationship :comments
patch_relationship :comments
delete_from_relationship :comments
end
"""
],
entities: [
@get,
@index,
@post,
@patch,
@delete,
@related,
@relationship,
@post_to_relationship,
@patch_relationship,
@delete_from_relationship
]
}
@primary_key %Ash.Dsl.Section{
name: :primary_key,
describe: "Encode the id of the JSON API response from selected attributes of a resource",
examples: [
"""
primary_key do
keys [:first_name, :last_name]
delimiter "~"
end
"""
],
schema: [
keys: [
type: {:custom, Ash.OptionsHelpers, :list_of_atoms, []},
doc: "the list of attributes to encode JSON API primary key",
required: true
],
delimiter: [
type: :string,
default: "-",
required: false,
doc: "The delimiter to concatenate the primary key values. Default to be '-'"
]
]
}
@json_api %Ash.Dsl.Section{
name: :json_api,
sections: [@routes, @primary_key],
describe: "Configure the resource's behavior in the JSON:API",
examples: [
"""
json_api do
type "post"
includes [
friends: [
:comments
],
comments: []
]
routes do
base_route "/posts"
get :read
get :me, route: "/me"
index :read
post :confirm_name, route: "/confirm_name"
patch :update
related :comments, :read
relationship :comments, :read
post_to_relationship :comments
patch_relationship :comments
delete_from_relationship :comments
end
end
"""
],
schema: [
type: [
type: :string,
doc: "The resource identifier type of this resource in JSON:API",
required: true
],
includes: [
type: :any,
default: [],
doc: "A keyword list of all paths that are includable from this resource"
]
]
}
@transformers [
AshJsonApi.Resource.Transformers.PrependRoutePrefix,
AshJsonApi.Resource.Transformers.ValidateNoOverlappingRoutes,
AshJsonApi.Resource.Transformers.RequirePrimaryKey
]
@sections [@json_api]
@moduledoc """
The entrypoint for adding JSON:API behavior to a resource"
# Table of Contents
#{Ash.Dsl.Extension.doc_index(@sections)}
#{Ash.Dsl.Extension.doc(@sections)}
"""
require Ash.Dsl.Extension
use Ash.Dsl.Extension, sections: @sections, transformers: @transformers
def type(resource) do
Extension.get_opt(resource, [:json_api], :type, nil, false)
end
def includes(resource) do
Extension.get_opt(resource, [:json_api], :includes, [], false)
end
def base_route(resource) do
Extension.get_opt(resource, [:json_api, :routes], :base, nil, false)
end
def encode_primary_key(%resource{} = record) do
case primary_key_fields(resource) do
[] ->
# Expect resource to have only 1 primary key if :primary_key section is not used
[key] = Ash.Resource.Info.primary_key(resource)
Map.get(record, key)
keys ->
delimiter = primary_key_delimiter(resource)
[_ | concatenated_keys] =
keys
|> Enum.reverse()
|> Enum.reduce([], fn key, acc -> [delimiter, to_string(Map.get(record, key)), acc] end)
IO.iodata_to_binary(concatenated_keys)
end
end
def primary_key_fields(resource) do
Extension.get_opt(resource, [:json_api, :primary_key], :keys, [], false)
end
def primary_key_delimiter(resource) do
Extension.get_opt(resource, [:json_api, :primary_key], :delimiter, [], false)
end
def routes(resource) do
Extension.get_entities(resource, [:json_api, :routes])
end
def route(resource, criteria \\ %{}) do
resource
|> routes()
|> Enum.find(fn route ->
Map.take(route, Map.keys(criteria)) == criteria
end)
end
@doc false
def set_related_route(%{route: nil, relationship: relationship} = route) do
{:ok, %{route | route: ":id/#{relationship}"}}
end
def set_related_route(route), do: {:ok, route}
@doc false
def set_relationship_route(%{route: nil, relationship: relationship} = route) do
{:ok, %{route | route: ":id/relationships/#{relationship}"}}
end
def set_relationship_route(route), do: {:ok, route}
@doc false
def validate_fields(fields) when is_list(fields) do
if Enum.all?(fields, &is_atom/1) do
{:ok, fields}
else
{:error, "Invalid fields"}
end
end
end