Skip to content

Instantly share code, notes, and snippets.

@wabrit
Created June 24, 2022 14:06
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wabrit/2d1e1f9520aa133908f0a3716338e5ff to your computer and use it in GitHub Desktop.
Save wabrit/2d1e1f9520aa133908f0a3716338e5ff to your computer and use it in GitHub Desktop.
POC for GraphQL DSL for Pact-JS V3
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