barnabasj
barnabasj
AEAsh Elixir
Created by barnabasj on 7/13/2023 in #showcase
We created a small extensions for rbac policies
184 replies
AEAsh Elixir
Created by barnabasj on 7/11/2023 in #support
Ambiguous Call during Compile because of Extension
I'm creating an extension and I tried adding an actions option to it, but it won't compile the resources anymore because it cant tell which actions function to use.
== Compilation error in file test/support/policy.ex ==
** (CompileError) test/support/policy.ex:52: function actions/1 imported from both Demo.Extensions.Rbac.Rbac.Role.Options and Ash.Resource.Dsl, call is ambiguous
expanding macro: Demo.Extensions.Rbac.Rbac.Role.role/3
test/support/policy.ex:51: PolicyTestSupport.RootResource (module)
expanding macro: Demo.Extensions.Rbac.rbac/1
test/support/policy.ex:48: PolicyTestSupport.RootResource (module)
== Compilation error in file test/support/policy.ex ==
** (CompileError) test/support/policy.ex:52: function actions/1 imported from both Demo.Extensions.Rbac.Rbac.Role.Options and Ash.Resource.Dsl, call is ambiguous
expanding macro: Demo.Extensions.Rbac.Rbac.Role.role/3
test/support/policy.ex:51: PolicyTestSupport.RootResource (module)
expanding macro: Demo.Extensions.Rbac.rbac/1
test/support/policy.ex:48: PolicyTestSupport.RootResource (module)
Extension definition:
@role %Spark.Dsl.Entity{
name: :role,
describe: "If the check is true, the request is forbidden, otherwise run remaining checks.",
target: Role,
args: [:role, :fields],
links: [],
schema: [
role: [
type: :atom,
required: true,
doc: """
The role this config is for
"""
],
fields: [
type: {:list, :atom},
required: true,
doc: """
The fields the role has access to
"""
],
allowed_actions: [
type: {:list, :atom},
required: false,
doc: """
The actions the role has access to
"""
]
],
examples: [
"role :user, [:id, :name]"
]
}

@rbac %Spark.Dsl.Section{
name: :rbac,
describe: @moduledoc,
examples: [
"""
rbac do
bypass :admin
role :user, [:name] do
actions [:read]
end
end
"""
],
schema: [
bypass: [
type: :atom,
doc: "Role that is allowed to bypass authorization"
],
public?: [
type: :boolean,
doc: "Allow all access",
default: false
]
],
entities: [
@role
]
}
@role %Spark.Dsl.Entity{
name: :role,
describe: "If the check is true, the request is forbidden, otherwise run remaining checks.",
target: Role,
args: [:role, :fields],
links: [],
schema: [
role: [
type: :atom,
required: true,
doc: """
The role this config is for
"""
],
fields: [
type: {:list, :atom},
required: true,
doc: """
The fields the role has access to
"""
],
allowed_actions: [
type: {:list, :atom},
required: false,
doc: """
The actions the role has access to
"""
]
],
examples: [
"role :user, [:id, :name]"
]
}

@rbac %Spark.Dsl.Section{
name: :rbac,
describe: @moduledoc,
examples: [
"""
rbac do
bypass :admin
role :user, [:name] do
actions [:read]
end
end
"""
],
schema: [
bypass: [
type: :atom,
doc: "Role that is allowed to bypass authorization"
],
public?: [
type: :boolean,
doc: "Allow all access",
default: false
]
],
entities: [
@role
]
}
35 replies
AEAsh Elixir
Created by barnabasj on 6/22/2023 in #support
Postgres Queries have wrong where clause
Recently we some read actions do not return the correct data, the Queries that are logged have an obviously wrong where clause
SELECT c0."id", c0."first_name", c0."last_name", c0."title", c0."legal_entity_name", c0."is_legal_entity" FROM "contact" AS c0 WHERE (false) []
SELECT c0."id", c0."first_name", c0."last_name", c0."title", c0."legal_entity_name", c0."is_legal_entity" FROM "contact" AS c0 WHERE (false) []
The clause should be c0."id" = ? with the id in the parameter list This is the resource and we query the current_user graphql query
defmodule Demo.Auth.Resources.User do
@moduledoc """
The Logged in User
"""
use Demo.AuditedResource,
data_layer: AshPostgres.DataLayer,
extensions: [AshGraphql.Resource]

attributes do
uuid_primary_key :id, generated?: false

attribute :email, :string
attribute :email_confirmed_at, :utc_datetime

attribute :contact_id, :uuid

attribute :features, Demo.Auth.Resources.User.Features

# attribute :roles, {:array, :atom}

create_timestamp :inserted_at
update_timestamp :updated_at
end

rbac do
bypass(:admin)
role(:user, [:id, :email, :email_confirmed_at, :contact, :contact_id, :features])
end

audit do
actors?(false)
timestamps?(false)
end

graphql do
type :user

queries do
read_one :user, :current_user
end
end

relationships do
belongs_to :contact, Demo.CustomerService.Resources.Contact do
filterable? false
api Demo.CustomerService.Api
end
end

actions do
defaults [:read, :create, :update]

read :current_user do
get? true
end
end

policies do
policy always() do
forbid_unless(actor_present())

authorize_if(expr(id == type(^actor(:id), Ash.Type.UUID)))

forbid_if(always())
end
end

postgres do
repo Demo.Repo
table "user"
end
end
defmodule Demo.Auth.Resources.User do
@moduledoc """
The Logged in User
"""
use Demo.AuditedResource,
data_layer: AshPostgres.DataLayer,
extensions: [AshGraphql.Resource]

attributes do
uuid_primary_key :id, generated?: false

attribute :email, :string
attribute :email_confirmed_at, :utc_datetime

attribute :contact_id, :uuid

attribute :features, Demo.Auth.Resources.User.Features

# attribute :roles, {:array, :atom}

create_timestamp :inserted_at
update_timestamp :updated_at
end

rbac do
bypass(:admin)
role(:user, [:id, :email, :email_confirmed_at, :contact, :contact_id, :features])
end

audit do
actors?(false)
timestamps?(false)
end

graphql do
type :user

queries do
read_one :user, :current_user
end
end

relationships do
belongs_to :contact, Demo.CustomerService.Resources.Contact do
filterable? false
api Demo.CustomerService.Api
end
end

actions do
defaults [:read, :create, :update]

read :current_user do
get? true
end
end

policies do
policy always() do
forbid_unless(actor_present())

authorize_if(expr(id == type(^actor(:id), Ash.Type.UUID)))

forbid_if(always())
end
end

postgres do
repo Demo.Repo
table "user"
end
end
We could also observe this behaviour when loading a has_one relationship Versions: ash: 06329b97cf531b6b585630638028233a48a7fa0b ash_graphql: c70e7dec7dac1aac7fd40a67b51a6d61d67f9d41 ash_postgres: 9e31f905861c8f97bb2b54fd8604eb362391e675
63 replies
AEAsh Elixir
Created by barnabasj on 6/5/2023 in #support
get selected fields in create/update action
Hi, I have this policy to check if a user is only selecting fields that they are allowed to see. This worked well for reads because it was possible to get the selected/loaded fields from the query. Is it possible to do something similar for mutations
defmodule Demo.Policies.SelectsAllowedFields do
@moduledoc """
Checks if an action selects only fields that are allowed

Takes a mapping of roles to allowed fields as well as a bypass option
to circumvent the check for a given role

e.g:

user: [:id, :name, :last_name],
super_user: [:id, :address],
bypass: admin
"""
use Ash.Policy.SimpleCheck

require Logger

@impl true
def describe(_) do
"Checks if only allowed fields are selected"
end

@impl true
def match?(actor, context, options) do
bypass_role = options[:bypass]

matched =
case get_role(actor) do
role when is_atom(bypass_role) and role == bypass_role ->
true

role ->
match(
options[role],
[]
|> Enum.concat(get_selects(context))
|> Enum.concat(get_loads(context))
|> Enum.concat(get_calculations(context))
|> Enum.concat(get_aggregates(context))
)
end

if not matched do
dbg()
allowed_fields = options[get_role(actor)] || []

selected_fields =
[]
|> Enum.concat(get_selects(context))
|> Enum.concat(get_loads(context))
|> Enum.concat(get_calculations(context))
|> Enum.concat(get_aggregates(context))

Logger.debug("""
Actor (#{Map.get(actor, :id, "unknown")}) with role #{get_role(actor)} tried to access resource #{context.resource}"

selecting: #{inspect(selected_fields)}
allowed: #{inspect(allowed_fields)}
diff: #{inspect(selected_fields -- allowed_fields)}
""")
end

matched
end

def get_role(%{roles: roles}) when is_list(roles), do: List.first(roles)
def get_role(_), do: nil

# Get all attributes from the resource struct if no fields are selected
#
# @see https://www.ash-hq.org/docs/module/ash/2.4.10/ash-query#function-select-3
#
# ignore meta fields starting with `__`
# and field with structs as values as those point to
# calculations/aggregates/relationships
defp get_selects(%{query: %{select: nil}, resource: resource}),
do: get_resource_fields(resource)

defp get_selects(%{query: nil, resource: resource}),
do: get_resource_fields(resource)

defp get_selects(%{query: %{select: select}}), do: select
defp get_selects(e), do: raise(e)

defp get_resource_fields(resource) do
struct = resource.__struct__

struct
|> Map.keys()
|> Enum.filter(fn key ->
!String.starts_with?(to_string(key), "__") and
!Enum.any?([:aggregates, :calculations], fn special_field -> special_field == key end) and
!is_struct(
Map.get(
struct,
key
)
)
end)
end

defp get_loads(%{query: %{load: load}}), do: Keyword.keys(load)
defp get_loads(_), do: []
defp get_calculations(%{query: %{calculations: calculations}}), do: Map.keys(calculations)
defp get_calculations(_), do: []
defp get_aggregates(%{query: %{aggregates: aggregates}}), do: Map.keys(aggregates)
defp get_aggregates(_), do: []

defp match(allowed_fields, selected_fields)
when is_list(allowed_fields) and is_list(selected_fields) do
case selected_fields -- allowed_fields do
[] ->
true

_ ->
false
end
end

defp match(_, _), do: false
end
defmodule Demo.Policies.SelectsAllowedFields do
@moduledoc """
Checks if an action selects only fields that are allowed

Takes a mapping of roles to allowed fields as well as a bypass option
to circumvent the check for a given role

e.g:

user: [:id, :name, :last_name],
super_user: [:id, :address],
bypass: admin
"""
use Ash.Policy.SimpleCheck

require Logger

@impl true
def describe(_) do
"Checks if only allowed fields are selected"
end

@impl true
def match?(actor, context, options) do
bypass_role = options[:bypass]

matched =
case get_role(actor) do
role when is_atom(bypass_role) and role == bypass_role ->
true

role ->
match(
options[role],
[]
|> Enum.concat(get_selects(context))
|> Enum.concat(get_loads(context))
|> Enum.concat(get_calculations(context))
|> Enum.concat(get_aggregates(context))
)
end

if not matched do
dbg()
allowed_fields = options[get_role(actor)] || []

selected_fields =
[]
|> Enum.concat(get_selects(context))
|> Enum.concat(get_loads(context))
|> Enum.concat(get_calculations(context))
|> Enum.concat(get_aggregates(context))

Logger.debug("""
Actor (#{Map.get(actor, :id, "unknown")}) with role #{get_role(actor)} tried to access resource #{context.resource}"

selecting: #{inspect(selected_fields)}
allowed: #{inspect(allowed_fields)}
diff: #{inspect(selected_fields -- allowed_fields)}
""")
end

matched
end

def get_role(%{roles: roles}) when is_list(roles), do: List.first(roles)
def get_role(_), do: nil

# Get all attributes from the resource struct if no fields are selected
#
# @see https://www.ash-hq.org/docs/module/ash/2.4.10/ash-query#function-select-3
#
# ignore meta fields starting with `__`
# and field with structs as values as those point to
# calculations/aggregates/relationships
defp get_selects(%{query: %{select: nil}, resource: resource}),
do: get_resource_fields(resource)

defp get_selects(%{query: nil, resource: resource}),
do: get_resource_fields(resource)

defp get_selects(%{query: %{select: select}}), do: select
defp get_selects(e), do: raise(e)

defp get_resource_fields(resource) do
struct = resource.__struct__

struct
|> Map.keys()
|> Enum.filter(fn key ->
!String.starts_with?(to_string(key), "__") and
!Enum.any?([:aggregates, :calculations], fn special_field -> special_field == key end) and
!is_struct(
Map.get(
struct,
key
)
)
end)
end

defp get_loads(%{query: %{load: load}}), do: Keyword.keys(load)
defp get_loads(_), do: []
defp get_calculations(%{query: %{calculations: calculations}}), do: Map.keys(calculations)
defp get_calculations(_), do: []
defp get_aggregates(%{query: %{aggregates: aggregates}}), do: Map.keys(aggregates)
defp get_aggregates(_), do: []

defp match(allowed_fields, selected_fields)
when is_list(allowed_fields) and is_list(selected_fields) do
case selected_fields -- allowed_fields do
[] ->
true

_ ->
false
end
end

defp match(_, _), do: false
end
97 replies
AEAsh Elixir
Created by barnabasj on 5/26/2023 in #support
have the generator return values from a list (unique)
Hi, I would like to use the generator functionality to create resources and I have a attribute with specific codes. I'm looking for a way to pass a generator that returns values from a list but does not return the same value twice in order for the unique constraint to work correctly.
2 replies
AEAsh Elixir
Created by barnabasj on 5/15/2023 in #support
add ilike filter to graphql queries
Is it possible to extend the generated filter options in AshGraphql by e.g. an ilike filter?
2 replies
AEAsh Elixir
Created by barnabasj on 4/19/2023 in #support
no FunctionClause matching when loading calculation
Hi, somewhere between this commit de943509f73ff0c6ef3749feb3cc6ab6e4d99d2d and now, I started getting an error in one of my graphql queries.
[error] Task #PID<0.6039.0> started from #PID<0.6032.0> terminating
** (Ash.Error.Unknown) Unknown Error

Context: resolving data on calculate hotel_itinerary_items.sort_order
* Context: resolving data on calculate hotel_itinerary_items.sort_order

** (FunctionClauseError) no function clause matching in Ash.Actions.Read.run_query/6
(ash 2.6.31) lib/ash/actions/read.ex:2208: Ash.Actions.Read.run_query(#Ash.Query<resource: Demo.Offer.Resources.ItineraryItem.HotelItineraryItem, filter: #Ash.Filter<id in ["0a23ede2-ff04-4636-949f-321f9c9a4314", "5e65f643-7463-4456-8d19-e46c370f7950"]>>, #Ecto.Query<from h0 in JdlEngine.Offer.Resources.ItineraryItem.HotelItineraryItem, as: 0, where: type(as(0).id, {:parameterized, Ash.Type.UUID.EctoType, []}) in [
type(^"0a23ede2-ff04-4636-949f-321f9c9a4314", {:parameterized, Ash.Type.UUID.EctoType, []}),
type(^"5e65f643-7463-4456-8d19-e46c370f7950", {:parameterized, Ash.Type.UUID.EctoType, []})
], select: merge(merge(h0, %{}), %{
sort_order:
type(
fragment(
"(select sort_order from offer_itinerary_item oii where id = ? and item_type = ?)",
type(as(0).id, {:parameterized, Ash.Type.UUID.EctoType, []}),
^"h"
),
{:parameterized, Ash.Type.Integer.EctoType, []}
)
})>, %{actor: %{id: "App User", roles: [:admin]}, api: Demo.Support, authorize?: true, tenant: nil}, false, [], [])
(ash 2.6.31) lib/ash/actions/read.ex:2697: Ash.Actions.Read.run_calculation_query/6
(ash 2.6.31) lib/ash/actions/read.ex:1529: anonymous fn/14 in Ash.Actions.Read.calculation_dependency_requests/9
(ash 2.6.31) lib/ash/engine/request.ex:1048: Ash.Engine.Request.do_try_resolve_local/4
(ash 2.6.31) lib/ash/engine/request.ex:282: Ash.Engine.Request.do_next/1
(ash 2.6.31) lib/ash/engine/request.ex:211: Ash.Engine.Request.next/1
(ash 2.6.31) lib/ash/engine/engine.ex:719: Ash.Engine.advance_request/2
(ash 2.6.31) lib/ash/engine/engine.ex:625: Ash.Engine.fully_advance_request/2
(ash 2.6.31) lib/ash/engine/engine.ex:566: Ash.Engine.do_run_iteration/2
(elixir 1.14.3) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
(ash 2.6.31) lib/ash/engine/engine.ex:307: Ash.Engine.run_to_completion/1
(ash 2.6.31) lib/ash/engine/engine.ex:252: Ash.Engine.do_run/2
(ash 2.6.31) lib/ash/engine/engine.ex:148: Ash.Engine.run/2
(ash 2.6.31) lib/ash/actions/read.ex:173: Ash.Actions.Read.do_run/3
(ash 2.6.31) lib/ash/actions/read.ex:96: Ash.Actions.Read.run/3
(ash 2.6.31) lib/ash/api/api.ex:1349: Ash.Api.load!/4
(ash_graphql 0.23.3) lib/graphql/dataloader.ex:318: Dataloader.Source.AshGraphql.Dataloader.run_batch/2
(ash_graphql 0.23.3) lib/graphql/dataloader.ex:182: anonymous fn/2 in Dataloader.Source.AshGraphql.Dataloader.run_batches/1
(elixir 1.14.3) lib/task/supervised.ex:89: Task.Supervised.invoke_mfa/2
(elixir 1.14.3) lib/task/supervised.ex:34: Task.Supervised.reply/4

[error] Task #PID<0.6039.0> started from #PID<0.6032.0> terminating
** (Ash.Error.Unknown) Unknown Error

Context: resolving data on calculate hotel_itinerary_items.sort_order
* Context: resolving data on calculate hotel_itinerary_items.sort_order

** (FunctionClauseError) no function clause matching in Ash.Actions.Read.run_query/6
(ash 2.6.31) lib/ash/actions/read.ex:2208: Ash.Actions.Read.run_query(#Ash.Query<resource: Demo.Offer.Resources.ItineraryItem.HotelItineraryItem, filter: #Ash.Filter<id in ["0a23ede2-ff04-4636-949f-321f9c9a4314", "5e65f643-7463-4456-8d19-e46c370f7950"]>>, #Ecto.Query<from h0 in JdlEngine.Offer.Resources.ItineraryItem.HotelItineraryItem, as: 0, where: type(as(0).id, {:parameterized, Ash.Type.UUID.EctoType, []}) in [
type(^"0a23ede2-ff04-4636-949f-321f9c9a4314", {:parameterized, Ash.Type.UUID.EctoType, []}),
type(^"5e65f643-7463-4456-8d19-e46c370f7950", {:parameterized, Ash.Type.UUID.EctoType, []})
], select: merge(merge(h0, %{}), %{
sort_order:
type(
fragment(
"(select sort_order from offer_itinerary_item oii where id = ? and item_type = ?)",
type(as(0).id, {:parameterized, Ash.Type.UUID.EctoType, []}),
^"h"
),
{:parameterized, Ash.Type.Integer.EctoType, []}
)
})>, %{actor: %{id: "App User", roles: [:admin]}, api: Demo.Support, authorize?: true, tenant: nil}, false, [], [])
(ash 2.6.31) lib/ash/actions/read.ex:2697: Ash.Actions.Read.run_calculation_query/6
(ash 2.6.31) lib/ash/actions/read.ex:1529: anonymous fn/14 in Ash.Actions.Read.calculation_dependency_requests/9
(ash 2.6.31) lib/ash/engine/request.ex:1048: Ash.Engine.Request.do_try_resolve_local/4
(ash 2.6.31) lib/ash/engine/request.ex:282: Ash.Engine.Request.do_next/1
(ash 2.6.31) lib/ash/engine/request.ex:211: Ash.Engine.Request.next/1
(ash 2.6.31) lib/ash/engine/engine.ex:719: Ash.Engine.advance_request/2
(ash 2.6.31) lib/ash/engine/engine.ex:625: Ash.Engine.fully_advance_request/2
(ash 2.6.31) lib/ash/engine/engine.ex:566: Ash.Engine.do_run_iteration/2
(elixir 1.14.3) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
(ash 2.6.31) lib/ash/engine/engine.ex:307: Ash.Engine.run_to_completion/1
(ash 2.6.31) lib/ash/engine/engine.ex:252: Ash.Engine.do_run/2
(ash 2.6.31) lib/ash/engine/engine.ex:148: Ash.Engine.run/2
(ash 2.6.31) lib/ash/actions/read.ex:173: Ash.Actions.Read.do_run/3
(ash 2.6.31) lib/ash/actions/read.ex:96: Ash.Actions.Read.run/3
(ash 2.6.31) lib/ash/api/api.ex:1349: Ash.Api.load!/4
(ash_graphql 0.23.3) lib/graphql/dataloader.ex:318: Dataloader.Source.AshGraphql.Dataloader.run_batch/2
(ash_graphql 0.23.3) lib/graphql/dataloader.ex:182: anonymous fn/2 in Dataloader.Source.AshGraphql.Dataloader.run_batches/1
(elixir 1.14.3) lib/task/supervised.ex:89: Task.Supervised.invoke_mfa/2
(elixir 1.14.3) lib/task/supervised.ex:34: Task.Supervised.reply/4

I tried to look at it, but I wasn't able to figure it out yet. It happens when the :unkown branch is executed here: https://github.com/ash-project/ash/blob/5005d57b1db3522a09accc6f10418ac230fe2c40/lib/ash/actions/read.ex#L1526 The context inside the query is empty and no action is set, therefore no run_query function matches. The calulation it's trying to load is a simple expr calulcation with a fragment inside on a resource using AshPostgres Datalayer, no args are anything.
11 replies
AEAsh Elixir
Created by barnabasj on 4/3/2023 in #support
Map type with specific field/values
Hi, lately I've been creating more Graphql Mutations and I'm really missing a good way to type the arguments. I tried using embedded resources as types and that gave me nicely typed inputs but it did not work correctly as the underlying datastructure is a struct and all fields are always present, which lead to fields which were not being sent in the mutation being overwritten with nil. Is there a way to create typed maps right now?
20 replies
AEAsh Elixir
Created by barnabasj on 2/23/2023 in #support
Exposing data of the managed relationship
I created an action on Resource1 that creates another Resource2. Resource2 is created correctly using manage_relationship but I need to return the generated id for Resource2 and I have not found a way to get that information. Is it possible to access the return values from the action called by manage_relationship ? If they are accesible I could put them into metadata
5 replies
AEAsh Elixir
Created by barnabasj on 2/21/2023 in #support
no data in Ash.DataLayer.Simple
I have this action in one of my resources
read :read do
argument :ref, :string do
allow_nil? false
end

get? true

prepare fn query, _ ->
Ash.Query.before_action(query, fn query ->
case Api.quote(%Input{
option_ref_id: query.arguments.ref
}) do
{:ok, response} ->
data =
response
|> Map.get(:body)
|> get_in(["data", "hotelX", "quote", "optionQuote"])
|> JdlEngine.Util.Json.json_to_elixir()
|> Map.put(:id, query.arguments.ref)
|> IO.inspect(label: :data)

query = Ash.DataLayer.Simple.set_data(query, [data])

IO.inspect(Map.from_struct(query))

query

error ->
error
end
end)
end
end
read :read do
argument :ref, :string do
allow_nil? false
end

get? true

prepare fn query, _ ->
Ash.Query.before_action(query, fn query ->
case Api.quote(%Input{
option_ref_id: query.arguments.ref
}) do
{:ok, response} ->
data =
response
|> Map.get(:body)
|> get_in(["data", "hotelX", "quote", "optionQuote"])
|> JdlEngine.Util.Json.json_to_elixir()
|> Map.put(:id, query.arguments.ref)
|> IO.inspect(label: :data)

query = Ash.DataLayer.Simple.set_data(query, [data])

IO.inspect(Map.from_struct(query))

query

error ->
error
end
end)
end
end
I can see that ASh.DataLayer.Simple.set_data is called with the correct value, but I always get a %Ash.Error.SimpleDataLayer.NoDataProvidederror back. Not sure what I'm doing wrong Thanks in advance
13 replies
AEAsh Elixir
Created by barnabasj on 2/21/2023 in #support
new ash_graphql version generates duplicate enums
I tried upgrading today from ash_graphql fbebb21d1867acf9d43ad09c56374ad98ea8594f to fbebb21d1867acf9d43ad09c56374ad98ea8594f (also latest commits on other ash libraries) and I was unable to compile. I debugged a bit and was able to locate the problem here https://github.com/ash-project/ash_graphql/blob/0ecf70b54b78c6caa75d706ab4deacb214a3ee51/lib/resource/resource.ex#L2341 AshGraphql.all_attributes_and_arguments() returns all fields including the fields of embedded types. I have two fields descriptione_en and description_de both use the same embedded Type, the embedded type has a :boolean attribute with a one_of constraint. AshGraphql now tries to define the type twice for each boolean field. This results in a compile Error inside of absinthe. I was unsure on how to handle the embeded resources in that part of the code. The graphql enum should be defined for the embedded type I guess and not the one where it is embedded.
6 replies
AEAsh Elixir
Created by barnabasj on 2/3/2023 in #support
Recursive Flows
Hi I'm trying to right a flow that runs itself given a condition.
defmodule Demo.Inventory.Flows.Search do
@moduledoc """

"""

use Ash.Flow, otp_app: :demo

flow do
api(Demo.Support)

argument(:size, :integer)

argument(:token, :string)

argument(:query, :struct)

argument(:check_in, :date) do
allow_nil? false
end

argument(:check_out, :date) do
allow_nil? false
end

argument(:occupancies, {:array, Demo.Inventory.Resources.Types.Occupancy}) do
allow_nil? false
end

returns(:merge_results)
end

steps do
custom :get_hotels, Demo.Inventory.Flows.Steps.GetHotels do
input(%{
check_in: arg(:check_in),
check_out: arg(:check_out),
occupancies: arg(:occupancies),
page: [limit: arg(:size), after: arg(:token)]
})
end

custom :filter_unavailable_hotels, Demo.Inventory.Flows.Steps.FilterUnavailableHotels do
input %{
hotels: result(:get_hotels)
}
end

custom :need_more_hotels?, Demo.Inventory.Flows.Steps.NeedMoreHotels do
input %{
keyset: result(:get_hotels),
available_hotels: result(:filter_unavailable_hotels),
needed_size: arg(:size)
}
end

branch :maybe_get_more_hotels, result(:need_more_hotels?) do
output :get_next_token

custom :get_next_token, fn %{hotels: hotels}, _ ->
{:ok, get_in(Map.from_struct(List.last(hotels)), [:__metadata__, :keyset])}
end do
input %{
hotels: result(:filter_unavailable_hotels)
}
end

custom :get_number_of_missing_hotels, fn %{hotels: hotels, size: size}, _ ->
{:ok, size - length(hotels)}
end do
input %{
hotels: result(:filter_unavailable_hotels),
size: arg(:size)
}
end

run_flow :get_more_hotels, __MODULE__ do
input %{
check_in: arg(:check_in),
check_out: arg(:check_out),
occupancies: arg(:occupancies),
size: result(:get_number_of_missing_hotels),
token: result(:get_next_token)
}
end
end

custom :merge_results, Demo.Inventory.Flows.Steps.MergeResults do
input %{
hotels: result(:get_hotels),
more_hotels: result(:maybe_get_more_hotels)
}
end
end
end
defmodule Demo.Inventory.Flows.Search do
@moduledoc """

"""

use Ash.Flow, otp_app: :demo

flow do
api(Demo.Support)

argument(:size, :integer)

argument(:token, :string)

argument(:query, :struct)

argument(:check_in, :date) do
allow_nil? false
end

argument(:check_out, :date) do
allow_nil? false
end

argument(:occupancies, {:array, Demo.Inventory.Resources.Types.Occupancy}) do
allow_nil? false
end

returns(:merge_results)
end

steps do
custom :get_hotels, Demo.Inventory.Flows.Steps.GetHotels do
input(%{
check_in: arg(:check_in),
check_out: arg(:check_out),
occupancies: arg(:occupancies),
page: [limit: arg(:size), after: arg(:token)]
})
end

custom :filter_unavailable_hotels, Demo.Inventory.Flows.Steps.FilterUnavailableHotels do
input %{
hotels: result(:get_hotels)
}
end

custom :need_more_hotels?, Demo.Inventory.Flows.Steps.NeedMoreHotels do
input %{
keyset: result(:get_hotels),
available_hotels: result(:filter_unavailable_hotels),
needed_size: arg(:size)
}
end

branch :maybe_get_more_hotels, result(:need_more_hotels?) do
output :get_next_token

custom :get_next_token, fn %{hotels: hotels}, _ ->
{:ok, get_in(Map.from_struct(List.last(hotels)), [:__metadata__, :keyset])}
end do
input %{
hotels: result(:filter_unavailable_hotels)
}
end

custom :get_number_of_missing_hotels, fn %{hotels: hotels, size: size}, _ ->
{:ok, size - length(hotels)}
end do
input %{
hotels: result(:filter_unavailable_hotels),
size: arg(:size)
}
end

run_flow :get_more_hotels, __MODULE__ do
input %{
check_in: arg(:check_in),
check_out: arg(:check_out),
occupancies: arg(:occupancies),
size: result(:get_number_of_missing_hotels),
token: result(:get_next_token)
}
end
end

custom :merge_results, Demo.Inventory.Flows.Steps.MergeResults do
input %{
hotels: result(:get_hotels),
more_hotels: result(:maybe_get_more_hotels)
}
end
end
end
This leads to somekind of infinity recursion. I also tried copying the flow to a different file but that lead to an engine deadlock, it seems like the "sub" flow is run in the same context as the "parent" flow leading and as they both have the same step names it does not know which of the steps belong to which flow.
13 replies
AEAsh Elixir
Created by barnabasj on 2/2/2023 in #support
Array of Arrays argument
Should it be possible to create an argument of type {:array, {:array, :integer}}? Right now I'm getting a compile error.
** (KeyError) key :type not found in: [min_length: [type: :non_neg_integer, doc: "A minimum length for the items"], items: [type: :any, doc: "A schema for individual items"], max_length: [type: :non_neg_integer, doc: "A maximum length for the items"], nil_items?: [type: :boolean, doc: "Whether or not the list can contain nil items", default: false], empty_values: [type: {:list, :any}, doc: "A set of values that, if encountered, will be considered an empty list.", default: [""]]]
(elixir 1.14.3) lib/keyword.ex:1115: Keyword.update!/4
(elixir 1.14.3) lib/keyword.ex:1111: Keyword.update!/4
(spark 0.3.9) lib/spark/options_helpers.ex:143: anonymous fn/1 in Spark.OptionsHelpers.sanitize_schema/1
(elixir 1.14.3) lib/enum.ex:1658: Enum."-map/2-lists^map/1-0-"/2
(spark 0.3.9) lib/spark/options_helpers.ex:110: Spark.OptionsHelpers.validate/2
(ash 2.5.16) lib/ash/type/type.ex:949: Ash.Type.validate_constraints/2
(ash 2.5.16) lib/ash/type/type.ex:936: Ash.Type.set_type_transformation/1
lib/jdl_engine/inventory/resources/hotel.ex:111: (module)
** (KeyError) key :type not found in: [min_length: [type: :non_neg_integer, doc: "A minimum length for the items"], items: [type: :any, doc: "A schema for individual items"], max_length: [type: :non_neg_integer, doc: "A maximum length for the items"], nil_items?: [type: :boolean, doc: "Whether or not the list can contain nil items", default: false], empty_values: [type: {:list, :any}, doc: "A set of values that, if encountered, will be considered an empty list.", default: [""]]]
(elixir 1.14.3) lib/keyword.ex:1115: Keyword.update!/4
(elixir 1.14.3) lib/keyword.ex:1111: Keyword.update!/4
(spark 0.3.9) lib/spark/options_helpers.ex:143: anonymous fn/1 in Spark.OptionsHelpers.sanitize_schema/1
(elixir 1.14.3) lib/enum.ex:1658: Enum."-map/2-lists^map/1-0-"/2
(spark 0.3.9) lib/spark/options_helpers.ex:110: Spark.OptionsHelpers.validate/2
(ash 2.5.16) lib/ash/type/type.ex:949: Ash.Type.validate_constraints/2
(ash 2.5.16) lib/ash/type/type.ex:936: Ash.Type.set_type_transformation/1
lib/jdl_engine/inventory/resources/hotel.ex:111: (module)
4 replies
AEAsh Elixir
Created by barnabasj on 1/24/2023 in #support
Nested Embeds
Hi, I'm trying to create a resource with nested Embeds e.g.
:embed TimeRange

attributes do
attribute :start, :time
attribute :end, :time
end

:embed Checkin

attributes
attribute schedule: TimeRange
attribute ...
end

:postgres Hotel

attributes do
attribute :checkin, Checkin
...
end
:embed TimeRange

attributes do
attribute :start, :time
attribute :end, :time
end

:embed Checkin

attributes
attribute schedule: TimeRange
attribute ...
end

:postgres Hotel

attributes do
attribute :checkin, Checkin
...
end
But it seems Ecto cannot load structures like this If I understood this Issue https://github.com/elixir-ecto/ecto/issues/1256 correctly. Is this just a limitation we have to live with?
6 replies
AEAsh Elixir
Created by barnabasj on 1/24/2023 in #support
Compiler Warnings in Resources
Hi, I'm getting compiler warnings stating that I have unused aliases in my Resources but when I remove them I get compile errors because it can't find the module. Is it possible that this has something to do with how the resources get compiled?
5 replies