mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 05:23:03 +12:00
WIP on cleanup/documentation
This commit is contained in:
parent
df70095e39
commit
030c389225
13 changed files with 174 additions and 67 deletions
|
@ -31,5 +31,7 @@
|
||||||
* make ets dep optional
|
* make ets dep optional
|
||||||
* Bake in descriptions to the DSL
|
* Bake in descriptions to the DSL
|
||||||
* Contributor guideline and code of conduct
|
* Contributor guideline and code of conduct
|
||||||
|
* Do branch analysis of each record after authorizing it, in authorizer
|
||||||
|
* consider moving `type` and `name` for resources out into json api (or perhaps just `name`) since only json api uses that
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
use Mix.Config
|
|
||||||
|
|
||||||
if Mix.env() == :test do
|
|
||||||
config :ash,
|
|
||||||
resources: [
|
|
||||||
Ash.Test.Post
|
|
||||||
]
|
|
||||||
end
|
|
26
lib/ash.ex
26
lib/ash.ex
|
@ -1,5 +1,12 @@
|
||||||
defmodule Ash do
|
defmodule Ash do
|
||||||
|
@moduledoc """
|
||||||
|
The primary interface for interrogating apis and resources, an
|
||||||
|
|
||||||
|
This is not the code level interface for a resource. Instead, call functions
|
||||||
|
on an `Api` module that contains those resources.
|
||||||
|
"""
|
||||||
alias Ash.Resource.Relationships.{BelongsTo, HasOne, HasMany, ManyToMany}
|
alias Ash.Resource.Relationships.{BelongsTo, HasOne, HasMany, ManyToMany}
|
||||||
|
alias Ash.Resource.Actions.{Create, Read, Update, Destroy}
|
||||||
|
|
||||||
@type record :: struct
|
@type record :: struct
|
||||||
@type cardinality_one_relationship() :: HasOne.t() | BelongsTo.t()
|
@type cardinality_one_relationship() :: HasOne.t() | BelongsTo.t()
|
||||||
|
@ -7,20 +14,27 @@ defmodule Ash do
|
||||||
@type relationship :: cardinality_one_relationship() | cardinality_many_relationship()
|
@type relationship :: cardinality_one_relationship() | cardinality_many_relationship()
|
||||||
@type query :: struct
|
@type query :: struct
|
||||||
@type resource :: module
|
@type resource :: module
|
||||||
|
@type data_layer :: module
|
||||||
@type api :: module
|
@type api :: module
|
||||||
@type error :: struct
|
@type error :: struct
|
||||||
@type filter :: map()
|
@type filter :: map()
|
||||||
@type sort :: Keyword.t()
|
@type sort :: Keyword.t()
|
||||||
@type side_loads :: Keyword.t()
|
@type side_loads :: Keyword.t()
|
||||||
|
@type attribute :: Ash.Attributes.Attribute.t()
|
||||||
|
@type action :: Create.t() | Read.t() | Update.t() | Destroy.t()
|
||||||
|
@type side_load_config :: Keyword.t()
|
||||||
|
|
||||||
|
@spec resources(api) :: list(resource())
|
||||||
def resources(api) do
|
def resources(api) do
|
||||||
api.resources()
|
api.resources()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec primary_key(resource()) :: nil | attribute() | list(attribute)
|
||||||
def primary_key(resource) do
|
def primary_key(resource) do
|
||||||
resource.primary_key()
|
resource.primary_key()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec relationship(resource(), atom() | String.t()) :: relationship() | nil
|
||||||
def relationship(resource, relationship_name) when is_bitstring(relationship_name) do
|
def relationship(resource, relationship_name) when is_bitstring(relationship_name) do
|
||||||
Enum.find(resource.relationships(), &(to_string(&1.name) == relationship_name))
|
Enum.find(resource.relationships(), &(to_string(&1.name) == relationship_name))
|
||||||
end
|
end
|
||||||
|
@ -29,14 +43,17 @@ defmodule Ash do
|
||||||
Enum.find(resource.relationships(), &(&1.name == relationship_name))
|
Enum.find(resource.relationships(), &(&1.name == relationship_name))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec relationships(resource()) :: list(relationship())
|
||||||
def relationships(resource) do
|
def relationships(resource) do
|
||||||
resource.relationships()
|
resource.relationships()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec side_load_config(api()) :: side_load_config()
|
||||||
def side_load_config(api) do
|
def side_load_config(api) do
|
||||||
api.side_load_config()
|
api.side_load_config()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec primary_action(resource(), atom()) :: action() | nil
|
||||||
def primary_action(resource, type) do
|
def primary_action(resource, type) do
|
||||||
resource
|
resource
|
||||||
|> actions()
|
|> actions()
|
||||||
|
@ -47,14 +64,17 @@ defmodule Ash do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec action(resource(), atom(), atom()) :: action() | nil
|
||||||
def action(resource, name, type) do
|
def action(resource, name, type) do
|
||||||
Enum.find(resource.actions(), &(&1.name == name && &1.type == type))
|
Enum.find(resource.actions(), &(&1.name == name && &1.type == type))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec actions(resource()) :: list(action())
|
||||||
def actions(resource) do
|
def actions(resource) do
|
||||||
resource.actions()
|
resource.actions()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec attribute(resource(), String.t() | atom) :: attribute() | nil
|
||||||
def attribute(resource, name) when is_bitstring(name) do
|
def attribute(resource, name) when is_bitstring(name) do
|
||||||
Enum.find(resource.attributes, &(to_string(&1.name) == name))
|
Enum.find(resource.attributes, &(to_string(&1.name) == name))
|
||||||
end
|
end
|
||||||
|
@ -63,26 +83,32 @@ defmodule Ash do
|
||||||
Enum.find(resource.attributes, &(&1.name == name))
|
Enum.find(resource.attributes, &(&1.name == name))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec attributes(resource()) :: list(attribute())
|
||||||
def attributes(resource) do
|
def attributes(resource) do
|
||||||
resource.attributes()
|
resource.attributes()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec name(resource()) :: String.t()
|
||||||
def name(resource) do
|
def name(resource) do
|
||||||
resource.name()
|
resource.name()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec type(resource()) :: String.t()
|
||||||
def type(resource) do
|
def type(resource) do
|
||||||
resource.type()
|
resource.type()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec max_page_size(api(), resource()) :: non_neg_integer() | nil
|
||||||
def max_page_size(api, resource) do
|
def max_page_size(api, resource) do
|
||||||
min(api.max_page_size(), resource.max_page_size())
|
min(api.max_page_size(), resource.max_page_size())
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec default_page_size(api(), resource()) :: non_neg_integer() | nil
|
||||||
def default_page_size(api, resource) do
|
def default_page_size(api, resource) do
|
||||||
min(api.default_page_size(), resource.default_page_size())
|
min(api.default_page_size(), resource.default_page_size())
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec data_layer(resource()) :: data_layer()
|
||||||
def data_layer(resource) do
|
def data_layer(resource) do
|
||||||
resource.data_layer()
|
resource.data_layer()
|
||||||
end
|
end
|
||||||
|
|
4
lib/ash/constraints.ex
Normal file
4
lib/ash/constraints.ex
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
defmodule Ash.Constraints do
|
||||||
|
def positive?(integer), do: integer >= 0
|
||||||
|
def greater_than_zero?(integer), do: integer > 0
|
||||||
|
end
|
|
@ -1,6 +1,10 @@
|
||||||
defmodule Ash.Error.ResourceDslError do
|
defmodule Ash.Error.ResourceDslError do
|
||||||
defexception [:message, :path, :option, :resource]
|
defexception [:message, :path, :option, :resource]
|
||||||
|
|
||||||
|
def message(%{message: message, path: nil, option: option, resource: resource}) do
|
||||||
|
"#{inspect(resource)}: #{option} #{message}"
|
||||||
|
end
|
||||||
|
|
||||||
def message(%{message: message, path: dsl_path, option: nil, resource: resource}) do
|
def message(%{message: message, path: dsl_path, option: nil, resource: resource}) do
|
||||||
dsl_path = Enum.join(dsl_path, "->")
|
dsl_path = Enum.join(dsl_path, "->")
|
||||||
"#{inspect(resource)}: #{message} at #{dsl_path}"
|
"#{inspect(resource)}: #{message} at #{dsl_path}"
|
||||||
|
|
|
@ -1,45 +1,118 @@
|
||||||
defmodule Ash.Resource do
|
defmodule Ash.Resource do
|
||||||
|
@primary_key_schema Ashton.schema(
|
||||||
|
opts: [field: :atom, type: :atom],
|
||||||
|
defaults: [field: :id, type: :uuid],
|
||||||
|
describe: [
|
||||||
|
field: "The field name of the primary key of the resource.",
|
||||||
|
type: "The data type of the primary key of the resource."
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
@resource_opts_schema Ashton.schema(
|
||||||
|
opts: [
|
||||||
|
name: :string,
|
||||||
|
type: :string,
|
||||||
|
max_page_size: :integer,
|
||||||
|
default_page_size: :integer,
|
||||||
|
primary_key: [
|
||||||
|
:boolean,
|
||||||
|
@primary_key_schema
|
||||||
|
]
|
||||||
|
],
|
||||||
|
describe: [
|
||||||
|
name:
|
||||||
|
"The name of the resource. This will typically be the pluralized form of the type",
|
||||||
|
type:
|
||||||
|
"The type of the resource, e.g `post` or `author`. This is used throughout the system.",
|
||||||
|
max_page_size:
|
||||||
|
"The maximum page size for any read action. Any request for a higher page size will simply use this number.",
|
||||||
|
default_page_size:
|
||||||
|
"The default page size for any read action. If no page size is specified, this value is used.",
|
||||||
|
primary_key:
|
||||||
|
"If true, a default `id` uuid primary key is used. If false, none is created. See the primary_key opts for info on specifying primary key options."
|
||||||
|
],
|
||||||
|
required: [:name, :type],
|
||||||
|
defaults: [
|
||||||
|
primary_key: true
|
||||||
|
],
|
||||||
|
constraints: [
|
||||||
|
max_page_size:
|
||||||
|
{&Ash.Constraints.greater_than_zero?/1, "must be greater than zero"},
|
||||||
|
default_page_size:
|
||||||
|
{&Ash.Constraints.greater_than_zero?/1, "must be greater than zero"}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
@moduledoc """
|
||||||
|
The entry point for creating an `Ash.Resource`.
|
||||||
|
|
||||||
|
This brings in the top level DSL macros, defines module attributes for aggregating state as
|
||||||
|
DSL functions are called, and defines a set of functions internal to the resource that can be
|
||||||
|
used to inspect them.
|
||||||
|
|
||||||
|
Simply add `use Ash.Resource, ...` at the top of your resource module, and refer to the DSL documentation
|
||||||
|
at `Ash.Resource.DSL` for the rest. The options for `use Ash.Resource` are described below.
|
||||||
|
|
||||||
|
#{Ashton.document(@resource_opts_schema)}
|
||||||
|
|
||||||
|
Note:
|
||||||
|
*Do not* call the functions on a resource, as in `MyResource.type()` as this is a *private*
|
||||||
|
API and can change at any time. Instead, use the `Ash` module, for example: `Ash.type(MyResource)`
|
||||||
|
"""
|
||||||
|
|
||||||
defmacro __using__(opts) do
|
defmacro __using__(opts) do
|
||||||
quote do
|
quote do
|
||||||
@before_compile Ash.Resource
|
@before_compile Ash.Resource
|
||||||
@skip_data_layer unquote(Keyword.get(opts, :no_data_layer, false))
|
|
||||||
|
|
||||||
Module.register_attribute(__MODULE__, :before_compile_hooks, accumulate: true)
|
opts = Ash.Resource.validate_use_opts(__MODULE__, unquote(opts))
|
||||||
|
Ash.Resource.define_resource_module_attributes(__MODULE__, opts)
|
||||||
|
Ash.Resource.define_primary_key(__MODULE__, opts)
|
||||||
|
|
||||||
Module.register_attribute(__MODULE__, :actions, accumulate: true)
|
use Ash.Resource.DSL
|
||||||
Module.register_attribute(__MODULE__, :attributes, accumulate: true)
|
end
|
||||||
Module.register_attribute(__MODULE__, :relationships, accumulate: true)
|
|
||||||
Module.register_attribute(__MODULE__, :mix_ins, accumulate: true)
|
|
||||||
|
|
||||||
if unquote(Keyword.get(opts, :primary_key?, true)) do
|
|
||||||
@attributes Ash.Resource.Attributes.Attribute.new(__MODULE__, :id, :uuid,
|
|
||||||
primary_key?: true
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Module.put_attribute(__MODULE__, :custom_threshold_for_lib, 10)
|
@doc false
|
||||||
import Ash.Resource
|
def define_resource_module_attributes(mod, opts) do
|
||||||
import Ash.Resource.Actions, only: [actions: 1]
|
Module.register_attribute(mod, :before_compile_hooks, accumulate: true)
|
||||||
import Ash.Resource.Attributes, only: [attributes: 1]
|
Module.register_attribute(mod, :actions, accumulate: true)
|
||||||
import Ash.Resource.Relationships, only: [relationships: 1]
|
Module.register_attribute(mod, :attributes, accumulate: true)
|
||||||
import Ash.Authorization.Rule
|
Module.register_attribute(mod, :relationships, accumulate: true)
|
||||||
|
Module.register_attribute(mod, :mix_ins, accumulate: true)
|
||||||
|
|
||||||
name = unquote(opts[:name])
|
Module.put_attribute(mod, :name, opts[:name])
|
||||||
resource_type = unquote(opts[:type])
|
Module.put_attribute(mod, :resource_type, opts[:type])
|
||||||
|
Module.put_attribute(mod, :max_page_size, opts[:max_page_size])
|
||||||
@name name
|
Module.put_attribute(mod, :default_page_size, opts[:default_page_size])
|
||||||
@resource_type resource_type
|
|
||||||
@max_page_size nil
|
|
||||||
@default_page_size nil
|
|
||||||
|
|
||||||
unless @name do
|
|
||||||
raise "Must set name"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
unless @resource_type do
|
@doc false
|
||||||
raise "Must set resource type"
|
def define_primary_key(mod, opts) do
|
||||||
|
case opts[:primary_key] do
|
||||||
|
true ->
|
||||||
|
attribute = Ash.Resource.Attributes.Attribute.new(mod, :id, :uuid, primary_key?: true)
|
||||||
|
Module.put_attribute(mod, :attributes, attribute)
|
||||||
|
|
||||||
|
false ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
opts ->
|
||||||
|
attribute =
|
||||||
|
Ash.Resource.Attributes.Attribute.new(mod, opts[:field], opts[:type], primary_key?: true)
|
||||||
|
|
||||||
|
Module.put_attribute(mod, :attributes, attribute)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def validate_use_opts(mod, opts) do
|
||||||
|
case Ashton.validate(opts, @resource_opts_schema) do
|
||||||
|
{:error, [{key, message} | _]} ->
|
||||||
|
raise Ash.Error.ResourceDslError, resource: mod, option: key, message: message
|
||||||
|
|
||||||
|
{:ok, opts} ->
|
||||||
|
opts
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defmacro __before_compile__(env) do
|
defmacro __before_compile__(env) do
|
||||||
|
@ -91,16 +164,8 @@ defmodule Ash.Resource do
|
||||||
@default_page_size
|
@default_page_size
|
||||||
end
|
end
|
||||||
|
|
||||||
unless @skip_data_layer || @data_layer do
|
|
||||||
raise "Must `use` a data layer module or pass `no_data_layer: true`"
|
|
||||||
end
|
|
||||||
|
|
||||||
def data_layer() do
|
def data_layer() do
|
||||||
if @skip_data_layer do
|
@data_layer || false
|
||||||
false
|
|
||||||
else
|
|
||||||
@data_layer
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
Enum.map(@mix_ins || [], fn hook_module ->
|
Enum.map(@mix_ins || [], fn hook_module ->
|
||||||
|
@ -110,18 +175,6 @@ defmodule Ash.Resource do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defmacro max_page_size(page_size) do
|
|
||||||
quote do
|
|
||||||
@max_page_size unquote(page_size)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defmacro default_page_size(page_size) do
|
|
||||||
quote do
|
|
||||||
@default_page_size unquote(page_size)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
def primary_key(attributes) do
|
def primary_key(attributes) do
|
||||||
attributes
|
attributes
|
||||||
|
|
|
@ -1,12 +1,22 @@
|
||||||
defmodule Ash.Resource.Attributes.Attribute do
|
defmodule Ash.Resource.Attributes.Attribute do
|
||||||
|
@moduledoc """
|
||||||
|
The struct containing information about an attribute.
|
||||||
|
See the DSL documentation for more information on their usage in the DSL
|
||||||
|
"""
|
||||||
|
|
||||||
defstruct [:name, :type, :primary_key?]
|
defstruct [:name, :type, :primary_key?]
|
||||||
|
|
||||||
@builtins Ash.Type.builtins()
|
@type t :: %__MODULE__{
|
||||||
|
name: atom(),
|
||||||
|
type: Ash.type(),
|
||||||
|
primary_key?: boolean()
|
||||||
|
}
|
||||||
|
|
||||||
@option_schema Ashton.schema(opts: [primary_key?: :boolean])
|
@builtins Ash.Type.builtins()
|
||||||
|
@schema Ashton.schema(opts: [primary_key?: :boolean], defaults: [primary_key?: false])
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
def attribute_schema(), do: @option_schema
|
def attribute_schema(), do: @schema
|
||||||
|
|
||||||
def new(resource, name, type, opts \\ [])
|
def new(resource, name, type, opts \\ [])
|
||||||
|
|
||||||
|
@ -25,7 +35,7 @@ defmodule Ash.Resource.Attributes.Attribute do
|
||||||
end
|
end
|
||||||
|
|
||||||
def new(resource, name, type, opts) when type in @builtins do
|
def new(resource, name, type, opts) when type in @builtins do
|
||||||
case Ashton.validate(opts, @option_schema) do
|
case Ashton.validate(opts, @schema) do
|
||||||
{:error, [{key, message} | _]} ->
|
{:error, [{key, message} | _]} ->
|
||||||
raise Ash.Error.ResourceDslError,
|
raise Ash.Error.ResourceDslError,
|
||||||
resource: resource,
|
resource: resource,
|
||||||
|
|
19
lib/ash/resource/dsl.ex
Normal file
19
lib/ash/resource/dsl.ex
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
defmodule Ash.Resource.DSL do
|
||||||
|
@moduledoc """
|
||||||
|
The entrypoint for the Ash DSL documentation and interface.
|
||||||
|
|
||||||
|
Available DSL sections:
|
||||||
|
|
||||||
|
* `actions` - `Ash.Resource.Actions`
|
||||||
|
* `attributes` - `Ash.Resource.Attributes`
|
||||||
|
* `relationships` - `Ash.Resource.Relationships`
|
||||||
|
"""
|
||||||
|
|
||||||
|
defmacro __using__(_) do
|
||||||
|
quote do
|
||||||
|
import Ash.Resource.Actions, only: [actions: 1]
|
||||||
|
import Ash.Resource.Attributes, only: [attributes: 1]
|
||||||
|
import Ash.Resource.Relationships, only: [relationships: 1]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,2 +0,0 @@
|
||||||
defmodule Ash.Test do
|
|
||||||
end
|
|
2
mix.exs
2
mix.exs
|
@ -43,7 +43,7 @@ defmodule Ash.MixProject do
|
||||||
{:ecto, "~> 3.0"},
|
{:ecto, "~> 3.0"},
|
||||||
{:ets, github: "zachdaniel/ets", ref: "b96da05e75926e340e8a0fdfea9c095d97ed8d50"},
|
{:ets, github: "zachdaniel/ets", ref: "b96da05e75926e340e8a0fdfea9c095d97ed8d50"},
|
||||||
{:ex_doc, "~> 0.21", only: :dev, runtime: false},
|
{:ex_doc, "~> 0.21", only: :dev, runtime: false},
|
||||||
{:ashton, "~> 0.3.6"}
|
{:ashton, "~> 0.3.9"}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
2
mix.lock
2
mix.lock
|
@ -1,5 +1,5 @@
|
||||||
%{
|
%{
|
||||||
"ashton": {:hex, :ashton, "0.3.6", "95f5d598c2e05662498349d81e7579e897b1e1cfe1aa79606d07a35305a47efe", [:mix], [], "hexpm"},
|
"ashton": {:hex, :ashton, "0.3.7", "9349c196fb4302a08d5e19748f1af74bbe891b15c9a2ec11184f69ec1912f478", [:mix], [], "hexpm"},
|
||||||
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"},
|
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"},
|
||||||
"dataloader": {:hex, :dataloader, "1.0.6", "fb724d6d3fb6acb87d27e3b32dea3a307936ad2d245faf9cf5221d1323d6a4ba", [:mix], [{:ecto, ">= 0.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
|
"dataloader": {:hex, :dataloader, "1.0.6", "fb724d6d3fb6acb87d27e3b32dea3a307936ad2d245faf9cf5221d1323d6a4ba", [:mix], [{:ecto, ">= 0.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
|
||||||
"db_connection": {:hex, :db_connection, "2.1.1", "a51e8a2ee54ef2ae6ec41a668c85787ed40cb8944928c191280fe34c15b76ae5", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"},
|
"db_connection": {:hex, :db_connection, "2.1.1", "a51e8a2ee54ef2ae6ec41a668c85787ed40cb8944928c191280fe34c15b76ae5", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
defmodule Ash.Test.Actions.ReadTest do
|
defmodule Ash.Test.Actions.ReadTest do
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: true
|
||||||
# import Ash.Test
|
|
||||||
|
|
||||||
defmodule Post do
|
defmodule Post do
|
||||||
use Ash.Resource, name: "posts", type: "post"
|
use Ash.Resource, name: "posts", type: "post"
|
||||||
|
|
0
test/dsl/resource/top_level_test.exs
Normal file
0
test/dsl/resource/top_level_test.exs
Normal file
Loading…
Reference in a new issue