diff --git a/README.md b/README.md index b8914189..a7e8208a 100755 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ some unsupported areas: - Interfaces are also converted into polymorphic variants. Overlapping interface selections and other more uncommon use cases are not yet supported. - Basic fragment support +- Required arguments validation - you're not going to miss required arguments on any field. ## Extra features @@ -268,8 +269,11 @@ By default graphql_ppx uses `graphql_schema.json` filed from your root directory ``` npm install -g esy@latest +esy @402 install +esy @402 dune build -p graphql_ppx +# or esy install -esy build +esy dune build -p graphql_ppx ``` ## Running tests diff --git a/graphql_schema.json b/graphql_schema.json index eeb5d9f1..aa8bc6b7 100644 --- a/graphql_schema.json +++ b/graphql_schema.json @@ -517,7 +517,7 @@ { "kind": "SCALAR", "name": "Int", - "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ", + "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.", "fields": null, "inputFields": null, "interfaces": null, @@ -527,7 +527,7 @@ { "kind": "SCALAR", "name": "Float", - "description": "The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point). ", + "description": "The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).", "fields": null, "inputFields": null, "interfaces": null, @@ -1202,6 +1202,61 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "optionalInputArgs", + "description": null, + "args": [ + { + "name": "required", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "optional", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "anotherRequired", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -2013,54 +2068,6 @@ }, "isDeprecated": false, "deprecationReason": null - }, - { - "name": "onOperation", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": true, - "deprecationReason": "Use `locations`." - }, - { - "name": "onFragment", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": true, - "deprecationReason": "Use `locations`." - }, - { - "name": "onField", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": true, - "deprecationReason": "Use `locations`." } ], "inputFields": null, @@ -2118,6 +2125,12 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "VARIABLE_DEFINITION", + "description": "Location adjacent to a variable definition.", + "isDeprecated": false, + "deprecationReason": null + }, { "name": "SCHEMA", "description": "Location adjacent to a schema definition.", @@ -2489,7 +2502,7 @@ "args": [ { "name": "reason", - "description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted in [Markdown](https://daringfireball.net/projects/markdown/).", + "description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax (as specified by [CommonMark](https://commonmark.org/).", "type": { "kind": "SCALAR", "name": "String", diff --git a/schema.graphql b/schema.graphql index cd3e9ae0..7f316bf2 100644 --- a/schema.graphql +++ b/schema.graphql @@ -54,6 +54,11 @@ type Query { type Mutation { mutationWithError: MutationWithErrorResult! + optionalInputArgs( + required: String! + optional: String + anotherRequired: String! + ): String! } type Subscription { diff --git a/src/base/option.re b/src/base/option.re index 9ee7ae6c..6bad1272 100644 --- a/src/base/option.re +++ b/src/base/option.re @@ -14,3 +14,8 @@ let unsafe_unwrap = fun | None => raise(Option_unwrap_error) | Some(v) => v; + +let get_or_else = default => + fun + | None => default + | Some(v) => v; diff --git a/src/base/rule_required_arguments.re b/src/base/rule_required_arguments.re new file mode 100644 index 00000000..7fb5ca93 --- /dev/null +++ b/src/base/rule_required_arguments.re @@ -0,0 +1,44 @@ +module Visitor: Traversal_utils.VisitorSig = { + open Traversal_utils; + open Source_pos; + open Graphql_ast; + open Schema; + + include AbstractVisitor; + + let enter_field = (_self, ctx, def) => { + let field_meta = + Context.parent_type(ctx) + |> Option.flat_map(t => Schema.lookup_field(t, def.item.fd_name.item)); + + let provided_args = + def.item.fd_arguments + |> Option.map(span => span.item) + |> Option.get_or_else([]) + |> List.map(arg => (arg |> fst).item); + + let expected_args = + field_meta + |> Option.map(fm => fm.fm_arguments) + |> Option.get_or_else([]) + |> List.filter(arg => + switch(arg.am_arg_type) { + | NonNull(_) => true + | _ => false + }); + + expected_args + |> List.iter(arg => { + let provided = provided_args |> List.exists(arg_name => arg_name == arg.am_name); + if(!provided) { + let message = + Printf.sprintf("Argument \"%s\" on field \"%s\" not provided", arg.am_name, def.item.fd_name.item); + + Context.push_error(ctx, def.span, message); + } + }); + }; + + type t = unit; + let make_self = () => (); +}; diff --git a/src/base/validations.re b/src/base/validations.re index 0afe74d7..bda870a6 100644 --- a/src/base/validations.re +++ b/src/base/validations.re @@ -6,7 +6,10 @@ module AllRulesImpl = ( Multi_visitor.Visitor( Rule_no_unused_variables.Visitor, - Multi_visitor.NullVisitor, + Multi_visitor.Visitor( + Rule_required_arguments.Visitor, + Multi_visitor.NullVisitor, + ) ) ), ); diff --git a/tests_bucklescript/__tests__/mutationWithArgs.re b/tests_bucklescript/__tests__/mutationWithArgs.re new file mode 100644 index 00000000..b338a124 --- /dev/null +++ b/tests_bucklescript/__tests__/mutationWithArgs.re @@ -0,0 +1,14 @@ +open Jest; +open Expect; + +module MyQuery = [%graphql {| + mutation MyMutation($required: String!) { + optionalInputArgs(required: $required, anotherRequired: "val") + } +|}]; + +describe("Mutation with args", () => + test("Printed query is a mutation", () => + MyQuery.query |> Js.String.indexOf("mutation") |> expect |> toBe(0) + ) +); diff --git a/tests_bucklescript/run.js b/tests_bucklescript/run.js index 2128d103..56d67aa1 100644 --- a/tests_bucklescript/run.js +++ b/tests_bucklescript/run.js @@ -27,7 +27,7 @@ async function test(folder) { await command(`cp ./${folder}/* .`); await command("npm install"); await command("npm run test"); - await cleanup(); + // await cleanup(); } async function run() {