mirror of
https://github.com/ash-project/ash.git
synced 2024-09-19 21:13:10 +12:00
improvement: move docs out of priv, update spark
This commit is contained in:
parent
c2a69b93ba
commit
d7a9ff7996
40 changed files with 6 additions and 767 deletions
|
@ -280,8 +280,7 @@ defmodule Ash.Actions.Create do
|
|||
changeset =
|
||||
Ash.Changeset.require_values(
|
||||
changeset,
|
||||
:create,
|
||||
true
|
||||
:create
|
||||
)
|
||||
|> Ash.Changeset.require_values(
|
||||
:update,
|
||||
|
|
|
@ -4,7 +4,6 @@ defmodule Ash.DocIndex do
|
|||
"""
|
||||
|
||||
use Spark.DocIndex,
|
||||
otp_app: :ash,
|
||||
guides_from: [
|
||||
"documentation/**/*.md"
|
||||
]
|
||||
|
@ -13,23 +12,6 @@ defmodule Ash.DocIndex do
|
|||
@spec for_library() :: String.t()
|
||||
def for_library, do: "ash"
|
||||
|
||||
@overview Spark.DocIndex.read!(:ash, "documentation/topics/overview.md")
|
||||
|
||||
@impl true
|
||||
def guides do
|
||||
guides = Enum.reject(super(), &(&1.name == "Overview"))
|
||||
|
||||
[
|
||||
%{
|
||||
name: "Overview",
|
||||
category: "Topics",
|
||||
text: @overview,
|
||||
route: "topics/overview.md"
|
||||
}
|
||||
| guides
|
||||
]
|
||||
end
|
||||
|
||||
@impl true
|
||||
def default_guide, do: "Overview"
|
||||
|
||||
|
|
6
mix.exs
6
mix.exs
|
@ -30,7 +30,7 @@ defmodule Ash.MixProject do
|
|||
end
|
||||
|
||||
defp extras() do
|
||||
"priv/documentation/**/*.md"
|
||||
"documentation/**/*.md"
|
||||
|> Path.wildcard()
|
||||
|> Enum.map(fn path ->
|
||||
title =
|
||||
|
@ -55,7 +55,7 @@ defmodule Ash.MixProject do
|
|||
end
|
||||
|
||||
defp groups_for_extras() do
|
||||
"priv/documentation/*"
|
||||
"documentation/*"
|
||||
|> Path.wildcard()
|
||||
|> Enum.map(fn folder ->
|
||||
name =
|
||||
|
@ -153,6 +153,8 @@ defmodule Ash.MixProject do
|
|||
[
|
||||
name: :ash,
|
||||
licenses: ["MIT"],
|
||||
files: ~w(lib .formatter.exs mix.exs README* LICENSE*
|
||||
CHANGELOG* documentation),
|
||||
links: %{
|
||||
GitHub: "https://github.com/ash-project/ash"
|
||||
}
|
||||
|
|
2
mix.lock
2
mix.lock
|
@ -36,7 +36,7 @@
|
|||
"providers": {:hex, :providers, "1.8.1", "70b4197869514344a8a60e2b2a4ef41ca03def43cfb1712ecf076a0f3c62f083", [:rebar3], [{:getopt, "1.0.1", [hex: :getopt, repo: "hexpm", optional: false]}], "hexpm", "e45745ade9c476a9a469ea0840e418ab19360dc44f01a233304e118a44486ba0"},
|
||||
"sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"},
|
||||
"sourceror": {:hex, :sourceror, "0.11.2", "549ce48be666421ac60cfb7f59c8752e0d393baa0b14d06271d3f6a8c1b027ab", [:mix], [], "hexpm", "9ab659118896a36be6eec68ff7b0674cba372fc8e210b1e9dc8cf2b55bb70dfb"},
|
||||
"spark": {:hex, :spark, "0.1.11", "7415e7775d4cc9ed898bf082e60cfa2bdebd71a690c09b5f92b6f8f05d6be992", [:mix], [{:libgraph, "~> 0.13.3", [hex: :libgraph, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "1c4df53d53f952a8f6cb7876b6499032870f0eceb8ef55e48aa0b3bc61a5eba0"},
|
||||
"spark": {:hex, :spark, "0.1.15", "299c7b9a8d1d2c994940ff851370892e8484135abec47ab7ae71c4a731e759b4", [:mix], [{:libgraph, "~> 0.13.3", [hex: :libgraph, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "529ec5946186e3976de75ec979fc8b8fb079af2a927c624214cb1f0dd4653118"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
|
||||
"stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"},
|
||||
"telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"},
|
||||
|
|
|
@ -1,396 +0,0 @@
|
|||
# Getting Started Tutorial
|
||||
|
||||
This tutorial will walk you through creating a very simple application that uses
|
||||
Ash. The finished application will look like this:
|
||||
https://github.com/mario-mazo/my_app
|
||||
|
||||
## Creating an application
|
||||
|
||||
The first step is to [create an application](https://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html).
|
||||
|
||||
```shell
|
||||
mix new my_app
|
||||
```
|
||||
|
||||
Note: alternatively you create a phoenix application with `mix phx.new` (which is covered in more detail in the [next guide](getting_started_phx.html)).
|
||||
|
||||
## Add Ash
|
||||
|
||||
Add `ash` to your dependencies in `mix.exs`. The latest version can be found by running `mix hex.info ash`.
|
||||
|
||||
```elixir
|
||||
# in mix.exs
|
||||
def deps() do
|
||||
[
|
||||
{:ash, "~> x.x.x"}
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
If you want to have a more idiomatic formatting (like the formatting used in the
|
||||
documentation) of your Ash resource and APIs, you need to add `:ash` (and any other
|
||||
extensions you use like `:ash_postgres`) to your `.formatter.exs` otherwise the
|
||||
default Elixir formatter will wrap portions of the DSL in parenthesis.
|
||||
|
||||
```elixir
|
||||
import_deps: [
|
||||
:ash # add this line
|
||||
]
|
||||
```
|
||||
|
||||
Without that, instead of:
|
||||
|
||||
```elixir
|
||||
attribute :id, :integer, allow_nil?: true
|
||||
```
|
||||
|
||||
the Elixir formatter will change it to:
|
||||
|
||||
```elixir
|
||||
attribute(:id, :integer, allow_nil?: true)
|
||||
```
|
||||
|
||||
## Create an Ash API
|
||||
|
||||
Create an API module. This will be your primary way to interact with your Ash resources. We recommend `lib/my_app/api.ex` for simple setups.
|
||||
|
||||
```elixir
|
||||
# lib/my_app/api.ex
|
||||
defmodule MyApp.Api do
|
||||
use Ash.Api
|
||||
|
||||
resources do
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Create a registry
|
||||
|
||||
The registry is in charge of keeping track of the resources available to an api.
|
||||
|
||||
```elixir
|
||||
# lib/my_app/registry.ex
|
||||
defmodule MyApp.Registry do
|
||||
use Ash.Registry,
|
||||
extensions: [Ash.Registry.ResourceValidations]
|
||||
|
||||
entries do
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Refer to that registry in your api
|
||||
|
||||
```elixir
|
||||
# lib/my_app/api.ex
|
||||
defmodule MyApp.Api do
|
||||
use Ash.Api
|
||||
|
||||
resources do
|
||||
registry MyApp.Registry
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Create a resource
|
||||
|
||||
A resource is the primary entity in Ash. Your API module ties your resources together and gives them an interface, but the vast majority of your configuration will live in resources.
|
||||
|
||||
In your typical setup, you might have a resource per database table. For those already familiar with [Ecto](https://github.com/elixir-ecto/ecto), a resource and an Ecto schema are very similar. In fact, all resources define an Ecto schema under the hood. This can be leveraged when you need to do things that are not yet implemented or fall outside of the scope of Ash. The current recommendation for where to put your resources is in `lib/my_app/resources/<resource_name>.ex`. Here are a few examples:
|
||||
|
||||
```elixir
|
||||
# in lib/my_app/resources/tweet.ex
|
||||
defmodule MyApp.Tweet do
|
||||
use Ash.Resource
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
attribute :body, :string do
|
||||
allow_nil? false
|
||||
constraints max_length: 255
|
||||
end
|
||||
|
||||
# Alternatively, you can use the keyword list syntax
|
||||
# You can also set functional defaults, via passing in a zero
|
||||
# argument function or an MFA
|
||||
attribute :public, :boolean, allow_nil?: false, default: false
|
||||
|
||||
# This is set on create
|
||||
create_timestamp :inserted_at
|
||||
# This is updated on all updates
|
||||
update_timestamp :updated_at
|
||||
|
||||
# `create_timestamp` above is just shorthand for:
|
||||
# attribute :inserted_at, :utc_datetime_usec,
|
||||
# private?: true,
|
||||
# writable?: false,
|
||||
# default: &DateTime.utc_now/0
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# in lib/my_app/resources/user.ex
|
||||
defmodule MyApp.User do
|
||||
use Ash.Resource
|
||||
|
||||
attributes do
|
||||
attribute :email, :string,
|
||||
allow_nil?: false,
|
||||
constraints: [
|
||||
# Note: This regex is just an example
|
||||
match: ~r/^[\w.!#$%&’*+\-\/=?\^`{|}~]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*$/i
|
||||
]
|
||||
|
||||
uuid_primary_key :id
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
For full details on defining a resource, see: `Ash.Resource.Dsl`.
|
||||
|
||||
## Add resources to your API
|
||||
|
||||
Alter your Registry (`lib/my_app/registry.ex`) to add the resources we created on the previous step:
|
||||
|
||||
```elixir
|
||||
entries do
|
||||
entry MyApp.User
|
||||
entry MyApp.Tweet
|
||||
end
|
||||
```
|
||||
|
||||
### Test the resources
|
||||
|
||||
Now you are able to create changesets for your resources using `Ash.Changeset.new/2`:
|
||||
|
||||
```elixir
|
||||
iex(7)> changeset = Ash.Changeset.new(MyApp.User, %{email: "ash.man@enguento.com"})
|
||||
#Ash.Changeset<
|
||||
action_type: :create,
|
||||
attributes: %{email: "ash.man@enguento.com"},
|
||||
relationships: %{},
|
||||
errors: [],
|
||||
data: %MyApp.User{
|
||||
__meta__: #Ecto.Schema.Metadata<:built, "">,
|
||||
__metadata__: %{},
|
||||
aggregates: %{},
|
||||
calculations: %{},
|
||||
email: nil,
|
||||
id: nil
|
||||
},
|
||||
valid?: true
|
||||
>
|
||||
```
|
||||
|
||||
If you try to use an invalid email (the email regex is for demonstration purposes only)
|
||||
an error will be returned:
|
||||
|
||||
```elixir
|
||||
iex(6)> changeset = Ash.Changeset.new(MyApp.User, %{email: "@eng.com"})
|
||||
#Ash.Changeset<
|
||||
action_type: :create,
|
||||
attributes: %{},
|
||||
relationships: %{},
|
||||
errors: [
|
||||
%Ash.Error.Changes.InvalidAttribute{
|
||||
class: :invalid,
|
||||
field: :email,
|
||||
message: {"must match the pattern %{regex}",
|
||||
[
|
||||
regex: "~r/^[\\w.!#$%&’*+\\-\\/=?\\^`{|}~]+@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*$/i"
|
||||
]},
|
||||
path: [],
|
||||
stacktrace: #Stacktrace<>
|
||||
}
|
||||
],
|
||||
data: %MyApp.User{
|
||||
__meta__: #Ecto.Schema.Metadata<:built, "">,
|
||||
__metadata__: %{},
|
||||
aggregates: %{},
|
||||
calculations: %{},
|
||||
email: nil,
|
||||
id: nil
|
||||
},
|
||||
valid?: false
|
||||
>
|
||||
```
|
||||
|
||||
## Add your data layer
|
||||
|
||||
To be able to store and later on read your resources, a _data layer_ is required. For more information, see the documentation for the data layer you would like to use. The currently supported data layers are listed below:
|
||||
|
||||
| Storage | Datalayer | Storage Documentation |
|
||||
| -------- | -------------------------------------------------------- | ------------------------------------------------------------------------ |
|
||||
| postgres | [AshPostgres.DataLayer](https://hexdocs.pm/ash_postgres) | [Postgres Documentation](https://www.postgresql.org/docs/) |
|
||||
| csv | [AshCsv.DataLayer](https://hexdocs.pm/ash_csv) | [CSV Information](https://en.wikipedia.org/wiki/Comma-separated_values) |
|
||||
| ets | `Ash.DataLayer.Ets` | [Erlang Term Storage Documentation](https://erlang.org/doc/man/ets.html) |
|
||||
| mnesia | `Ash.DataLayer.Mnesia` | [Mnesia Documentation](https://erlang.org/doc/man/mnesia.html) |
|
||||
|
||||
To add a data layer, we need to add it to the `use Ash.Resource` statement. In
|
||||
this case we are going to use ETS which is a in-memory data layer that is built
|
||||
into the BEAM and works well for testing purposes.
|
||||
|
||||
```elixir
|
||||
# in both lib/my_app/resources/user.ex
|
||||
# and lib/my_app/resources/tweet.ex
|
||||
|
||||
use Ash.Resource, data_layer: Ash.DataLayer.Ets
|
||||
```
|
||||
|
||||
## Add actions to enable functionality
|
||||
|
||||
Actions are the primary driver for adding specific interactions to your resource.
|
||||
You can read the about `Ash.Resource.Dsl` [actions](Ash.Resource.Dsl.html#module-actions)
|
||||
to learn how to customize the functionality. For now we will enable all of them with default implementations by adding the following block to your resources:
|
||||
|
||||
```elixir
|
||||
# in both lib/my_app/resources/user.ex
|
||||
# and lib/my_app/resources/tweet.ex
|
||||
|
||||
actions do
|
||||
create :create
|
||||
read :read
|
||||
update :update
|
||||
destroy :destroy
|
||||
end
|
||||
```
|
||||
|
||||
### Test functionality
|
||||
|
||||
Now you should be able to use your API to do CRUD operations on your resources.
|
||||
|
||||
#### Create resource
|
||||
|
||||
```elixir
|
||||
iex(1)> user_changeset = Ash.Changeset.new(MyApp.User, %{email: "ash.man@enguento.co
|
||||
m"})
|
||||
#Ash.Changeset<
|
||||
action_type: :create,
|
||||
attributes: %{email: "ash.man@enguento.com"},
|
||||
relationships: %{},
|
||||
errors: [],
|
||||
data: %MyApp.User{
|
||||
__meta__: #Ecto.Schema.Metadata<:built, "">,
|
||||
__metadata__: %{},
|
||||
aggregates: %{},
|
||||
calculations: %{},
|
||||
email: nil,
|
||||
id: nil
|
||||
},
|
||||
valid?: true
|
||||
>
|
||||
iex(2)> MyApp.Api.create(user_changeset)
|
||||
{:ok,
|
||||
%MyApp.User{
|
||||
__meta__: #Ecto.Schema.Metadata<:built, "">,
|
||||
__metadata__: %{},
|
||||
aggregates: %{},
|
||||
calculations: %{},
|
||||
email: "ash.man@enguento.com",
|
||||
id: "2642ca11-330b-4a07-83c7-b0e9ef391df6"
|
||||
}}
|
||||
```
|
||||
|
||||
##### List and Read a resource
|
||||
|
||||
```elixir
|
||||
iex(3)> MyApp.Api.read(MyApp.User)
|
||||
{:ok,
|
||||
[
|
||||
%MyApp.User{
|
||||
__meta__: #Ecto.Schema.Metadata<:built, "">,
|
||||
__metadata__: %{},
|
||||
aggregates: %{},
|
||||
calculations: %{},
|
||||
email: "ash.man@enguento.com",
|
||||
id: "2642ca11-330b-4a07-83c7-b0e9ef391df6"
|
||||
}
|
||||
]}
|
||||
iex(4)> MyApp.Api.get(MyApp.User, "2642ca11-330b-4a07-83c7-b0e9ef391df6")
|
||||
{:ok,
|
||||
%MyApp.User{
|
||||
__meta__: #Ecto.Schema.Metadata<:built, "">,
|
||||
__metadata__: %{},
|
||||
aggregates: %{},
|
||||
calculations: %{},
|
||||
email: "ash.man@enguento.com",
|
||||
id: "2642ca11-330b-4a07-83c7-b0e9ef391df6"
|
||||
}}
|
||||
```
|
||||
|
||||
## Add relationships
|
||||
|
||||
With our resources stored in a data layer we can move on
|
||||
to create relationships between them. In this case we will
|
||||
specify that a `User` can have many `Tweets` - this implies that
|
||||
a `Tweet` belongs to a specific `User`.
|
||||
|
||||
```elixir
|
||||
# in lib/my_app/resources/user.ex
|
||||
relationships do
|
||||
has_many :tweets, MyApp.Tweet, destination_attribute: :user_id
|
||||
end
|
||||
|
||||
# in lib/my_app/resources/tweet.ex
|
||||
relationships do
|
||||
belongs_to :user, MyApp.User
|
||||
end
|
||||
```
|
||||
|
||||
### Test relationships
|
||||
|
||||
Now we can use the new relationship to create a `Tweet` that belongs to a specific `User`:
|
||||
|
||||
```elixir
|
||||
iex(8)> {:ok, user} = Ash.Changeset.new(MyApp.User, %{email: "ash.man@enguento.com"}) |> MyApp.Api.create()
|
||||
{:ok,
|
||||
%MyApp.User{
|
||||
__meta__: #Ecto.Schema.Metadata<:built, "">,
|
||||
__metadata__: %{},
|
||||
aggregates: %{},
|
||||
calculations: %{},
|
||||
email: "ash.man@enguento.com",
|
||||
id: "0d7063f8-b07c-4d02-88b2-b671f1aa0ad9",
|
||||
tweets: #Ash.NotLoaded<:relationship>
|
||||
}}
|
||||
iex(9)> MyApp.Tweet |> Ash.Changeset.new(%{body: "ashy slashy"}) |> Ash.Changeset.replace_relationship(:user, user) |> MyApp.Api.create()
|
||||
{:ok,
|
||||
%MyApp.Tweet{
|
||||
__meta__: #Ecto.Schema.Metadata<:built, "">,
|
||||
__metadata__: %{},
|
||||
aggregates: %{},
|
||||
body: "ashy slashy",
|
||||
calculations: %{},
|
||||
inserted_at: ~U[2020-11-14 12:54:06Z],
|
||||
id: "f0b0b9d5-832c-45c9-9313-5e3fb9f1af24",
|
||||
public: false,
|
||||
updated_at: ~U[2020-11-14 12:54:06Z],
|
||||
user: %MyApp.User{
|
||||
__meta__: #Ecto.Schema.Metadata<:built, "">,
|
||||
__metadata__: %{},
|
||||
aggregates: %{},
|
||||
calculations: %{},
|
||||
email: "ash.man@enguento.com",
|
||||
id: "0d7063f8-b07c-4d02-88b2-b671f1aa0ad9",
|
||||
tweets: #Ash.NotLoaded<:relationship>
|
||||
},
|
||||
user_id: "0d7063f8-b07c-4d02-88b2-b671f1aa0ad9"
|
||||
}}
|
||||
```
|
||||
|
||||
## Add a Phoenix Frontend
|
||||
|
||||
Now that the Elixir API is complete, you can move on to the [next
|
||||
guide](getting_started_phx.html) to learn how to change the data_layer to
|
||||
PostgreSQL and expose it via a JSON API.
|
||||
|
||||
- `AshJsonApi` - can be used to build a spec compliant JSON:API.
|
||||
- `AshPostgres.DataLayer` - can be used to persist your resources to PostgreSQL.
|
||||
|
||||
## See Ash documentation for the rest
|
||||
|
||||
- `Ash.Api` for what you can do with your resources.
|
||||
- `Ash.Query` for the kinds of queries you can make.
|
||||
- `Ash.Resource.Dsl` for the resource DSL documentation.
|
||||
- `Ash.Api.Dsl` for the API DSL documentation.
|
|
@ -1,348 +0,0 @@
|
|||
# Getting started with Ash and Phoenix
|
||||
|
||||
In this guide we will convert the sample app from the [getting
|
||||
stated guide](getting_started.html) into
|
||||
a full blown service backed by PostgreSQL as a storage and a Json Web API.
|
||||
|
||||
For the web part of the application we will rely on the
|
||||
[Phoenix framework](https://www.phoenixframework.org/) as both frameworks are complementary.
|
||||
Keep in mind that using Phoenix is not a requirement, you could
|
||||
alternatively use [Plug](https://github.com/elixir-plug/plug).
|
||||
|
||||
You can check out the completed application and source code in this [repo](https://github.com/mario-mazo/my_app_phx).
|
||||
|
||||
## Create Phoenix app
|
||||
|
||||
We create a simple Phoenix application and we remove some unnecessary parts,
|
||||
also we are using `--app` to rename the application so it matches the name from
|
||||
the getting started guide.
|
||||
|
||||
```shell
|
||||
mix phx.new my_app --no-html --no-webpack --no-gettext
|
||||
```
|
||||
|
||||
## Add dependencies and formatter
|
||||
|
||||
Now we need to add the dependencies, `ash` and [ash_postgres](https://hexdocs.pm/ash_postgres/readme.html). To find out what the latest available version is you can use `mix hex.info`:
|
||||
|
||||
```shell
|
||||
mix hex.info ash_postgres
|
||||
mix hex.info ash
|
||||
```
|
||||
|
||||
Next modify the the `.formatter` and `mix.exs` files:
|
||||
|
||||
```diff
|
||||
--- a/.formatter.exs
|
||||
+++ b/.formatter.exs
|
||||
@@ -1,4 +1,13 @@
|
||||
[
|
||||
+ import_deps: [
|
||||
+ :ash_json_api,
|
||||
+ :ash_postgres
|
||||
+ ],
|
||||
|
||||
--- b/mix.exs
|
||||
+++ b/mix.exs
|
||||
@@ -33,6 +33,8 @@ defmodule MyAppPhx.MixProject do
|
||||
# Type `mix help deps` for examples and options.
|
||||
defp deps do
|
||||
[
|
||||
+ {:ash_postgres, "~> 0.25.5"},
|
||||
+ {:ash, "~> 1.24"}
|
||||
|
||||
```
|
||||
|
||||
Next, modify `MyApp.Repo` to use `AshPostgres.Repo` instead of `Ecto.Repo`, and add the `uuid-ossp` extension (unless you won't be using uuids).
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.Repo do
|
||||
use AshPostgres.Repo,
|
||||
otp_app: :my_app
|
||||
|
||||
def installed_extensions do
|
||||
["uuid-ossp"]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Make sure you can connect to Postgres by verifying that the credentials in `config/dev.exs` are correct and create the database by running:
|
||||
|
||||
```shell
|
||||
mix ecto.create
|
||||
* The database for MyApp.Repo has been created
|
||||
```
|
||||
|
||||
To configure Phoenix to support the [jsonapi](https://jsonapi.org/) content type, add the following configuration to `config/config.exs`:
|
||||
|
||||
```diff
|
||||
--- a/config/config.exs
|
||||
+++ b/config/config.exs
|
||||
@@ -10,6 +10,10 @@ use Mix.Config
|
||||
config :my_app,
|
||||
ecto_repos: [MyApp.Repo]
|
||||
|
||||
+config :mime, :types, %{
|
||||
+ "application/vnd.api+json" => ["json"]
|
||||
+}
|
||||
+
|
||||
```
|
||||
|
||||
### Reuse the files from the Getting Started guide
|
||||
|
||||
Copy the `lib/my_app/api.ex`, `lib/my_app/resources/tweet.ex`
|
||||
and `lib/my_app/resources/user.ex` from the Getting Started
|
||||
sample app into this project in the same path.
|
||||
|
||||
## Switch data layer to Postgres
|
||||
|
||||
We can now proceed to switch the data layer from `ETS`
|
||||
to `PostgreSQL` simply by changing the `data_layer` to
|
||||
`AshPostgres.DataLayer` in our resources
|
||||
and adding the table name and our repo. In this case we will
|
||||
use the default repo created by Phoenix.
|
||||
|
||||
```diff
|
||||
--- a/my_app_phx/lib/my_app/resources/tweet.ex
|
||||
+++ b/my_app_phx/lib/my_app/resources/tweet.ex
|
||||
@@ -1,6 +1,11 @@
|
||||
# in my_app_phx/lib/my_app/resources/tweet.ex
|
||||
defmodule MyApp.Tweet do
|
||||
- use Ash.Resource, data_layer: Ash.DataLayer.Ets
|
||||
+ use Ash.Resource, data_layer: AshPostgres.DataLayer
|
||||
+
|
||||
+ postgres do
|
||||
+ table "tweets"
|
||||
+ repo MyApp.Repo
|
||||
+ end
|
||||
|
||||
--- a/my_app_phx/lib/my_app/resources/user.ex
|
||||
+++ b/my_app_phx/lib/my_app/resources/user.ex
|
||||
@@ -1,6 +1,11 @@
|
||||
# in my_app_phx/lib/my_app/resources/user.ex
|
||||
defmodule MyApp.User do
|
||||
- use Ash.Resource, data_layer: Ash.DataLayer.Ets
|
||||
+ use Ash.Resource, data_layer: AshPostgres.DataLayer
|
||||
+
|
||||
+ postgres do
|
||||
+ table "users"
|
||||
+ repo MyApp.Repo
|
||||
+ end
|
||||
```
|
||||
|
||||
Now you can tell Ash to generate the migrations from your API:
|
||||
|
||||
```shell
|
||||
mix ash_postgres.generate_migrations --apis MyApp.Api
|
||||
* creating priv/repo/migrations/20201120214857_migrate_resources1.exs
|
||||
```
|
||||
|
||||
and run the ecto migration to generate the tables:
|
||||
|
||||
```shell
|
||||
run mix ecto.migrate
|
||||
|
||||
23:23:46.067 [info] == Running 20201120222312 MyApp.Repo.Migrations.MigrateResources1.up/0 forward
|
||||
|
||||
23:23:46.070 [info] create table users
|
||||
|
||||
23:23:46.076 [info] create table tweets
|
||||
|
||||
23:23:46.090 [info] == Migrated 20201120222312 in 0.0s
|
||||
|
||||
```
|
||||
|
||||
### Test PostgreSQL integration
|
||||
|
||||
Start IEx with `iex -S mix phx.server` and lets run the same test
|
||||
we ran in the initial `my_app`. You will now see that SQL statements
|
||||
are being executed and data is now stored in your PostgreSQL database.
|
||||
|
||||
```elixir
|
||||
iex(1)> {:ok, user} = Ash.Changeset.new(MyApp.User, %{email: "ash.man@enguento.com"}) |> MyApp.Api.create()
|
||||
[debug] QUERY OK db=1.2ms idle=1432.0ms
|
||||
begin []
|
||||
[debug] QUERY OK db=0.4ms
|
||||
INSERT INTO "users" ("email","id") VALUES ($1,$2) ["ash.man@enguento.com", <<72, 22
|
||||
6, 94, 187, 145, 81, 66, 25, 183, 79, 59, 199, 93, 88, 32, 243>>]
|
||||
[debug] QUERY OK db=0.3ms
|
||||
commit []
|
||||
{:ok,
|
||||
%MyApp.User{
|
||||
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
|
||||
__metadata__: %{},
|
||||
aggregates: %{},
|
||||
calculations: %{},
|
||||
email: "ash.man@enguento.com",
|
||||
id: "48e25ebb-9151-4219-b74f-3bc75d5820f3",
|
||||
tweets: #Ash.NotLoaded<:relationship>
|
||||
}}
|
||||
|
||||
iex(2)> MyApp.Tweet |> Ash.Changeset.new(%{body: "ashy slashy"}) |> Ash.Changeset.r
|
||||
eplace_relationship(:user, user) |> MyApp.Api.create()
|
||||
[debug] QUERY OK db=0.1ms idle=1197.5ms
|
||||
begin []
|
||||
[debug] QUERY OK db=2.2ms
|
||||
INSERT INTO "tweets" ("body","inserted_at","id","public","updated_at","user_id") VAL
|
||||
UES ($1,$2,$3,$4,$5,$6) ["ashy slashy", ~U[2020-11-22 21:15:33Z], <<163, 22, 225, 4
|
||||
3, 217, 10, 67, 242, 152, 149, 197, 133, 253, 154, 244, 95>>, false, ~U[2020-11-22
|
||||
21:15:33Z], <<72, 226, 94, 187, 145, 81, 66, 25, 183, 79, 59, 199, 93, 88, 32, 243>
|
||||
>]
|
||||
[debug] QUERY OK db=0.3ms
|
||||
commit []
|
||||
{:ok,
|
||||
%MyApp.Tweet{
|
||||
__meta__: #Ecto.Schema.Metadata<:loaded, "tweets">,
|
||||
__metadata__: %{},
|
||||
aggregates: %{},
|
||||
body: "ashy slashy",
|
||||
calculations: %{},
|
||||
inserted_at: ~U[2020-11-22 21:15:33Z],
|
||||
id: "a316e12b-d90a-43f2-9895-c585fd9af45f",
|
||||
public: false,
|
||||
updated_at: ~U[2020-11-22 21:15:33Z],
|
||||
user: %MyApp.User{
|
||||
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
|
||||
__metadata__: %{},
|
||||
aggregates: %{},
|
||||
calculations: %{},
|
||||
email: "ash.man@enguento.com",
|
||||
id: "48e25ebb-9151-4219-b74f-3bc75d5820f3",
|
||||
tweets: #Ash.NotLoaded<:relationship>
|
||||
},
|
||||
user_id: "48e25ebb-9151-4219-b74f-3bc75d5820f3"
|
||||
}}
|
||||
```
|
||||
|
||||
### Exposing the API with a JSON API
|
||||
|
||||
First we need to add the extension dependency for [ash_json_api](https://hexdocs.pm/ash_json_api/readme.html).
|
||||
|
||||
|
||||
```shell
|
||||
mix hex.info ash_json_api
|
||||
```
|
||||
|
||||
Add it to your dependencies and don't forget to run `mix deps.get`:
|
||||
|
||||
```diff
|
||||
--- a/mix.exs
|
||||
+++ b/mix.exs
|
||||
@@ -33,6 +33,7 @@ defmodule MyApp.MixProject do
|
||||
# Type `mix help deps` for examples and options.
|
||||
defp deps do
|
||||
[
|
||||
+ {:ash_json_api, "~> 0.24.1"},
|
||||
{:ash_postgres, "~> 0.25.5"},
|
||||
{:ash, "~> 1.24"},
|
||||
{:phoenix, "~> 1.5.6"},
|
||||
```
|
||||
|
||||
Create a router module for your Api
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.MyApi.Router do
|
||||
# The registry must be explicitly provided here
|
||||
use AshJsonApi.Api.Router, api: Api, registry: Registry
|
||||
end
|
||||
```
|
||||
|
||||
We can proceed to add a route in the Phoenix router to forward requests
|
||||
to our Ash API. To do so we use `AshJsonApi.forward/3` as shown in
|
||||
`lib/my_app_web/router.ex`:
|
||||
|
||||
```diff
|
||||
--- a/lib/my_app_web/router.ex
|
||||
+++ b/lib/my_app_web/router.ex
|
||||
@@ -1,12 +1,14 @@
|
||||
defmodule MyAppWeb.Router do
|
||||
|
||||
- scope "/api", MyAppWeb do
|
||||
+ scope "/api" do
|
||||
pipe_through :api
|
||||
+ forward("/", MyApp.MyApi.Router)
|
||||
end
|
||||
```
|
||||
|
||||
After that, all we have to do is configure our resources for the JSON:API.
|
||||
In this guide we will only expose an API for the `user` resource, exposing the `tweet` resource is left as an exercise for the reader.
|
||||
|
||||
We need to add the extension to our resource and define a mapping between
|
||||
the REST verbs and our internal API actions.
|
||||
|
||||
```diff
|
||||
--- a/lib/my_app/resources/user.ex
|
||||
+++ b/lib/my_app/resources/user.ex
|
||||
@@ -1,7 +1,24 @@
|
||||
# in lib/my_app/resources/user.ex
|
||||
defmodule MyApp.User do
|
||||
- use Ash.Resource, data_layer: AshPostgres.DataLayer
|
||||
+ use Ash.Resource, data_layer: AshPostgres.DataLayer,
|
||||
+ extensions: [
|
||||
+ AshJsonApi.Resource
|
||||
+ ]
|
||||
|
||||
+ json_api do
|
||||
+ type "user"
|
||||
+
|
||||
+ routes do
|
||||
+ base "/users"
|
||||
+
|
||||
+ get :read
|
||||
+ index :read
|
||||
+ post :create
|
||||
+ patch :update
|
||||
+ delete :destroy
|
||||
+ end
|
||||
+ end
|
||||
+
|
||||
```
|
||||
|
||||
### Test Web Json API
|
||||
|
||||
Fire up IEx with `iex -S mix phx.server` and curl the API:
|
||||
|
||||
```shell
|
||||
curl -s --request GET --url 'http://localhost:4000/api/users' | jq
|
||||
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"attributes": {
|
||||
"email": "ash.man@enguento.com"
|
||||
},
|
||||
"id": "46b60ec8-5b0f-461d-95ab-bcc5169ff831",
|
||||
"links": {},
|
||||
"meta": {},
|
||||
"relationships": {},
|
||||
"type": "user"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"email": "ash.man@enguento.com"
|
||||
},
|
||||
"id": "cd84148a-4af4-4f9f-952f-9daa28946e01",
|
||||
"links": {},
|
||||
"meta": {},
|
||||
"relationships": {},
|
||||
"type": "user"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"email": "ash.man@enguento.com"
|
||||
},
|
||||
"id": "48e25ebb-9151-4219-b74f-3bc75d5820f3",
|
||||
"links": {},
|
||||
"meta": {},
|
||||
"relationships": {},
|
||||
"type": "user"
|
||||
}
|
||||
],
|
||||
"jsonapi": {
|
||||
"version": "1.0"
|
||||
},
|
||||
"links": {
|
||||
"self": "http://localhost:4000/api/users"
|
||||
}
|
||||
}
|
||||
```
|
Loading…
Reference in a new issue