-
Couldn't load subscription status.
- Fork 16
New Generic Field Rewriter & Custom Rewriter #51
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
base: master
Are you sure you want to change the base?
Changes from 16 commits
dfb15cf
b73c68a
6717867
d924691
f67ca80
ac91ec4
b05e39c
9da7701
5a2b3d3
5d1e8fc
843d057
8ebdb48
54e7491
40eda8b
a267589
b7073f0
d021875
0b67967
7873cfa
ccbb642
a752523
0d313a5
8ce15ad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 { | ||
AndresPrez marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| import { ArgumentNode, ASTNode, FieldNode, Kind, SelectionSetNode } from 'graphql'; | ||
| import { astArgVarNode, NodeAndVarDefs } from '../ast'; | ||
| import Rewriter, { RewriterOpts, Variables } from './Rewriter'; | ||
|
|
||
| interface FieldRewriterOpts extends RewriterOpts { | ||
| newFieldName?: string; | ||
| arguments?: string[]; | ||
| objectFieldName?: string; | ||
| } | ||
|
|
||
| /** | ||
| * More generic version of ScalarFieldToObjectField rewriter | ||
| */ | ||
| class FieldRewriter extends Rewriter { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks like it's doing the same thing as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, in addition to what the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's already There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, adding arguments to a Field that has no initial arguments is missing, also argument value coercion I think is also lacking. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right FieldArgTypeRewriter has argument coercion. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. aah interesting I see what you mean about maybe needing to add an argument to make an old query valid in a new schema. That is a use-case that none of the current rewriters handle currently. Philosophically this library has favored small targeted rewriters for each use-case rather than 1 big one that does most things, but maybe this is a good change actually. For adding arguments, what do you think about making it a map instead of an array? ex There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes totally, that sounds even better |
||
| protected newFieldName?: string; | ||
| protected arguments?: string[]; | ||
| protected objectFieldName?: string; | ||
|
|
||
| constructor(options: FieldRewriterOpts) { | ||
| super(options); | ||
| this.newFieldName = options.newFieldName; | ||
| this.arguments = options.arguments; | ||
| this.objectFieldName = options.objectFieldName; | ||
| } | ||
|
|
||
| public matches(nodeAndVars: NodeAndVarDefs, parents: ASTNode[]): boolean { | ||
| if (!super.matches(nodeAndVars, parents)) return false; | ||
| const node = nodeAndVars.node as FieldNode; | ||
| // if there's the intention of converting the field to a subselection | ||
| // make sure there's no subselections on this field | ||
| if (node.selectionSet && !!this.objectFieldName) return false; | ||
| return true; | ||
| } | ||
|
|
||
| public rewriteQuery(nodeAndVarDefs: NodeAndVarDefs, variables: Variables) { | ||
| const node = nodeAndVarDefs.node as FieldNode; | ||
| const { variableDefinitions } = nodeAndVarDefs; | ||
| // if there's the intention of converting the field to a subselection | ||
| // and there's a subselection already, just return | ||
| if (node.selectionSet && !!this.objectFieldName) return nodeAndVarDefs; | ||
|
|
||
| // if fieldName is meant to be renamed. | ||
| if (this.newFieldName) { | ||
| let newName = this.newFieldName; | ||
| if (this.newFieldName.includes(':')) { | ||
| const [alias, name] = this.newFieldName.split(':'); | ||
| newName = name.trim(); | ||
| Object.assign(node, { alias: { value: alias.trim(), kind: Kind.NAME } }); | ||
| } | ||
| Object.assign(node.name, { value: newName }); | ||
| } | ||
|
|
||
| // if there's the intention of converting the field to a subselection | ||
| // of objectFieldNames assign SelectionSetNode to the field accordingly. | ||
| if (this.objectFieldName) { | ||
| const selectionSet: SelectionSetNode = { | ||
| kind: 'SelectionSet', | ||
| selections: [ | ||
| { | ||
| kind: 'Field', | ||
| name: { kind: 'Name', value: this.objectFieldName } | ||
| } | ||
| ] | ||
| }; | ||
| Object.assign(node, { selectionSet }); | ||
| } | ||
|
|
||
| // If, 1) the field is a SelectionSet, | ||
| // 2) this.arguments is not empty nor undefined, and | ||
| // 3) query comes with variables, then assign ArgumentNodes to the field accordingly. | ||
| if (node.selectionSet && !!this.arguments && variables) { | ||
| // field may already come with some arguments | ||
| const newArguments: ArgumentNode[] = [...(node.arguments || [])]; | ||
| this.arguments.forEach(argName => { | ||
| if ( | ||
| this.isArgumentInVariables(argName, variables) && | ||
| !this.isArgumentInArguments(argName, newArguments) | ||
| ) { | ||
| newArguments.push(astArgVarNode(argName)); | ||
| } | ||
| }); | ||
| if (!!newArguments) Object.assign(node, { arguments: newArguments }); | ||
| } | ||
|
|
||
| return { | ||
| variableDefinitions, | ||
| node | ||
| } as NodeAndVarDefs; | ||
| } | ||
|
|
||
| public rewriteResponse(response: any, key: string, index?: number) { | ||
| // Extract the element we are working on | ||
| const element = super.extractReponseElement(response, key, index); | ||
| if (element === null) return response; | ||
|
|
||
| let originalKey = key; | ||
| // if the key is found to be the renamed field | ||
| // then change the name of such field in the response | ||
| // and pass the new key (field name) down. | ||
| if (this.newFieldName) { | ||
| let newFieldName = this.newFieldName; | ||
| // the newFieldName may be alised. | ||
| if (this.newFieldName.includes(':')) { | ||
| const [alias] = this.newFieldName.split(':'); | ||
| newFieldName = alias.trim(); | ||
| } | ||
| if (key === newFieldName) { | ||
| if (this.fieldName) { | ||
| originalKey = this.fieldName; | ||
| Object.assign(response, { [originalKey]: response[key] }); | ||
| delete response[key]; | ||
| } | ||
| } | ||
| } | ||
| // Undo the nesting in the response so it matches the original query | ||
| let newElement = element; | ||
| if (this.objectFieldName) { | ||
| newElement = element[this.objectFieldName]; | ||
| } | ||
| return super.rewriteResponseElement(response, newElement, originalKey, index); | ||
| } | ||
|
|
||
| private isArgumentInArguments(argName: string, argumentNodes: ArgumentNode[]) { | ||
| return argumentNodes.map(argNode => argNode.name.value).includes(argName); | ||
| } | ||
|
|
||
| private isArgumentInVariables(argName: string, variables: Variables): boolean { | ||
| if (variables && Object.keys(variables).includes(argName)) return true; | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| export default FieldRewriter; | ||
Uh oh!
There was an error while loading. Please reload this page.