Created
June 24, 2022 14:06
-
-
Save wabrit/2d1e1f9520aa133908f0a3716338e5ff to your computer and use it in GitHub Desktop.
POC for GraphQL DSL for Pact-JS V3
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
MatchersV3, | |
PactV3, | |
PactV3Options, | |
V3Request, | |
V3Response, | |
GraphQLVariables | |
} from '@pact-foundation/pact'; | |
import { gql } from '@apollo/client/core'; | |
import { ASTNode, print } from 'graphql'; | |
class ConfigurationError extends Error { | |
} | |
class GraphQLQueryError extends Error { | |
} | |
enum OperationType { | |
Mutation = 'Mutation', | |
Query = 'Query' | |
} | |
/** | |
* The PactV3 implementation does not currently support v2's GraphQLInteraction class, | |
* so on the advisement of pact gurus the approach taken for now is to subclass PactV3 to | |
* expose a GraphQL DSL to the pact author; mostly these are thin wrappers around the | |
* underlying class. | |
*/ | |
export class GraphQLPactV3 extends PactV3 { | |
private operation?: string = undefined; | |
private variables?: GraphQLVariables = undefined; | |
private query?: string | ASTNode = undefined; | |
private req?: V3Request = undefined; | |
/** | |
* Constructor. | |
* @param opts {PactV3Options} options for mock server | |
*/ | |
constructor(opts: PactV3Options) { | |
super(opts); | |
} | |
/** | |
* The GraphQL operation name, if used. | |
* @param operation {string} the name of the operation | |
* @return this object | |
*/ | |
withOperation(operation: string) : GraphQLPactV3 { | |
this.operation = operation; | |
return this; | |
} | |
/** | |
* Add variables used in the Query. | |
* @param variables {GraphQLVariables} | |
* @return this object | |
*/ | |
withVariables(variables: GraphQLVariables) : GraphQLPactV3 { | |
this.variables = variables; | |
return this; | |
} | |
/** | |
* The actual GraphQL query as a string. | |
* | |
* NOTE: spaces are not important, Pact will auto-generate a space-insensitive matcher | |
* | |
* e.g. the value for the "query" field in the GraphQL HTTP payload: | |
* '{ "query": "{ | |
* Category(id:7) { | |
* id, | |
* name, | |
* subcategories { | |
* id, | |
* name | |
* } | |
* } | |
* }" | |
* }' | |
* @param query {string|ASTNode} parsed or unparsed query | |
* @return this object | |
*/ | |
withQuery(query: string | ASTNode) : GraphQLPactV3 { | |
this.query = validateQuery(query, OperationType.Query); | |
return this; | |
} | |
/** | |
* The actual GraphQL mutation as a string or parse tree. | |
* | |
* NOTE: spaces are not important, Pact will auto-generate a space-insensitive matcher | |
* | |
* e.g. the value for the "query" field in the GraphQL HTTP payload: | |
* | |
* mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) { | |
* createReview(episode: $ep, review: $review) { | |
* stars | |
* commentary | |
* } | |
* } | |
* @param mutation {string|ASTNode} parsed or unparsed mutation | |
* @return this object | |
*/ | |
withMutation(mutation: string | ASTNode) : GraphQLPactV3 { | |
this.query = validateQuery(mutation, OperationType.Mutation); | |
return this; | |
} | |
/** | |
* Used to pass in the method, path and content-type; the body detail would | |
* not typically be passed here as that will be internally constructed from | |
* withQuery/withMutation/withVariables calls. | |
* | |
* @see {@link withQuery} | |
* @see {@link withMutation} | |
* @see {@link withVariables} | |
* @param req {V3Request} request | |
* @return this object | |
*/ | |
withRequest(req: V3Request) : GraphQLPactV3 { | |
// Just take what we need from the request, as most of the detail will | |
// come from withQuery/withMutation/withVariables | |
this.req = req; | |
return this; | |
} | |
/** | |
* Overridden as this is the "trigger point" by which we should have received all | |
* request information. | |
* @param res {V3Response} the expected response | |
* @returns this object | |
*/ | |
willRespondWith(res: V3Response) : GraphQLPactV3 { | |
if (!this.query) { | |
throw new ConfigurationError('You must provide a GraphQL query.'); | |
} | |
if (!this.req) { | |
throw new ConfigurationError('You must provide a GraphQL request.'); | |
} | |
this.req.method ||= 'POST'; | |
this.req.contentType ||= 'application/json'; | |
this.req.body = <MatchersV3.AnyTemplate>{ | |
operationName: this.operation, | |
query: this.query, | |
variables: this.variables, | |
}; | |
super.withRequest(this.req); | |
super.willRespondWith(res); | |
return this; | |
} | |
} | |
/** | |
* Accepts a raw or pre-parsed query, validating in the former case, and | |
* returns a normalized raw query. | |
* @param query {string|ASTNode} the query to validate | |
* @param type the operation type | |
*/ | |
function validateQuery(query: string | ASTNode, type: OperationType) : string { | |
if (!query) { | |
throw new ConfigurationError(`You must provide a GraphQL ${type}.`); | |
} | |
if (typeof query !== 'string') { | |
if (query?.kind === 'Document') { | |
// Already parsed, store in string form | |
return print(query); | |
} else { | |
throw new ConfigurationError('You must provide a either a string or parsed GraphQL.'); | |
} | |
} else { | |
// String, so validate it | |
try { | |
gql(query); | |
} catch (e:any) { | |
throw new GraphQLQueryError(`GraphQL ${type} is invalid: ${e.message}`); | |
} | |
return query; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment