2019-10-03 16:08:36 +13:00
defmodule Ash do
2019-12-05 20:18:13 +13:00
@moduledoc """
2020-06-14 18:39:11 +12:00
! [ Logo ] ( https :/ / github . com / ash - project / ash / blob / master / logos / cropped - for - header . png? raw = true )
## Quick Links
- [ Resource Documentation ] ( Ash.Resource . html )
- [ DSL Documentation ] ( Ash.Dsl . html )
- [ Code API documentation ] ( Ash.Api.Interface . html )
## Introduction
Traditional MVC Frameworks ( Rails , Django , . Net , Phoenix , etc ) leave it up to the user to build the glue between requests for data ( HTTP requests in various forms as well as server - side domain logic ) and their respective ORMs . In that space , there is an incredible amount of boilerplate code that must get written from scratch for each application ( authentication , authorization , sorting , filtering , sideloading relationships , serialization , etc ) .
Ash is an opinionated yet configurable framework designed to reduce boilerplate in an Elixir application . Ash does this by providing a layer of abstraction over your system ' s data layer(s) with `Resources`. It is designed to be used in conjunction with a phoenix application, or on its own.
To riff on a famous JRR Tolkien quote , a ` Resource ` is " One Interface to rule them all, One Interface to find them " and will become an indispensable place to define contracts for interacting with data throughout your application .
To start using Ash , first declare your ` Resources ` using the Ash ` Resource ` DSL . You could technically stop there , and just leverage the Ash Elixir API to avoid writing boilerplate . More likely , you would use extensions like Ash.JsonApi or Ash.GraphQL with Phoenix to add external interfaces to those resources without having to write any extra code at all .
Ash is an open - source project and draws inspiration from similar ideas in other frameworks and concepts . The goal of Ash is to lower the barrier to adopting and using Elixir and Phoenix , and in doing so help these amazing communities attract new developers , projects , and companies .
## Example Resource
` ` ` elixir
defmodule Post do
use Ash.Resource
actions do
read :default
create :default
end
attributes do
attribute :name , :string
end
relationships do
belongs_to :author , Author
end
end
` ` `
2020-06-14 18:42:44 +12:00
For those looking to add ash extensions :
* see ` Ash.Dsl.Extension ` for adding configuration .
* If you are looking to write a new data source , also see the ` Ash.DataLayer ` documentation .
* If you are looking to write a new authorizer , see ` Ash.Authorizer `
* If you are looking to write a " front end " , something powered by Ash resources , a guide on
2020-06-14 18:39:11 +12:00
building those kinds of tools is in the works .
2019-12-05 20:18:13 +13:00
"""
2020-06-02 17:47:25 +12:00
alias Ash.Resource.Actions . { Create , Destroy , Read , Update }
alias Ash.Resource.Relationships . { BelongsTo , HasMany , HasOne , ManyToMany }
2020-01-14 07:16:24 +13:00
2019-10-07 09:36:06 +13:00
@type record :: struct
2020-05-26 16:45:10 +12:00
@type relationship_cardinality :: :many | :one
2019-10-07 09:36:06 +13:00
@type cardinality_one_relationship ( ) :: HasOne . t ( ) | BelongsTo . t ( )
@type cardinality_many_relationship ( ) :: HasMany . t ( ) | ManyToMany . t ( )
@type relationship :: cardinality_one_relationship ( ) | cardinality_many_relationship ( )
@type resource :: module
2019-12-05 20:18:13 +13:00
@type data_layer :: module
2020-05-14 03:54:44 +12:00
@type data_layer_query :: struct
2019-12-03 05:25:00 +13:00
@type api :: module
2019-10-07 09:36:06 +13:00
@type error :: struct
2019-11-29 19:54:11 +13:00
@type filter :: map ( )
2019-12-23 17:28:40 +13:00
@type params :: Keyword . t ( )
2019-11-30 05:36:01 +13:00
@type sort :: Keyword . t ( )
2019-11-28 10:36:25 +13:00
@type side_loads :: Keyword . t ( )
2020-06-14 18:39:11 +12:00
@type attribute :: Ash.Resource.Attribute . t ( )
2019-12-05 20:18:13 +13:00
@type action :: Create . t ( ) | Read . t ( ) | Update . t ( ) | Destroy . t ( )
2020-05-14 03:54:44 +12:00
@type query :: Ash.Query . t ( )
2020-05-21 10:59:58 +12:00
@type actor :: Ash . record ( )
2020-06-14 18:39:11 +12:00
require Ash.Dsl.Extension
alias Ash.Dsl.Extension
2020-05-02 21:01:57 +12:00
2020-06-04 18:16:41 +12:00
@doc " A list of authorizers to be used when accessing the resource "
@spec authorizers ( resource ( ) ) :: [ module ]
2020-05-21 10:59:58 +12:00
def authorizers ( resource ) do
2020-06-22 15:26:47 +12:00
{ resource , :authorizers }
|> :persistent_term . get ( [ ] )
|> List . wrap ( )
2019-10-03 20:18:07 +13:00
end
2020-06-04 18:16:41 +12:00
@doc " A list of field names corresponding to the primary key of a resource "
2020-06-02 15:23:33 +12:00
@spec primary_key ( resource ( ) ) :: list ( atom )
2019-11-03 09:36:46 +13:00
def primary_key ( resource ) do
2020-06-14 18:39:11 +12:00
:persistent_term . get ( { resource , :primary_key } , [ ] )
end
def relationships ( resource ) do
Extension . get_entities ( resource , [ :relationships ] )
2019-11-03 09:36:46 +13:00
end
2020-06-22 15:26:47 +12:00
@spec relationship ( any , any ) :: any
2020-06-04 18:16:41 +12:00
@doc " Gets a relationship by name from the resource "
2020-06-22 15:26:47 +12:00
def relationship ( resource , [ name ] ) do
relationship ( resource , name )
end
def relationship ( resource , [ name | rest ] ) do
case relationship ( resource , name ) do
nil ->
nil
relationship ->
relationship ( relationship . destination , rest )
end
end
2019-11-30 05:36:01 +13:00
def relationship ( resource , relationship_name ) when is_bitstring ( relationship_name ) do
2020-06-14 18:39:11 +12:00
resource
|> relationships ( )
|> Enum . find ( & ( to_string ( &1 . name ) == relationship_name ) )
2019-11-30 05:36:01 +13:00
end
2019-10-31 04:10:01 +13:00
def relationship ( resource , relationship_name ) do
2020-06-14 18:39:11 +12:00
resource
|> relationships ( )
|> Enum . find ( & ( &1 . name == relationship_name ) )
2019-10-04 15:33:55 +13:00
end
2020-06-15 18:40:33 +12:00
def implements_behaviour? ( module , behaviour ) do
2020-06-05 14:43:51 +12:00
:attributes
|> module . module_info ( )
2020-06-15 18:40:33 +12:00
|> Enum . flat_map ( fn
{ :behaviour , value } -> List . wrap ( value )
_ -> [ ]
end )
|> Enum . any? ( & ( &1 == behaviour ) )
end
@spec resource_module? ( module ) :: boolean
def resource_module? ( module ) do
implements_behaviour? ( module , Ash.Resource )
2020-06-05 14:43:51 +12:00
end
2020-06-04 18:16:41 +12:00
@doc false
2020-05-21 10:59:58 +12:00
def primary_action! ( resource , type ) do
case primary_action ( resource , type ) do
nil -> raise " Required primary #{ type } action for #{ inspect ( resource ) } "
action -> action
end
end
2020-06-04 18:16:41 +12:00
@doc " Returns the primary action of a given type for a resource "
2019-12-05 20:18:13 +13:00
@spec primary_action ( resource ( ) , atom ( ) ) :: action ( ) | nil
2019-11-25 13:01:21 +13:00
def primary_action ( resource , type ) do
resource
|> actions ( )
2019-11-28 10:36:25 +13:00
|> Enum . filter ( & ( &1 . type == type ) )
|> case do
[ action ] -> action
actions -> Enum . find ( actions , & &1 . primary? )
end
2019-11-25 13:01:21 +13:00
end
2020-06-14 18:39:11 +12:00
def actions ( resource ) do
Extension . get_entities ( resource , [ :actions ] )
end
2020-06-04 18:16:41 +12:00
@doc " Returns the action with the matching name and type on the resource "
2019-12-05 20:18:13 +13:00
@spec action ( resource ( ) , atom ( ) , atom ( ) ) :: action ( ) | nil
2019-11-28 10:36:25 +13:00
def action ( resource , name , type ) do
2020-06-14 18:39:11 +12:00
resource
|> actions ( )
|> Enum . find ( & ( &1 . name == name && &1 . type == type ) )
2019-10-31 04:10:01 +13:00
end
2020-06-14 18:39:11 +12:00
def attributes ( resource ) do
Extension . get_entities ( resource , [ :attributes ] )
end
def extensions ( resource ) do
:persistent_term . get ( { resource , :extensions } , [ ] )
2019-10-03 20:18:07 +13:00
end
2020-06-04 18:16:41 +12:00
@doc " Get an attribute name from the resource "
2019-12-05 20:18:13 +13:00
@spec attribute ( resource ( ) , String . t ( ) | atom ) :: attribute ( ) | nil
2019-11-30 05:36:01 +13:00
def attribute ( resource , name ) when is_bitstring ( name ) do
2020-06-14 18:39:11 +12:00
resource
|> attributes ( )
|> Enum . find ( & ( to_string ( &1 . name ) == name ) )
2019-11-30 05:36:01 +13:00
end
2019-11-28 10:36:25 +13:00
def attribute ( resource , name ) do
2020-06-14 18:39:11 +12:00
resource
|> attributes ( )
|> Enum . find ( & ( &1 . name == name ) )
2019-10-03 20:18:07 +13:00
end
2020-06-19 14:59:30 +12:00
def related ( resource , [ ] ) , do : resource
def related ( resource , [ path | rest ] ) do
case relationship ( resource , path ) do
%{ destination : destination } -> related ( destination , rest )
nil -> nil
end
end
2020-06-04 18:16:41 +12:00
@doc " The data layer of the resource, or nil if it does not have one "
2019-12-05 20:18:13 +13:00
@spec data_layer ( resource ( ) ) :: data_layer ( )
2019-10-07 09:36:06 +13:00
def data_layer ( resource ) do
2020-06-14 18:39:11 +12:00
:persistent_term . get ( { resource , :data_layer } )
2019-10-07 09:36:06 +13:00
end
2020-06-04 18:16:41 +12:00
@doc false
@spec data_layer_can? ( resource ( ) , Ash.DataLayer . feature ( ) ) :: boolean
def data_layer_can? ( resource , feature ) do
data_layer = data_layer ( resource )
data_layer && Ash.DataLayer . can? ( feature , resource )
end
@doc false
@spec data_layer_filters ( resource ) :: map
def data_layer_filters ( resource ) do
Ash.DataLayer . custom_filters ( resource )
end
2019-10-03 16:08:36 +13:00
end