WIP, action tests

This commit is contained in:
Zach Daniel 2019-12-23 13:17:22 -05:00
parent 2c3c368a7e
commit 50941958e6
No known key found for this signature in database
GPG key ID: A57053A671EE649E
8 changed files with 435 additions and 319 deletions

View file

@ -120,3 +120,4 @@ end
is supposed to be optional
* relationships updates are *extremely* unoptimized
* Clean up and test filter inspecting code.
* Handle related values on delete

View file

@ -1,43 +1,47 @@
defmodule Ash.Actions.ChangesetHelpers do
alias Ash.Actions.PrimaryKeyHelpers
@type before_change_callback :: (Ecto.Changeset.t() -> Ecto.Changeset.t())
@type after_change_callback ::
(Ecto.Changeset.t(), Ash.record() -> {:ok, Ash.record()} | {:error, Ash.error()})
@spec before_change(Ecto.Changeset.t(), before_change_callback) :: Ecto.Changeset.t()
def before_change(changeset, func) do
Map.update(changeset, :__before_ash_changes__, [func], fn funcs ->
[func | funcs]
end)
end
@spec after_change(Ecto.Changeset.t(), after_change_callback) :: Ecto.Changeset.t()
def after_change(changeset, func) do
Map.update(changeset, :__after_ash_changes__, [func], fn funcs ->
[func | funcs]
end)
end
@spec run_before_changes(Ecto.Changeset.t()) :: Ecto.Changeset.t()
def run_before_changes(%{__before_ash_changes__: hooks} = changeset) do
Enum.reduce(hooks, changeset, fn
hook, %Ecto.Changeset{valid?: true} = changeset ->
case hook.(changeset) do
:ok -> changeset
{:ok, changeset} -> changeset
%Ecto.Changeset{} = changeset -> changeset
end
_, %Ecto.Changeset{} = changeset ->
changeset
_, {:error, error} ->
{:error, error}
end)
end
def run_before_changes(changeset), do: changeset
@spec run_after_changes(Ecto.Changeset.t(), Ash.record()) ::
{:ok, Ash.record()} | {:error, Ash.error()}
def run_after_changes(%{__after_ash_changes__: hooks} = changeset, result) do
Enum.reduce(hooks, {:ok, result}, fn
hook, {:ok, result} ->
case hook.(changeset, result) do
{:ok, result} -> {:ok, result}
:ok -> {:ok, result}
{:error, error} -> {:error, error}
end
@ -50,110 +54,208 @@ defmodule Ash.Actions.ChangesetHelpers do
{:ok, result}
end
def belongs_to_assoc_update(
%{__ash_api__: api} = changeset,
%{
destination: destination,
destination_field: destination_field,
source_field: source_field
} = relationship,
identifier,
@spec prepare_relationship_changes(
Ecto.Changeset.t(),
Ash.resource(),
map(),
boolean,
Ash.user()
) :: Ecto.Changeset.t()
def prepare_relationship_changes(
changeset,
resource,
relationships,
authorize?,
user
) do
case PrimaryKeyHelpers.value_to_primary_key_filter(destination, identifier) do
{:error, _error} ->
Ecto.Changeset.add_error(changeset, relationship.name, "Invalid primary key supplied")
Enum.reduce(relationships, changeset, fn {relationship, value}, changeset ->
with {:rel, rel} when not is_nil(rel) <- {:rel, Ash.relationship(resource, relationship)},
{:ok, filter} <- primary_key_filter(rel, value) do
case rel.type do
:belongs_to ->
belongs_to_assoc_update(changeset, rel, filter, authorize?, user)
{:ok, filter} ->
before_change(changeset, fn changeset ->
case api.get(destination, filter, authorize?: authorize?, user: user) do
{:ok, record} when not is_nil(record) ->
changeset
|> Ecto.Changeset.put_change(source_field, Map.get(record, destination_field))
|> after_change(fn _changeset, result ->
{:ok, Map.put(result, relationship.name, record)}
end)
:has_one ->
has_one_assoc_update(changeset, rel, filter, authorize?, user)
{:ok, nil} ->
{:error, "not found"}
:has_many ->
has_many_assoc_update(changeset, rel, filter, authorize?, user)
{:error, error} ->
{:error, error}
end
end)
end
:many_to_many ->
many_to_many_assoc_update(changeset, rel, filter, value, authorize?, user)
end
else
{:rel, nil} ->
Ecto.Changeset.add_error(changeset, relationship, "No such relationship")
{:error, error} ->
Ecto.Changeset.add_error(changeset, relationship, error)
end
end)
end
def has_one_assoc_update(
%{__ash_api__: api} = changeset,
%{
destination: destination,
destination_field: destination_field,
source_field: source_field
} = relationship,
identifier,
authorize?,
user
) do
case PrimaryKeyHelpers.value_to_primary_key_filter(destination, identifier) do
{:error, _error} ->
Ecto.Changeset.add_error(changeset, relationship.name, "Invalid primary key supplied")
{:ok, filter} ->
after_change(changeset, fn _changeset, result ->
value = Map.get(result, source_field)
with {:ok, record} <-
api.get(destination, filter, authorize?: authorize?, user: user),
{:ok, updated_record} <-
api.update(record, attributes: %{destination_field => value}) do
{:ok, Map.put(result, relationship.name, updated_record)}
end
end)
end
defp primary_key_filter(%{cardinality: :many, destination: destination}, value) do
PrimaryKeyHelpers.values_to_primary_key_filters(destination, value)
end
def many_to_many_assoc_on_create(changeset, %{name: rel_name}, identifier, _, _)
when not is_list(identifier) do
defp primary_key_filter(%{destination: destination}, value) do
PrimaryKeyHelpers.value_to_primary_key_filter(destination, value)
end
defp belongs_to_assoc_update(
%{__ash_api__: api} = changeset,
%{
destination: destination,
destination_field: destination_field,
source_field: source_field
} = relationship,
filter,
authorize?,
user
) do
before_change(changeset, fn changeset ->
case api.get(destination, filter, authorize?: authorize?, user: user) do
{:ok, record} when not is_nil(record) ->
changeset
|> Ecto.Changeset.put_change(source_field, Map.get(record, destination_field))
|> after_change(fn _changeset, result ->
{:ok, Map.put(result, relationship.name, record)}
end)
{:ok, nil} ->
{:error, "not found"}
{:error, error} ->
{:error, error}
end
end)
end
defp has_one_assoc_update(
%{__ash_api__: api} = changeset,
%{
destination: destination,
destination_field: destination_field,
source_field: source_field
} = relationship,
filter,
authorize?,
user
) do
changeset
|> before_change(fn changeset ->
value = Map.get(changeset.data, source_field)
if changeset.action == :update && value do
case api.get(destination, [{destination_field, value}]) do
{:ok, nil} ->
changeset
{:ok, record} ->
case api.update(record, attributes: %{destination_field => nil}) do
{:ok, _} ->
changeset
{:error, error} ->
Ecto.Changeset.add_error(changeset, relationship.name, error)
end
{:error, error} ->
Ecto.Changeset.add_error(
changeset,
relationship.name,
error
)
end
else
changeset
end
end)
|> after_change(fn _changeset, result ->
value = Map.get(result, source_field)
with {:ok, record} <-
api.get(destination, filter, authorize?: authorize?, user: user),
{:ok, updated_record} <-
api.update(record, attributes: %{destination_field => value}) do
{:ok, Map.put(result, relationship.name, updated_record)}
end
end)
end
defp many_to_many_assoc_update(changeset, %{name: rel_name}, filter, _, _, _)
when not is_list(filter) do
Ecto.Changeset.add_error(changeset, rel_name, "Invalid value")
end
def many_to_many_assoc_on_create(changeset, rel, identifiers, authorize?, user) do
case PrimaryKeyHelpers.values_to_primary_key_filters(rel.destination, identifiers) do
{:error, _error} ->
Ecto.Changeset.add_error(changeset, rel.name, "Invalid primary key supplied")
defp many_to_many_assoc_update(changeset, rel, filters, identifiers, authorize?, user) do
changeset
|> before_change(fn %{__ash_api__: api} = changeset ->
source_field_value = Ecto.Changeset.get_field(changeset, rel.source_field)
{:ok, filters} ->
changeset
|> before_change(fn %{__ash_api__: api} = changeset ->
source_field_value = Ecto.Changeset.get_field(changeset, rel.source_field)
destroy_result =
destroy_no_longer_related_join_table_rows(
api,
source_field_value,
rel,
filters,
authorize?,
user
)
destroy_result =
destroy_no_longer_related_join_table_rows(
api,
source_field_value,
rel,
filters,
authorize?,
user
)
case destroy_result do
:ok -> changeset
{:error, error} -> {:error, error}
end
end)
|> after_change(fn %{__ash_api__: api}, result ->
case fetch_and_ensure_related(identifiers, api, result, rel, authorize?, user) do
{:error, error} ->
{:error, error}
case destroy_result do
:ok -> changeset
{:error, error} -> {:error, error}
end
end)
|> after_change(fn %{__ash_api__: api}, result ->
case fetch_and_ensure_related(identifiers, api, result, rel, authorize?, user) do
{:error, error} ->
{:error, error}
{:ok, related} ->
{:ok, Map.put(result, rel.name, related)}
end
end)
end
{:ok, related} ->
{:ok, Map.put(result, rel.name, related)}
end
end)
end
defp has_many_assoc_update(
%{__ash_api__: api} = changeset,
%{
destination: destination,
destination_field: destination_field,
source_field: source_field
} = relationship,
filters,
authorize?,
user
) do
after_change(changeset, fn _changeset, %resource{} = result ->
value = Map.get(result, source_field)
currently_related_filter =
result
|> Map.take(Ash.primary_key(resource))
|> Map.to_list()
params = [
filter: currently_related_filter,
paginate?: false,
authorize?: authorize?,
user: user
]
with {:ok, %{results: related}} <-
api.read(destination, params),
{:ok, to_relate} <-
get_to_relate(api, filters, destination, authorize?, user),
to_clear <- get_no_longer_present(resource, related, to_relate),
:ok <- clear_related(api, resource, to_clear, destination_field, authorize?, user),
{:ok, now_related} <-
relate_items(api, to_relate, destination_field, value, authorize?, user) do
{:ok, Map.put(result, relationship.name, now_related)}
end
end)
end
defp fetch_and_ensure_related(identifiers, api, %resource{} = result, rel, authorize?, user) do
@ -276,61 +378,6 @@ defmodule Ash.Actions.ChangesetHelpers do
end
end
def many_to_many_assoc_update(changeset, %{name: rel_name}, identifier, _, _)
when not is_list(identifier) do
Ecto.Changeset.add_error(changeset, rel_name, "Invalid value")
end
def has_many_assoc_update(changeset, %{name: rel_name}, identifier, _, _)
when not is_list(identifier) do
Ecto.Changeset.add_error(changeset, rel_name, "Invalid value")
end
def has_many_assoc_update(
%{__ash_api__: api} = changeset,
%{
destination: destination,
destination_field: destination_field,
source_field: source_field
} = relationship,
identifiers,
authorize?,
user
) do
case PrimaryKeyHelpers.values_to_primary_key_filters(destination, identifiers) do
{:error, _error} ->
Ecto.Changeset.add_error(changeset, relationship.name, "Invalid primary key supplied")
{:ok, filters} ->
after_change(changeset, fn _changeset, %resource{} = result ->
value = Map.get(result, source_field)
currently_related_filter =
result
|> Map.take(Ash.primary_key(resource))
|> Map.to_list()
params = [
filter: currently_related_filter,
paginate?: false,
authorize?: authorize?,
user: user
]
with {:ok, %{results: related}} <-
api.read(destination, params),
{:ok, to_relate} <-
get_to_relate(api, filters, destination, authorize?, user),
to_clear <- get_no_longer_present(resource, related, to_relate),
:ok <- clear_related(api, resource, to_clear, destination_field, authorize?, user),
{:ok, now_related} <-
relate_items(api, to_relate, destination_field, value, authorize?, user) do
{:ok, Map.put(result, relationship.name, now_related)}
end
end)
end
end
defp relate_items(api, to_relate, _destination_field, destination_field_value, authorize?, user) do
Enum.reduce(to_relate, {:ok, []}, fn
to_be_related, {:ok, now_related} ->

View file

@ -63,11 +63,20 @@ defmodule Ash.Actions.Create do
authorize? = Keyword.get(params, :authorize?, false)
user = Keyword.get(params, :user)
with %{valid?: true} = changeset <- prepare_create_attributes(resource, attributes),
changeset <- Map.put(changeset, :__ash_api__, api) do
prepare_create_relationships(changeset, resource, relationships, authorize?, user)
else
%{valid?: false} = changeset -> changeset
case prepare_create_attributes(resource, attributes) do
%{valid?: true} = changeset ->
changeset = Map.put(changeset, :__ash_api__, api)
ChangesetHelpers.prepare_relationship_changes(
changeset,
resource,
relationships,
authorize?,
user
)
changeset ->
changeset
end
end
@ -92,30 +101,10 @@ defmodule Ash.Actions.Create do
resource
|> struct()
|> Ecto.Changeset.cast(attributes_with_defaults, allowed_keys)
|> Map.put(:action, :create)
end
defp default(%{default: {:constant, value}}), do: value
defp default(%{default: {mod, func}}), do: apply(mod, func, [])
defp default(%{default: function}), do: function.()
defp prepare_create_relationships(changeset, resource, relationships, authorize?, user) do
Enum.reduce(relationships, changeset, fn {relationship, value}, changeset ->
case Ash.relationship(resource, relationship) do
%{type: :belongs_to} = rel ->
ChangesetHelpers.belongs_to_assoc_update(changeset, rel, value, authorize?, user)
%{type: :has_one} = rel ->
ChangesetHelpers.has_one_assoc_update(changeset, rel, value, authorize?, user)
%{type: :has_many} = rel ->
ChangesetHelpers.has_many_assoc_update(changeset, rel, value, authorize?, user)
%{type: :many_to_many} = rel ->
ChangesetHelpers.many_to_many_assoc_on_create(changeset, rel, value, authorize?, user)
_ ->
Ecto.Changeset.add_error(changeset, relationship, "No such relationship")
end
end)
end
end

View file

@ -74,7 +74,13 @@ defmodule Ash.Actions.Update do
with %{valid?: true} = changeset <- prepare_update_attributes(record, attributes),
changeset <- Map.put(changeset, :__ash_api__, api) do
prepare_update_relationships(changeset, resource, relationships, authorize?, user)
ChangesetHelpers.prepare_relationship_changes(
changeset,
resource,
relationships,
authorize?,
user
)
end
end
@ -84,27 +90,8 @@ defmodule Ash.Actions.Update do
|> Ash.attributes()
|> Enum.map(& &1.name)
Ecto.Changeset.cast(record, attributes, allowed_keys)
end
defp prepare_update_relationships(changeset, resource, relationships, authorize?, user) do
Enum.reduce(relationships, changeset, fn {relationship, value}, changeset ->
case Ash.relationship(resource, relationship) do
%{type: :belongs_to} = rel ->
ChangesetHelpers.belongs_to_assoc_update(changeset, rel, value, authorize?, user)
%{type: :has_one} = rel ->
ChangesetHelpers.has_one_assoc_update(changeset, rel, value, authorize?, user)
%{type: :has_many} = rel ->
ChangesetHelpers.has_many_assoc_update(changeset, rel, value, authorize?, user)
# %{type: :many_to_many} = rel ->
# ChangesetHelpers.many_to_many_assoc_update(changeset, rel, value, authorize?, user)
_ ->
Ecto.Changeset.add_error(changeset, relationship, "No such relationship")
end
end)
record
|> Ecto.Changeset.cast(attributes, allowed_keys)
|> Map.put(:action, :update)
end
end

View file

@ -63,7 +63,7 @@ defmodule Ash.Api.Interface do
@spec destroy(Ash.record(), Ash.update_params()) ::
{:ok, Ash.record()} | {:error, Ash.error()}
def destory(record, params \\ []) do
def destroy(record, params \\ []) do
case Ash.Api.Interface.destroy(__MODULE__, record, params) do
{:ok, instance} -> {:ok, instance}
{:error, error} -> {:error, List.wrap(error)}
@ -231,7 +231,8 @@ defmodule Ash.Api.Interface do
raise Ash.Error.FrameworkError.exception(message: error)
end
defp unwrap_or_raise!({:error, %Ecto.Changeset{}}) do
defp unwrap_or_raise!({:error, %Ecto.Changeset{} = cs}) do
IO.inspect(cs)
raise(Ash.Error.FrameworkError, message: "invalid changes")
end
@ -248,8 +249,9 @@ defmodule Ash.Api.Interface do
string when is_bitstring(string) ->
Ash.Error.FrameworkError.exception(message: string)
_ = %Ecto.Changeset{} ->
_ = %Ecto.Changeset{} = cs ->
# TODO: format these
IO.inspect(cs)
"invalid changes"
error ->

View file

@ -1,12 +1,17 @@
defmodule Ash.Authorization.Checks do
@moduledoc "Built in authorization checks."
def always(), do: [__always__: true]
def always() do
[always: true]
end
def related_to_user_via(relationship) do
relationship
|> List.wrap()
|> put_nested_relationship()
filter =
relationship
|> List.wrap()
|> put_nested_relationship()
[filter: filter]
end
defp put_nested_relationship([rel | rest]) do

View file

@ -87,120 +87,4 @@ defmodule Ash.Test.Actions.DestroyTest do
refute Api.get!(Post, post.id)
end
end
# describe "simple creates" do
# test "allows creating a record with valid attributes" do
# assert %Post{title: "foo", contents: "bar"} =
# Api.create!(Post, attributes: %{title: "foo", contents: "bar"})
# end
# test "constant default values are set properly" do
# assert %Post{tag: "garbage"} = Api.create!(Post, attributes: %{title: "foo"})
# end
# test "constant functions values are set properly" do
# assert %Post{tag2: "garbage2"} = Api.create!(Post, attributes: %{title: "foo"})
# end
# test "constant module/function values are set properly" do
# assert %Post{tag3: "garbage3"} = Api.create!(Post, attributes: %{title: "foo"})
# end
# end
# describe "creating with has_one relationships" do
# test "allows creating with has_one relationship" do
# profile = Api.create!(Profile, attributes: %{bio: "best dude"})
# Api.create!(Author,
# attributes: %{name: "fred"},
# relationships: %{profile: profile.id}
# )
# end
# test "it sets the relationship on the destination record accordingly" do
# profile = Api.create!(Profile, attributes: %{bio: "best dude"})
# author =
# Api.create!(Author,
# attributes: %{name: "fred"},
# relationships: %{profile: profile.id}
# )
# assert Api.get!(Profile, profile.id).author_id == author.id
# end
# test "it responds with the relationshi filled in" do
# profile = Api.create!(Profile, attributes: %{bio: "best dude"})
# author =
# Api.create!(Author,
# attributes: %{name: "fred"},
# relationships: %{profile: profile.id}
# )
# assert author.profile == Map.put(profile, :author_id, author.id)
# end
# end
# describe "creating with a has_many relationship" do
# test "allows creating with a has_many relationship" do
# post = Api.create!(Post, attributes: %{title: "sup"})
# Api.create!(Author,
# attributes: %{title: "foobar"},
# relationships: %{
# posts: [post.id]
# }
# )
# end
# end
# describe "creating with belongs_to relationships" do
# test "allows creating with belongs_to relationship" do
# author = Api.create!(Author, attributes: %{bio: "best dude"})
# Api.create!(Post,
# attributes: %{title: "foobar"},
# relationships: %{
# author: author.id
# }
# )
# end
# test "it sets the relationship on the destination record accordingly" do
# author = Api.create!(Author, attributes: %{bio: "best dude"})
# post =
# Api.create!(Post,
# attributes: %{title: "foobar"},
# relationships: %{
# author: author.id
# }
# )
# assert Api.get!(Post, post.id).author_id == author.id
# end
# test "it responds with the relationship field filled in" do
# author = Api.create!(Author, attributes: %{bio: "best dude"})
# assert Api.create!(Post,
# attributes: %{title: "foobar"},
# relationships: %{
# author: author.id
# }
# ).author_id == author.id
# end
# test "it responds with the relationship filled in" do
# author = Api.create!(Author, attributes: %{bio: "best dude"})
# assert Api.create!(Post,
# attributes: %{title: "foobar"},
# relationships: %{
# author: author.id
# }
# ).author == author
# end
# end
end

View file

@ -0,0 +1,201 @@
defmodule Ash.Test.Actions.UpdateTest do
use ExUnit.Case, async: true
defmodule Profile do
use Ash.Resource, name: "authors", type: "author"
use Ash.DataLayer.Ets, private?: true
actions do
read :default
create :default
update :default
end
attributes do
attribute :bio, :string
end
relationships do
belongs_to :author, Ash.Test.Actions.UpdateTest.Author
end
end
defmodule Author do
use Ash.Resource, name: "authors", type: "author"
use Ash.DataLayer.Ets, private?: true
actions do
read :default
create :default
update :default
end
attributes do
attribute :name, :string
end
relationships do
has_one :profile, Profile
has_many :posts, Ash.Test.Actions.UpdateTest.Post
end
end
defmodule PostDefaults do
def garbage2(), do: "garbage2"
def garbage3(), do: "garbage3"
end
defmodule Post do
use Ash.Resource, name: "posts", type: "post"
use Ash.DataLayer.Ets, private?: true
actions do
read :default
create :default
update :default
end
attributes do
attribute :title, :string
attribute :contents, :string
attribute :tag, :string, default: {:constant, "garbage"}
attribute :tag2, :string, default: &PostDefaults.garbage2/0
attribute :tag3, :string, default: {PostDefaults, :garbage3}
end
relationships do
belongs_to :author, Author
end
end
defmodule Api do
use Ash.Api
resources [Author, Post, Profile]
end
describe "simple updates" do
test "allows updating a record with valid attributes" do
post = Api.create!(Post, attributes: %{title: "foo", contents: "bar"})
assert %Post{title: "bar", contents: "foo"} =
Api.update!(post, attributes: %{title: "bar", contents: "foo"})
end
end
describe "updating with has_one relationships" do
test "allows creating with has_one relationship" do
profile = Api.create!(Profile, attributes: %{bio: "best dude"})
profile2 = Api.create!(Profile, attributes: %{bio: "second best dude"})
author =
Api.create!(Author,
attributes: %{name: "fred"},
relationships: %{profile: profile.id}
)
Api.update!(author, relationships: %{profile: profile2.id})
end
test "it sets the relationship on the destination record accordingly" do
profile = Api.create!(Profile, attributes: %{bio: "best dude"})
profile2 = Api.create!(Profile, attributes: %{bio: "second best dude"})
author =
Api.create!(Author,
attributes: %{name: "fred"},
relationships: %{profile: profile.id}
)
Api.update!(author, relationships: %{profile: profile2.id})
assert Api.get!(Profile, profile.id).author_id == nil
assert Api.get!(Profile, profile2.id).author_id == author.id
end
test "it responds with the relationship filled in" do
profile = Api.create!(Profile, attributes: %{bio: "best dude"})
profile2 = Api.create!(Profile, attributes: %{bio: "second best dude"})
author =
Api.create!(Author,
attributes: %{name: "fred"},
relationships: %{profile: profile.id}
)
updated_author = Api.update!(author, relationships: %{profile: profile2.id})
assert updated_author.profile == %{profile2 | author_id: author.id}
end
end
describe "updating with a has_many relationship" do
test "allows updating with a has_many relationship" do
post = Api.create!(Post, attributes: %{title: "sup"})
post2 = Api.create!(Post, attributes: %{title: "sup2"})
author =
Api.create!(Author,
attributes: %{title: "foobar"},
relationships: %{
posts: [post.id]
}
)
Api.update!(author,
relationships: %{
posts: [post.id, post2.id]
}
)
end
end
# describe "creating with belongs_to relationships" do
# test "allows creating with belongs_to relationship" do
# author = Api.create!(Author, attributes: %{bio: "best dude"})
# Api.create!(Post,
# attributes: %{title: "foobar"},
# relationships: %{
# author: author.id
# }
# )
# end
# test "it sets the relationship on the destination record accordingly" do
# author = Api.create!(Author, attributes: %{bio: "best dude"})
# post =
# Api.create!(Post,
# attributes: %{title: "foobar"},
# relationships: %{
# author: author.id
# }
# )
# assert Api.get!(Post, post.id).author_id == author.id
# end
# test "it responds with the relationship field filled in" do
# author = Api.create!(Author, attributes: %{bio: "best dude"})
# assert Api.create!(Post,
# attributes: %{title: "foobar"},
# relationships: %{
# author: author.id
# }
# ).author_id == author.id
# end
# test "it responds with the relationship filled in" do
# author = Api.create!(Author, attributes: %{bio: "best dude"})
# assert Api.create!(Post,
# attributes: %{title: "foobar"},
# relationships: %{
# author: author.id
# }
# ).author == author
# end
# end
end