Raw actions

CFE:
defmodule App.Echo do
use Ash.Resource,
extensions: [AshJsonApi.Resource, AshGraphql.Resource]

actions do
raw :echo, fn input ->
{:ok, input}
end
end

graphql do
# ...
mutations do
create :echo, :echo
end
end

json_api do
# ...
post :echo
end
end
defmodule App.Echo do
use Ash.Resource,
extensions: [AshJsonApi.Resource, AshGraphql.Resource]

actions do
raw :echo, fn input ->
{:ok, input}
end
end

graphql do
# ...
mutations do
create :echo, :echo
end
end

json_api do
# ...
post :echo
end
end
Would there be room for something like this in core? One of the big strengths of Ash is being able to package up business logic once and expose it over several channels, but sometimes the plumbing that makes things that fit the model so easy makes other things that don't quite fit tough. I'm not married to the specific syntax, rather food for thought regarding the functionality. It would be great to have a layer in the middle between the JSON:API/GraphQL/etc. frontend transformers and the underlying CRUD, Query and Changeset wiring. Is there already a way folks are accomplishing this besides manually adding plug routers/absinthe resolvers? Has there been any discussion of something like this?
44 Replies
ZachDaniel
ZachDaniel3y ago
Yes, there is absolutely a place for this in core, and I intend to add it soon 🙂
\ ឵឵឵
\ ឵឵឵OP3y ago
For sure this would be slightly more interesting if one could provide a cross-transformer way to describe the input and output schemas for the transformers to consume.
ZachDaniel
ZachDaniel3y ago
The syntax will be:
action :name, <return_type> do
argument :argument, :foo
step <just like a change>
impl fn input, context ->
{:ok, return_type}
end
end
action :name, <return_type> do
argument :argument, :foo
step <just like a change>
impl fn input, context ->
{:ok, return_type}
end
end
and it will have an action lifecycle just like any other action
\ ឵឵឵
\ ឵឵឵OP3y ago
Fantastic, this will be really nice.
ZachDaniel
ZachDaniel3y ago
Will likely operate on a struct called something like %Ash.Action.Input{} or perhaps %Ash.Action.Execution{} some names to be decided on still there 😆 Once we have action :name, <return_type> we will also get flow :name, FlowModule action types for dispatching directly to an Ash.Flow
\ ឵឵឵
\ ឵឵឵OP3y ago
I'd imagine that input in this case will also carry along a little bit more of the upper level channel stuff than currently do the CRUD actions.
ZachDaniel
ZachDaniel3y ago
🤔 probably not? Anything you want to capture from the parent scope can be added to the context in a plug So you can thread through whatever you want and have it at changeset.context.whatever and query.context.whatever What did you have in mind for what additionally we'd supply?
\ ឵឵឵
\ ឵឵឵OP3y ago
It's not the end of the world to have plugs to inject things like headers etc. but given this action type is quite specifically targeting cases where you want to break the mold a bit, maybe it would make sense to pass that stuff through.
ZachDaniel
ZachDaniel3y ago
Yeah, I see what you're saying...I think what we would do to support that is add an option to all actions for a way to capture transport specific stuff
\ ឵឵឵
\ ឵឵឵OP3y ago
Like, why not pass along a struct that is keyed on the incoming channel type (:graphql, :json_api) and dump a bunch of stuff in there that users can access if they need it.
ZachDaniel
ZachDaniel3y ago
read :whoami do
transport_context fn :ash_graphql, conn ->
get_ip(conn)
end
end
read :whoami do
transport_context fn :ash_graphql, conn ->
get_ip(conn)
end
end
Yeah
\ ឵឵឵
\ ឵឵឵OP3y ago
I honestly wouldn't hate that happening for the existing action types either, but I don't find the need quite as often. Similarly, if I really want that I can obviously just inject the whole Conn into my context now (;
ZachDaniel
ZachDaniel3y ago
The explicit mapper is more verbose I guess, but what I like is that you can look at an action and understand how it needs to be adapted to another transport layer. Just passing everything in means some random thing deep down could be accessing something off of the conn and now Ash ceases to be the unifying layer in between but I like the spirit of this, and I think there is a way to get the best of both worlds
\ ឵឵឵
\ ឵឵឵OP3y ago
That's what I was thinking with the keying, though. That makes it pretty legible in the action itself where you're pulling stuff off of, and easy to catch by CaseClauseError when there's an action that wants stuff off the transport but hasn't implemented it. I definitely get that the actions are intended to be mostly transport-agnostic, and I get the appeal from that standpoint to extract just what needs to be there from whatever upstream looks like.
ZachDaniel
ZachDaniel3y ago
Yeah, the main problem is that action invocations and changes will often be in other files where their changes live, that kind of thing So its not so easy to see what they are using and usually that isn't a big deal because you know it can only be transformations of the resource itself So I think something that leaves the transport layer at the front door will be important Even if its just use_transport_layer_context [:ash_graphql, :ash_json_api] (but I think I like the function better :D) I'll have to play with it though to see how it feels. If you were interested that could be an interesting project to tackle
\ ឵឵឵
\ ឵឵឵OP3y ago
Sounded like this stuff was pretty early-stage at this point. Is there a branch yet, or still on the drawing board?
ZachDaniel
ZachDaniel3y ago
oh, no I mean the transport layer stuff no work in progress on either thing though, but open to contributions for both 🙂
\ ឵឵឵
\ ឵឵឵OP3y ago
Something like this looks good to me, I think that would cover 90% of my use-cases already. Did you have any plan in mind for specifying the schema a bit more flexibly than argument? Or augmenting argument to take structured types?
ZachDaniel
ZachDaniel3y ago
argument can take arbitrarily structured types just fine argument :argument_name, SomeEmbeddedresource would generally be the way to do that
\ ឵឵឵
\ ឵឵឵OP3y ago
Nice
ZachDaniel
ZachDaniel3y ago
but you could also define a custom type that implements the graphql_type and json_schema callbacks for ash_graphql and ash_json_api respectively (or only one of those if you're only using one of them)
\ ឵឵឵
\ ឵឵឵OP3y ago
Definitely. Is there a way to define this (embedded resource style types) inline? For top-level types I for sure don't mind having a seperate resource module, but you can imagine in the case where you have deeply-nested types for I/O it could get a bit messy.
ZachDaniel
ZachDaniel3y ago
🤔 well, kind of You could make a custom map type
defmodule YourMap do
use Ash.Type

....

def json_schema(constraints), do: constraints[:json_schema]
end
defmodule YourMap do
use Ash.Type

....

def json_schema(constraints), do: constraints[:json_schema]
end
And then do
argument :input, YourMap do
constraints json_schema: %{
...
}
end
argument :input, YourMap do
constraints json_schema: %{
...
}
end
or we could potentially support json_schema in the core map type but there are compiler issues w/ actually defining types in-line (maybe side-steppable)
\ ឵឵឵
\ ឵឵឵OP3y ago
Great! This looks like a thing that could make good use of a DSL as well, I must say.
type :a, :object do
type :a, :string
type :b, :array, :object do
type :a, :string,
type :b, :integer
end
end
end
type :a, :object do
type :a, :string
type :b, :array, :object do
type :a, :string,
type :b, :integer
end
end
end
ZachDaniel
ZachDaniel3y ago
Potentially 😄 That could be its own package. Like an ash_type_builder for building complex types that will automatically define graphql object types and json schemas.
\ ឵឵឵
\ ឵឵឵OP3y ago
You can get validation as well by being able to use Ash.Type.NewType, embedded resources, etc.
ZachDaniel
ZachDaniel3y ago
I think there are some dragons waiting to rear their head with that kind of design really, which is why Ash sticks to arguments/embeds, and if you look at complex data validation/data shape tools, it gets really hairy at some point. We try to push people down a more easy to model path
\ ឵឵឵
\ ឵឵឵OP3y ago
For sure, it could be its own thing, basically just constructing Ash.Types. Depends whether you want to force new transformers to support it. But maybe those types could internally be transformed into embedded resources if the support for generating GQL and JSON types off of those is already there. (and would also be mandatory for new transformers)
ZachDaniel
ZachDaniel3y ago
Yeah, embedded resources can be automatically turned into json schemas/graphql types I see no harm in making a more terse way of declaring deeply nested embeds and we could potentially support it in core, similar to the way that ecto supports in-line embeds
\ ឵឵឵
\ ឵឵឵OP3y ago
I guess it's a reasonably straightforward transform if you make use of nested modules.
ZachDaniel
ZachDaniel3y ago
We do actually also eventually want a json schema/open api -> ash resource transformer But even still, the hard part of all of that is that embeds support actions & calculations and all of that, stuff that make your deeply nested data much richer
\ ឵឵឵
\ ឵឵឵OP3y ago
This would be for exposing other existing APIs as Ash resources, no?
ZachDaniel
ZachDaniel3y ago
Yeah Basically to generate a rich client library that can be plugged into your other resources with relationships & calculations and the like
\ ឵឵឵
\ ឵឵឵OP3y ago
The raw action type also goes a long way in that regard.
ZachDaniel
ZachDaniel3y ago
i.e
has_many :open_github_issues, Github.Issue do
filter expr(status == :open)
end
has_many :open_github_issues, Github.Issue do
filter expr(status == :open)
end
\ ឵឵឵
\ ឵឵឵OP3y ago
Right on, that makes sense. This seems something close to writing your own data layer, but partially so. Given a lot of those APIs won't expose a full CRUD, will have various structures, etc.
ZachDaniel
ZachDaniel3y ago
Yeah, we need a lot of different action types at the end of the day For example:
actions do
# takes a single record and returns the value of a given calculation
calculate :get_some_calc, :some_calc

# returns an aggregation over all records
count :total_count do
...
end
end
actions do
# takes a single record and returns the value of a given calculation
calculate :get_some_calc, :some_calc

# returns an aggregation over all records
count :total_count do
...
end
end
all of which will be wrappers over the generic action
\ ឵឵឵
\ ឵឵឵OP3y ago
I mean, I like the idea of being able to try to map what you can onto the Ash functionality. If the backend lets you paginate, you tell it you can paginate and write a bit of code to transform the Ash pagination input onto the backend structure. If not, you say you can't paginate. Then you can write other actions on it as usual and mix it into Queries + Changesets.
ZachDaniel
ZachDaniel3y ago
Yeah, the idea is that if you can map your domain stuff onto our kinds of things (read, create, update, destroy) then you are going to have a lot of work done for you The main thing is just that there are other shapes of actions that we want to support so that we cover the case if you think of actions as type signatures it makes more sense
read() :: [record]
create() :: record
update(record) :: record
destroy(record) :: :ok
read() :: [record]
create() :: record
update(record) :: record
destroy(record) :: :ok
So we're missing
calculate(record) :: value
aggregate() :: value
action() :: value
calculate(record) :: value
aggregate() :: value
action() :: value
aggregate is just a convenience really, an alternative to action() when you want to do stuff we know how to aggregate for you already
\ ឵឵឵
\ ឵឵឵OP3y ago
And I believe bulk versions of CUD are on the way as well.
ZachDaniel
ZachDaniel3y ago
yes, I've been unfortunately delayed on those projects but I'm planning on picking them back up soon atomic updates will come with bulk updates but those won't be separate action types you'll be able to use any (with maybe some exceptions) CUD action type as a bulk action, with tools to optimize for that case
\ ឵឵឵
\ ឵឵឵OP3y ago
No, but I guess they'll change the type signature?
create() :: record | [record]
update(record | [record]) :: record | [record]
create() :: record | [record]
update(record | [record]) :: record | [record]
Or just [record] as the case may be.
ZachDaniel
ZachDaniel3y ago
ah, yes. With an | :ok at the end since most bulk actions probably won't want to return everything but yes that is the general shape we're achieving I think I'll be back onto bulk actions this coming week or next week, if all goes well I'll have bulk creates merged by end of next week.
\ ឵឵឵
\ ឵឵឵OP3y ago
Right, awesome. I dropped the new OpenAPI stuff into one of my projects earlier. I'm gonna open a new thread for that, 1m.

Did you find this page helpful?