Skip to content
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

feature: Add variable validation rule to query save #249

Closed

Conversation

markkelnar
Copy link
Contributor

@markkelnar markkelnar commented Aug 16, 2023

When processing a persisted query document string for saving, run the validation rules on the string. Add a new rule to error for arguments that are present in operations but instead should be a variable instead. Return the graphql Error or display the error in the editor.

Github issue

@markkelnar
Copy link
Contributor Author

Example query string validation against the graphql rules and error displayed in Admin Editor. In this example, a draft query was being saved as draft with 'typename' should have been '__typename'.

Screenshot 2023-08-18 at 9 01 21 AM

@@ -306,6 +307,15 @@ public function valid_or_throw( $post_content, $post_id ) {
throw new RequestError( sprintf( __( 'This query has already been associated with another query "%s"', 'wp-graphql-smart-cache' ), $existing_post->post_title ) );
}

// During save/parse of query string, check for variables of the query string.
// "To save this query, arguments should use variables instead of static values."
$rules = DocumentValidator::defaultRules();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds the other rules to the query string validation.

@@ -55,7 +55,7 @@ public function testQueriesAreDeletedByJob() {
[
'post_type' => 'graphql_document',
'post_date' => $date_string,
'post_content' => sprintf( "query Saved_%d { typename }", $counter ),
'post_content' => sprintf( "query Saved_%d { __typename }", $counter ),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing the rule validation, brought to attention some invalid test query strings. :)

// "query FooQuery { post( id: "1", idType: DATABASE_ID) { title } }" <-- this is the one we want to error on !!!!!
// "query ($num: ID!) { post( id: "1", idType: DATABASE_ID) { title } }"
// "query ($num: ID!) { post( id: $num, idType: DATABASE_ID) { title } }"
// looking for [4] => argument: (id) ()\n [5] => variable: (num)\n
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, my notes are in here. Ha!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd want to error on all 3 of these examples.

The problem we're trying to prevent, is folks doing string concatenation for their queries and instead push folks toward using variables.

All 3 of those queries could be concatenated like so:


example: query FooQuery { post( id: "1", idType: DATABASE_ID) { title } }

const id = 123;
const query = `query FooQuery { post( id: ${id}, idType: DATABASE_ID) { title } }`

example: query ($num: ID!) { post( id: "1", idType: DATABASE_ID) { title } } (perhaps this was a mistake, but this looks like it might be invalid already as an argument is defined but not used)

const id = 123;
const idType: "DATABASE_ID";
const query = `query ($num: ID!) { post( id: ${id}, idType: ${idType}) { title } }`

example: query ($num: ID!) { post( id: $num, idType: DATABASE_ID) { title } }

const idType = "DATABASE_ID";
const query = `query ($num: ID!) { post( id: $num, idType: ${idType}) { title } }`

In each of these cases we have arguments passed in to fields that do not map to an argument defined for the query.

The expected "valid" definition of the query would be:

query ($id: ID! $idType: PostIdType ) {
  post( id: $id idType: $idType ) {
    title
  }
}

Here, the field has 2 arguments, and both are defined in the operation.

If either of the arguments are not defined on the operation and used at the field level as an argument, then we might still be letting concatenated query strings persist many permutations of the same query, which is the situation we're trying to prevent.


Copy link
Collaborator

@jasonbahl jasonbahl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe this validates fully. See my comments re: how this still allows the problem we're trying to prevent.

// "query FooQuery { post( id: "1", idType: DATABASE_ID) { title } }" <-- this is the one we want to error on !!!!!
// "query ($num: ID!) { post( id: "1", idType: DATABASE_ID) { title } }"
// "query ($num: ID!) { post( id: $num, idType: DATABASE_ID) { title } }"
// looking for [4] => argument: (id) ()\n [5] => variable: (num)\n
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd want to error on all 3 of these examples.

The problem we're trying to prevent, is folks doing string concatenation for their queries and instead push folks toward using variables.

All 3 of those queries could be concatenated like so:


example: query FooQuery { post( id: "1", idType: DATABASE_ID) { title } }

const id = 123;
const query = `query FooQuery { post( id: ${id}, idType: DATABASE_ID) { title } }`

example: query ($num: ID!) { post( id: "1", idType: DATABASE_ID) { title } } (perhaps this was a mistake, but this looks like it might be invalid already as an argument is defined but not used)

const id = 123;
const idType: "DATABASE_ID";
const query = `query ($num: ID!) { post( id: ${id}, idType: ${idType}) { title } }`

example: query ($num: ID!) { post( id: $num, idType: DATABASE_ID) { title } }

const idType = "DATABASE_ID";
const query = `query ($num: ID!) { post( id: $num, idType: ${idType}) { title } }`

In each of these cases we have arguments passed in to fields that do not map to an argument defined for the query.

The expected "valid" definition of the query would be:

query ($id: ID! $idType: PostIdType ) {
  post( id: $id idType: $idType ) {
    title
  }
}

Here, the field has 2 arguments, and both are defined in the operation.

If either of the arguments are not defined on the operation and used at the field level as an argument, then we might still be letting concatenated query strings persist many permutations of the same query, which is the situation we're trying to prevent.


$I->seePostInDatabase( [
'post_type' => 'graphql_document',
'post_status' => 'publish',
'post_content' => "query (\$num: ID!) {\n post(id: \$num, idType: DATABASE_ID) {\n title\n }\n}\n",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This query should fail validation because the idType could be concatenated still, leading to duplicate query documents being stored for different possible values.

i.e.

const idTye = 'DATABASE_ID';
const query = `query ($num: ID!) { post( id: $num, idType: ${idTye}) { title } }`

or:

const idTye = 'SLUG';
const query = `query ($num: ID!) { post( id: $num, idType: ${idTye}) { title } }`

By using string interpolation / concatenation, we'd be able to have multiple versions of the same query, which is the situation we're trying to prevent.

Ideally we'd be guiding the user to:

const query = `query ($num: ID! $idType: PostIdType) { post( id: $num, idType: $idType }) { title } }`

Where here, each argument on the field level corresponds with a variable defined at the operation level.

Copy link
Collaborator

@jasonbahl jasonbahl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 I checked out this branch:

CleanShot 2023-08-24 at 10 25 25

And was able to sucessfully save this query:

CleanShot 2023-08-24 at 10 22 47

Copy link
Collaborator

@jasonbahl jasonbahl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried another query, and was able to publish it without error.

CleanShot 2023-08-24 at 11 20 53


https://github.com/wp-graphql/wp-graphql-smart-cache/pull/249/files#diff-cb45ae6ce2d1924aad6d649880ebd4bcf3ce4a8dd481b8f8be1d14e84ea60af6R34

I'm not super familiar with what's available in the ArgumentNode, but I'm curious if we can do any other type of check other than the in_array() check here 🤔

I wonder if there's prior art in the JavaScript GraphQL Ecosystem for this type of validation rule too? Maybe we can borrow from them, if there is.

@markkelnar
Copy link
Contributor Author

markkelnar commented Sep 5, 2023

If this is merged as is, apps with existing APQ style persisted queries that break this validation rule will fail to save. Thus a change in behavior and an impact to caching performance.

Consider making this validation on saving queries a) optional, opt-in, with a checkbox. b) only enforced in WP admin editor while saving a query.

@markkelnar markkelnar added the needs: discussion Requires a discussion to proceed label Sep 5, 2023
@jasonbahl jasonbahl added needs: author response Pending information from the author needs: reviewer response This needs the attention of a codeowner or maintainer and removed needs: author response Pending information from the author labels Sep 19, 2023
@jasonbahl
Copy link
Collaborator

closing for now. We can reference this PR when work begins again on #224.

@jasonbahl jasonbahl closed this Feb 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs: discussion Requires a discussion to proceed needs: reviewer response This needs the attention of a codeowner or maintainer
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants