Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
dfb15cf
enhances for argtype coercion
AndresPrez May 24, 2022
b73c68a
Adds unit tests for new functionalities
AndresPrez May 24, 2022
6717867
fixes linting issues
AndresPrez May 26, 2022
d924691
Creates a generic FieldRewriter
AndresPrez May 26, 2022
f67ca80
forgot to export new rewriter
AndresPrez May 26, 2022
ac91ec4
Adds ability to add arguments to the field in FieldRewriter
AndresPrez May 26, 2022
b05e39c
fixes issue when renamed object fields returned an array of values
AndresPrez May 26, 2022
9da7701
Adds support for alises. Implements new CustomRewriter
AndresPrez May 27, 2022
5a2b3d3
matchAnyPath moved from rewrite handler to rewrite claass
AndresPrez May 27, 2022
5d1e8fc
adds support to rename fields with alises
AndresPrez May 27, 2022
843d057
Adds matching node to the rewriteResponse fn
AndresPrez May 27, 2022
8ebdb48
Merge branch 'master' into feature/ap/field-rewriter
AndresPrez May 29, 2022
54e7491
Fixes issue with CustomRewriter default matchConditions array
AndresPrez May 30, 2022
40eda8b
Merge branch 'feature/ap/field-rewriter' of github.com:AndresPrez/gra…
AndresPrez May 30, 2022
a267589
Adds rewriteVariables to CustomRewriter
AndresPrez May 31, 2022
b7073f0
fixes reviewed code
AndresPrez Jun 6, 2022
d021875
variable trim and rename
AndresPrez Jun 7, 2022
0b67967
adds fix for when rewritten field results are empty arrays
AndresPrez Jun 19, 2022
7873cfa
v0.0.16
AndresPrez Jun 20, 2022
ccbb642
fixes "rewriteResultsAtPath" for non-field paths
AndresPrez Jun 28, 2022
a752523
v0.0.17
AndresPrez Jun 28, 2022
0d313a5
v0.0.1
AndresPrez Jun 28, 2022
8ce15ad
fixes unintended change to package.json
AndresPrez Jun 28, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 31 additions & 13 deletions src/RewriteHandler.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { FragmentDefinitionNode, parse, print } from 'graphql';
import { ASTNode, FragmentDefinitionNode, parse, print } from 'graphql';
import { extractPath, FragmentTracer, rewriteDoc, rewriteResultsAtPath } from './ast';
import Rewriter, { Variables } from './rewriters/Rewriter';

interface RewriterMatch {
rewriter: Rewriter;
paths: ReadonlyArray<ReadonlyArray<string>>;
// TODO:
// - allPaths hasnt been tested for fragments
// - Give that allPaths includes non-field paths, there might be paths
// that don't match to a key in the results object traversed in
// 'rewriteResultsAtPath'. For now the 'includesNonFieldPaths' flag is passed to
// this function.
allPaths: ReadonlyArray<ReadonlyArray<string>>;
nodeMatchAndParents?: ASTNode[];
}

/**
Expand Down Expand Up @@ -40,17 +48,22 @@ export default class RewriteHandler {
if (isMatch) {
rewrittenVariables = rewriter.rewriteVariables(rewrittenNodeAndVars, rewrittenVariables);
rewrittenNodeAndVars = rewriter.rewriteQuery(rewrittenNodeAndVars, rewrittenVariables);
const simplePath = extractPath([...parents, rewrittenNodeAndVars.node]);
let paths: ReadonlyArray<ReadonlyArray<string>> = [simplePath];
const fieldPath = extractPath([...parents, rewrittenNodeAndVars.node]);
const anyPath = extractPath([...parents, rewrittenNodeAndVars.node], true);
let fieldPaths: ReadonlyArray<ReadonlyArray<string>> = [fieldPath];
let allPaths: ReadonlyArray<ReadonlyArray<string>> = [anyPath];
const fragmentDef = parents.find(({ kind }) => kind === 'FragmentDefinition') as
| FragmentDefinitionNode
| undefined;
if (fragmentDef) {
paths = fragmentTracer.prependFragmentPaths(fragmentDef.name.value, simplePath);
fieldPaths = fragmentTracer.prependFragmentPaths(fragmentDef.name.value, fieldPath);
allPaths = fragmentTracer.prependFragmentPaths(fragmentDef.name.value, anyPath);
}
this.matches.push({
rewriter,
paths
allPaths,
paths: fieldPaths,
nodeMatchAndParents: [...parents, rewrittenNodeAndVars.node]
});
}
return isMatch;
Expand All @@ -70,15 +83,20 @@ export default class RewriteHandler {
if (this.hasProcessedResponse) throw new Error('This handler has already returned a response');
this.hasProcessedResponse = true;
let rewrittenResponse = response;
this.matches.reverse().forEach(({ rewriter, paths }) => {
paths.forEach(path => {
rewrittenResponse = rewriteResultsAtPath(
rewrittenResponse,
path,
(parentResponse, key, index) => rewriter.rewriteResponse(parentResponse, key, index)
);
this.matches
.reverse()
.forEach(({ rewriter, paths: fieldPaths, allPaths, nodeMatchAndParents }) => {
const paths = rewriter.includeNonFieldPathsInMatch ? allPaths : fieldPaths;
paths.forEach(path => {
rewrittenResponse = rewriteResultsAtPath(
rewrittenResponse,
path,
(parentResponse, key, index) =>
rewriter.rewriteResponse(parentResponse, key, index, nodeMatchAndParents),
rewriter.includeNonFieldPathsInMatch
);
});
});
});
return rewrittenResponse;
}
}
79 changes: 70 additions & 9 deletions src/ast.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { ASTNode, DocumentNode, FragmentDefinitionNode, VariableDefinitionNode } from 'graphql';
import {
ArgumentNode,
ASTNode,
DocumentNode,
FragmentDefinitionNode,
Kind,
VariableDefinitionNode
} from 'graphql';
import { pushToArrayAtKey } from './utils';

const ignoreKeys = new Set(['loc']);
Expand Down Expand Up @@ -239,19 +246,51 @@ export const replaceVariableDefinitions = (
};

/**
* return the path that will be returned in the response from from the chain of parents
* Return the path that will be returned in the response from the chain of parents.
* By default this will only build up paths for field nodes, but the anyKind flag allows
* to build paths for any named node.
*
* It also supports aliases.
*/
/** @hidden */
export const extractPath = (parents: ReadonlyArray<ASTNode>): ReadonlyArray<string> => {
export const extractPath = (
parents: ReadonlyArray<ASTNode>,
anyKind?: boolean
): ReadonlyArray<string> => {
const path: string[] = [];
parents.forEach(parent => {
if (parent.kind === 'Field') {
path.push(parent.name.value);
parents.forEach((parent: any) => {
if (parent.kind === 'Field' || anyKind) {
if (parent.alias) {
path.push(parent.alias.value);
} else if (parent.name) {
path.push(parent.name.value);
}
}
});
return path;
};

/**
* return an ArgumentNode with a VariableNode as its value node with matching name.
*/
/** @hidden */
export const astArgVarNode = (argName: string): ArgumentNode => {
return {
kind: Kind.ARGUMENT,
name: {
kind: Kind.NAME,
value: argName
},
value: {
kind: Kind.VARIABLE,
name: {
kind: Kind.NAME,
value: argName
}
}
};
};

/** @hidden */
interface ResultObj {
[key: string]: any;
Expand All @@ -261,14 +300,21 @@ interface ResultObj {
export const rewriteResultsAtPath = (
results: ResultObj,
path: ReadonlyArray<string>,
callback: (parentResult: any, key: string, position?: number) => any
callback: (parentResult: any, key: string, position?: number) => any,
includesNonFieldPaths?: boolean
): ResultObj => {
if (path.length === 0) return results;

const curPathElm = path[0];
const newResults = { ...results };
const curResults = results[curPathElm];

// if results[curPathElm] is an empty array, call the callback response rewriter
// because there's nothing left to do.
if (Array.isArray(curResults) && curResults.length === 0) {
callback(results, curPathElm);
}

if (path.length === 1) {
if (Array.isArray(curResults)) {
return curResults.reduce(
Expand All @@ -281,15 +327,30 @@ export const rewriteResultsAtPath = (
}

const remainingPath = path.slice(1);

// If curResults is undefined, and includesNonFieldPaths is true,
// then curResults is not a field path, so call the callback to allow rewrites
// for non-field paths.
if (remainingPath.length && includesNonFieldPaths && curResults === undefined) {
callback(results, curPathElm);
// Then just continue with the next path
return rewriteResultsAtPath(results, remainingPath, callback, includesNonFieldPaths);
}

// if the path stops here, just return results without any rewriting
if (curResults === undefined || curResults === null) return results;

if (Array.isArray(curResults)) {
newResults[curPathElm] = curResults.map(result =>
rewriteResultsAtPath(result, remainingPath, callback)
rewriteResultsAtPath(result, remainingPath, callback, includesNonFieldPaths)
);
} else {
newResults[curPathElm] = rewriteResultsAtPath(curResults, remainingPath, callback);
newResults[curPathElm] = rewriteResultsAtPath(
curResults,
remainingPath,
callback,
includesNonFieldPaths
);
}

return newResults;
Expand Down
73 changes: 73 additions & 0 deletions src/rewriters/CustomRewriter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { ASTNode } from 'graphql';
import { NodeAndVarDefs } from '../ast';
import Rewriter, { RewriterOpts, Variables } from './Rewriter';

interface CustomRewriterOpts extends RewriterOpts {
matchesFn?: (nodeAndVarDefs: NodeAndVarDefs, parents: ReadonlyArray<ASTNode>) => boolean;
rewriteQueryFn?: (nodeAndVarDefs: NodeAndVarDefs, variables: Variables) => NodeAndVarDefs;
rewriteVariablesFn?: (nodeAndVarDefs: NodeAndVarDefs, variables: Variables) => Variables;
rewriteResponseFn?: (
response: any,
key: string,
index?: number,
nodeMatchAndParents?: ASTNode[]
) => NodeAndVarDefs;
}

/**
* A Custom rewriter with its Rewriter functions received as arguments.
* This Rewriter allows users to write their own rewriter functions.
*/
class CustomRewriter extends Rewriter {
protected matchesFn: (nodeAndVarDefs: NodeAndVarDefs, parents: ReadonlyArray<ASTNode>) => boolean;
protected rewriteQueryFn: (
nodeAndVarDefs: NodeAndVarDefs,
variables: Variables
) => NodeAndVarDefs;
protected rewriteVariablesFn: (nodeAndVarDefs: NodeAndVarDefs, variables: Variables) => Variables;
protected rewriteResponseFn: (
response: any,
key: string,
index?: number,
nodeMatchAndParents?: ASTNode[]
) => NodeAndVarDefs;

constructor(options: CustomRewriterOpts) {
const {
matchesFn,
rewriteQueryFn,
rewriteVariablesFn,
rewriteResponseFn,
matchConditions = [() => true],
...rewriterOpts
} = options;
super({ ...rewriterOpts, matchConditions });
this.matchesFn = matchesFn || super.matches;
this.rewriteQueryFn = rewriteQueryFn || super.rewriteQuery;
this.rewriteVariablesFn = rewriteVariablesFn || super.rewriteVariables;
this.rewriteResponseFn = rewriteResponseFn || super.rewriteResponse;
}

public matches(nodeAndVarDefs: NodeAndVarDefs, parents: ReadonlyArray<ASTNode>): boolean {
return this.matchesFn(nodeAndVarDefs, parents);
}

public rewriteQuery(nodeAndVarDefs: NodeAndVarDefs, variables: Variables) {
return this.rewriteQueryFn(nodeAndVarDefs, variables);
}

public rewriteResponse(
response: any,
key: string,
index?: number,
nodeMatchAndParents?: ASTNode[]
) {
return this.rewriteResponseFn(response, key, index, nodeMatchAndParents);
}

public rewriteVariables(nodeAndVarDefs: NodeAndVarDefs, variables: Variables): Variables {
return this.rewriteVariablesFn(nodeAndVarDefs, variables);
}
}

export default CustomRewriter;
Loading