First, a disclaimer, I didn’t think too much about this idea so maybe there is already a better solution or even if this is possible giving GraphQL constraints.
The idea is to “replicate” the validate functionality of LV’s form validation but in the API’s generated by AshGraphql.
For example, right now, AFAIK, the only way to validate a create/update form using AshGraphql’s generated apis is to either submit the full form and get the errors, or to duplicate the validation in the frontend.
I was wondering if we could do something like adding a special field called validate_only or something like that that would allow the following:
Send the form with only a subset of the fields so the frontend can just send fields that the user filled and get validation for them only.
Only apply the changeset constraints and return instead of creating/updating the resource (basically replicating the functionality of a form validate).
Obs. Maybe (1) would not be possible with just the validate_only flag since the frontend graphql validation would block the user from sending fields that can’t be nullable? In that case, maybe another approach would be to add an option to the action in the graphql block that would generate another action only for validation where all fields can be nullable (or some other special type that indicates it is not filled yet), example:
graphql do
mutations do
create :create, :create, include_validation?: true
end
end
This would create a validateCreateResource api in graphql side besides the createResource api.
I looked a little bit into it and it seems possible to do.
What I’m not sure is how to tell absinthe to not validate the graphql constraints (if the field is required, etc) since I think this should be done by the Ash changeset during the validation.
The rest seems pretty straightforward, absinthe will give the arguments in the AshGraphql.Graphql.Resolver with only the fields the user really sent, that way we know which fields we should validate instead of validating the whole changeset.
With that we would just need to generate a changeset of the action, filter out the errors that are not from fields in the arguments (not sure how Ash does with the form validation) and return the errors if any.
Ah, so validation in Ash doesn’t happen “field-wise”, so it’s not like we validate a field at a time. We’d build a changeset with the given fields, and return any errors. I’m not entirely sure we should allow passing fields that can be statically confirmed to be required, but if that was desired we the input object for that query could be generated to not require the fields that would normally be required.
Is that also true for when using a AshPhoenix.Form.validate? Or do we actually send all the errors when we call that function and then the form filter only the ones related to the fields that the user already typed in the form?
Basically what I’m thinking is some good way to only show “errors” from fields that the user already typed instead of show errors for all fields. I’m guessing that this “filter” happens inside AshPhoenix.Form, but maybe it actually happens in the frontend by LiveView form?
Correct, it’s typically done in LV with something like Phoenix.Component.used_input?. We could implement something like that to hide errors for inputs that weren’t provided, but it may be better implemented on the front end itself.
I see, yep, I agree that this should be handled by the frontend.
So, to summarize:
A new validate query needs to be created for graphql;
The generated api will not force non-null so it will not be blocked by Absinthe and actually create the Ash code;
With the args, I generate the changeset for the action and run the constraints (can you indicate to me which function I should use that will run all constraints but will not persist the data to the DB?);
I return that to the client so the frontend can see the errors and display that to the end-user.
I can’t promise nothing since I never looked into ash_graphql code before, but as soon as I have some free time I will try to make something there to see if works.
Ash.Changeset.for_* or Ash.Query.for_read or Ash.ActionInput.for_action depending on the action type.
I think we should default to generating forced non-null constraints, but can make it an option to opt out? I’m open to not making that the default, not sure though.
Hey @zachdaniel, I advanced a little bit in this task. I now have a validate action that will correctly give me the errors and will put all fields as nullable. For example:
I still need to clean up the code a little bit before I push a PR and there are some issues that i was not able to fix and I was wondering if you would be able to fix/improve them in my PR.
Basically, the main issues are:
I wasn’t able to create it as a query api, only as a mutation one.
I couldn’t figure out how to remove the result field in the validation api since that is not needed at all.
This is an awesome start! Solving for #2 would involve creating a new type, like MutationNameValidationResult. Not sure about the 1st issue, I’d have to look at it. Could be a similar thing that we need to change it to return a new result type. Realistically I think all the “validation” endpoints should be queries as they do not make changes to the system. You are “querying” for “the validation errors that this action would produce”.
If you want to open a draft PR we could talk specifics? Really like where this is headed
Hey @zachdaniel , quick question.seems like the update mutation in AshGraphql will create a query and use Ash.bulk_update to any update action. I’m not 100% sure on how to “convert” this into an Ash.Changeset.for_update call.
For that, it would be changed to a “get and update” call. So first to retrieve the record by the id/identity provided, second to validate it according to the action.