-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add constraints phase #4
Conversation
92d473e
to
781ea8f
Compare
781ea8f
to
8ad276d
Compare
cc66e90
to
1e9ed35
Compare
|
||
defp get_constraint_module(constraint_name) do | ||
String.to_existing_atom( | ||
"Elixir.AbsintheHelpers.Constraints.#{Macro.camelize(Atom.to_string(constraint_name))}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note that unlike transforms, here we can't pass constraint modules directly as whatever we pass to a directive is rendered in the graphql schema itself 😓
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔
well.. I'm still not fan of "guessing modules". But at this point it's just an implementation detail - it will work either way.
One thing I would suggest is dropping the Constaints
modules. Right not they are mostly boilerplate, and logic form there could easily fit in this one - where all of them are documented. Something to think about
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe we can keep the modules separate for now, lets see how this grows, its a reversible decision :)
1b66424
to
c86b0d7
Compare
c86b0d7
to
8361bf0
Compare
else: {:ok, node} | ||
end | ||
|
||
def call(node = %{data: data}, {:max, max}) when is_binary(data) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How would the Constraints
directive work in conjunction with Transformation
? e.g If I've IDs in string format that I would like to transform to integers but also use the min/max constraints, what would be the order of execution? Is it transformation first and then constraints or do we get to set the order we like?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we decide on the order, it depends on the order its added to the pipeline, in our case I've added constraints then transforms here:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
btw, here how it look all together on a real-life schema:
%{message: :min_not_met, details: %{field: "override_ids", min: 5, value: 1}}, | ||
%{ | ||
message: :min_items_not_met, | ||
details: %{field: "location_ids", min_items: 2, items: [1]} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like it's missing scenario for max_items_not_met
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
now done here ✅
lib/phases/apply_constraints.ex
Outdated
field :my_list, list_of(:integer), directives: [constraints: [min_items: 2, max_items: 5, min: 1, max: 100]] do | ||
resolve(&MyResolver.resolve/3) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've now updated docs and test scenarios for demonstration of this 👍 ✅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
btw, note how it is still useful to have directive inline as a key when it is applied to an arg
and there is several of these args
received by the field
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep
|> Absinthe.Plug.default_pipeline(opts) | ||
|> AbsintheHelpers.Phases.ApplyConstraints.add_to_pipeline(opts) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do wonder if this is needed at all. They way I understand directives, they can (should) already include the implementation (with the extend
block?).
I think it would simplify a little bit the introduction into projects.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The directive is extended, but we still need to hook into the pipeline to interpret and execute the added constraints and their params
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated README here too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👌 I see it now.
610fdd7
to
509e123
Compare
509e123
to
6641e6e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please do review (rethink? is that right word?) values
being returned in errors. I'm still 👌 with discussing it, just would like to be sure that it will be considered.
And the valuse
is the only real "issue" I have.
@@ -1,16 +1,132 @@ | |||
# AbsintheHelpers | |||
# Absinthe Helpers |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice.
Since you are using README.md
you can use it at a main page of the docs - like we are doing here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done ✅
phase: __MODULE__, | ||
message: reason, | ||
extra: %{ | ||
details: Map.merge(details, %{field: node.name}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👌
that much better than what we got.
I would be much better if we could provide full path to it (like create_booing.service.cost
), but I'm not sure if that's possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would be much better if we could provide full path to it (like create_booing.service.cost), but I'm not sure if that's possible.
yep, in a separate PR 👍
lib/constraints/max.ex
Outdated
if String.length(data) > max, | ||
do: {:error, :max_exceeded, %{max: max, value: data}}, | ||
else: {:ok, node} | ||
end | ||
|
||
def call(node = %{data: data}, {:max, max}) do | ||
if data > max, | ||
do: {:error, :max_exceeded, %{max: max, value: data}}, | ||
else: {:ok, node} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if String.length(data) > max, | |
do: {:error, :max_exceeded, %{max: max, value: data}}, | |
else: {:ok, node} | |
end | |
def call(node = %{data: data}, {:max, max}) do | |
if data > max, | |
do: {:error, :max_exceeded, %{max: max, value: data}}, | |
else: {:ok, node} | |
if String.length(data) > max, | |
do: {:error, :max_exceeded, %{max: max}}, | |
else: {:ok, node} | |
end | |
def call(node = %{data: data}, {:max, max}) do | |
if data > max, | |
do: {:error, :max_exceeded, %{max: max}}, | |
else: {:ok, node} |
I would be against passing values here. I'm always afraid, that if that's a struct we will have problems with serializing those. But what worries me more, is that we are putting into error
parameters send to us from users. Some services are putting those errors into logs (all returned errors from graplQL - since we don't really have error codes...) and it could lead to leaking of confidential information :/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(and that goes for all values added to errors)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, removed user values from errors ✅
|> Absinthe.Plug.default_pipeline(opts) | ||
|> AbsintheHelpers.Phases.ApplyConstraints.add_to_pipeline(opts) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👌 I see it now.
|
||
defp get_constraint_module(constraint_name) do | ||
String.to_existing_atom( | ||
"Elixir.AbsintheHelpers.Constraints.#{Macro.camelize(Atom.to_string(constraint_name))}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔
well.. I'm still not fan of "guessing modules". But at this point it's just an implementation detail - it will work either way.
One thing I would suggest is dropping the Constaints
modules. Right not they are mostly boilerplate, and logic form there could easily fit in this one - where all of them are documented. Something to think about
end | ||
|
||
def call(node = %{data: data = %Decimal{}}, {:max, max}) do | ||
if is_integer(max) and Decimal.gt?(data, max), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also I do wonder if we need to use the Decimal
here 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remember that these constraint values are reflected in the GraphQL schema. I'm unsure how we should represent decimals in that context. Should we represent them as strings and parse them here, or as floats? In the Surgex parsers, we compared decimals against integers, maybe we'll be fine here as well, this is reversible too
Validates input nodes against constraints defined by the
constraints
directive in your Absinthe schema. Constraints can be applied to fields and arguments, enforcing rules such asmin
,max
, etc. These constraints can be applied to both individual items and lists.Example usage
Add this phase to your pipeline in your router:
Add the constraints directive's prototype schema to your schema:
Apply constraints to a field or argument: