Skip to content
This repository has been archived by the owner on Mar 20, 2023. It is now read-only.

feat(server fragments): server predefined fragments #576

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
"private": true,
"main": "index.js",
"types": "index.d.ts",
"homepage": "https://github.com/graphql/express-graphql",
"homepage": "https://github.com/Ariel-Dayan/express-graphql.git",
"bugs": {
"url": "https://github.com/graphql/express-graphql/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/graphql/express-graphql.git"
"url": "https://github.com/Ariel-Dayan/express-graphql.git"
},
"keywords": [
"express",
Expand Down
93 changes: 92 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ export type OptionsData = {|
* `__typename` field or alternatively calls the `isTypeOf` method).
*/
typeResolver?: ?GraphQLTypeResolver<mixed, mixed>,

/**
* A optional string which will used to predefined fragments
*/
serverFragments?: ?string,
|};

/**
Expand Down Expand Up @@ -238,6 +243,8 @@ function graphqlHTTP(options: Options): Middleware {
const typeResolver = optionsData.typeResolver;
const validationRules = optionsData.validationRules || [];
const graphiql = optionsData.graphiql;
const serverFragments = optionsData.serverFragments;

context = optionsData.context || request;

// GraphQL HTTP only supports GET and POST methods.
Expand All @@ -247,7 +254,7 @@ function graphqlHTTP(options: Options): Middleware {
}

// Get GraphQL params from the request and POST body data.
query = params.query;
query = serverFragments ? addServerFragments(serverFragments, params.query) : params.query;
variables = params.variables;
operationName = params.operationName;
showGraphiQL = canDisplayGraphiQL(request, params) && graphiql;
Expand Down Expand Up @@ -509,3 +516,87 @@ function sendResponse(response: $Response, type: string, data: string): void {
response.setHeader('Content-Length', String(chunk.length));
response.end(chunk);
}

/**
* Helper function to get the first word from string.
*
* @param { string } text - The full string.
*
* @returns { string } - First word.
*/
function sliceFirstWord(text: string): string {
let slicedText = text;

const firstSpaceIndex = slicedText.indexOf(' ');

if(firstSpaceIndex !== -1) {
slicedText = slicedText.slice(0, firstSpaceIndex);
}

const firstEndRowIndex = slicedText.indexOf('\n');

if(firstEndRowIndex !== -1) {
slicedText = slicedText.slice(0, firstEndRowIndex);
}

return slicedText;
}

/**
* Helper recursive function that finds all the fragments from the server that are used in the current request.
*
* @param { string } serverFragments - Fragments from the server.
* @param { string } query - Query from the request.
* @param { Set<string> } fragmentsInUsed - Set of relevant fragments.
*
* @returns { Set<string> } - relevant fragments for current request.
*/
function findFragments(serverFragments: string, query: string, fragmentsInUsed: Set<string>): Set<string> {
Copy link
Member

Choose a reason for hiding this comment

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

it would be much simpler and more concise to use the graphql visit() function here I think

Copy link
Member

@acao acao Dec 10, 2019

Choose a reason for hiding this comment

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

https://graphql.org/graphql-js/language/#visit just parse the operation string to AST, and visit all the FragmentSpread node types i think it would be for this case.

import { visit, print } from 'graphql'

function (serverFragments: string[], query: string) {
   const operationAST = parse(query)
   const serverFragmentString  = ''
   visit(operationAst, {
     FragmentSpread(node) {
      if(serverFragments.includes(node.name.value)) {
          serverFragmentString += print(node)
       }
     }
   })
   return serverFragmentString 
}

Copy link
Member

Choose a reason for hiding this comment

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

I could be wrong though, about which option is more performant, as parsing each query and then re-printing the nodes entails another expense, though much of this could be memoized, eventually cached even?

// Fragment declaration starts with 'fragment' key word
// Slice to remove text before the first fragment declaration
let fragmentDeclarationFields = serverFragments.split('fragment ').slice(1);

// Fragment variable starts with spread - '...'
// Slice to remove text before the first fragment variable
let fragmentVariableFields = query.split('...').slice(1);

fragmentVariableFields.forEach(fragmentVariable => {
const currFragmentVariableKeyName = sliceFirstWord(fragmentVariable);

for (let index = 0; index < fragmentDeclarationFields.length; index++) {
const currFragmentDeclaration = fragmentDeclarationFields[index];
const currFragmentDeclarationKeyName = sliceFirstWord(currFragmentDeclaration);

if(currFragmentDeclarationKeyName === currFragmentVariableKeyName) {

fragmentsInUsed.add(currFragmentDeclaration);

// Find fragments in the matching fragments
fragmentsInUsed = findFragments(serverFragments, currFragmentDeclaration, fragmentsInUsed)

break;
}

}
})

return fragmentsInUsed;
}

/**
* Add to query the relevant server fragments
*
* @param {*} serverFragments - Fragments from the server.
* @param {*} query - Query from the request.
*
* @returns - Concat relevant fragments to request query
*/
function addServerFragments(serverFragments: string, query: string): string {
let fragmentsInUsed = '';

[...(findFragments(serverFragments, query, new Set()))].forEach(fullFragment => {
fragmentsInUsed += `fragment ${fullFragment} `;
Copy link
Member

Choose a reason for hiding this comment

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

itll probably be easier and more perfomant to generate this from AST as well

})

return `${fragmentsInUsed}\n${query}`;
}
Copy link
Member

@acao acao Dec 10, 2019

Choose a reason for hiding this comment

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

EOF issue

5 changes: 5 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ declare namespace graphqlHTTP {
* `__typename` field or alternatively calls the `isTypeOf` method).
*/
typeResolver?: GraphQLTypeResolver<unknown, unknown> | null;

/**
* A optional string which will used to predefined fragments
*/
serverFragments?: string;
}

/**
Expand Down