Skip to content
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

chore: inline graphql-anywhere #301

Merged
merged 10 commits into from
Oct 26, 2022
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ An Apollo Link to easily try out GraphQL without a full server. It can be used t
## Installation

```bash
npm install apollo-link-rest apollo-link graphql graphql-anywhere qs --save
npm install apollo-link-rest apollo-link graphql qs --save
or
yarn add apollo-link-rest apollo-link graphql graphql-anywhere qs
yarn add apollo-link-rest apollo-link graphql qs
```

`apollo-link`, `graphql`, `qs` and `graphql-anywhere` are peer dependencies needed by `apollo-link-rest`.
`apollo-link`, `graphql` and `qs` are peer dependencies needed by `apollo-link-rest`.
alessbell marked this conversation as resolved.
Show resolved Hide resolved

## Usage

Expand Down
2 changes: 1 addition & 1 deletion docs/rest.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ npm install --save apollo-cache-inmemory
Then it is time to install our link and its `peerDependencies`:

```bash
npm install --save apollo-link-rest apollo-link graphql graphql-anywhere qs
npm install --save apollo-link-rest apollo-link graphql qs
```

After this, you are ready to setup your apollo client:
Expand Down
1 change: 0 additions & 1 deletion examples/advanced/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"apollo-client": "2.x",
"apollo-link-rest": "0.x",
"graphql": "0.x",
"graphql-anywhere": "4.x",
"graphql-tag": "2.x",
"qs": "^6.6.0",
"react": "16.x",
Expand Down
1 change: 0 additions & 1 deletion examples/simple/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"apollo-client": "2.x",
"apollo-link-rest": "0.x",
"graphql": "0.x",
"graphql-anywhere": "4.x",
"graphql-tag": "2.x",
"qs": "^6.6.0",
"react": "16.x",
Expand Down
1 change: 0 additions & 1 deletion examples/typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
"apollo-link": "1.x",
"apollo-link-rest": "0.x",
"graphql": "0.x",
"graphql-anywhere": "4.x",
"graphql-tag": "2.x",
"qs": "^6.6.0",
"react": "16.x",
Expand Down
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
},
"homepage": "https://github.com/apollographql/apollo-link-rest#readme",
"scripts": {
"build:browser": "browserify ./lib/bundle.umd.js -o=./lib/bundle.js --i @apollo/client/core --i @apollo/client/utilities --i apollo-utilities --i graphql --i react && npm run minify:browser",
"build:browser": "browserify ./lib/bundle.umd.js -o=./lib/bundle.js --i @apollo/client/core --i @apollo/client/utilities --i graphql --i react && npm run minify:browser",
"build": "tsc -p .",
"bundle": "rollup -c",
"clean": "rimraf lib/* coverage/* npm/*",
Expand Down Expand Up @@ -45,7 +45,6 @@
"peerDependencies": {
"@apollo/client": ">=3",
"graphql": ">=0.11",
"graphql-anywhere": ">=4",
"qs": ">=6"
},
"devDependencies": {
Expand All @@ -63,7 +62,6 @@
"danger": "6.x",
"fetch-mock": "7.x",
"graphql": "14.x",
"graphql-anywhere": "4.1.x",
"isomorphic-fetch": "2.2.x",
"jest": "23.x",
"jest-fetch-mock": "2.x",
Expand Down
2 changes: 0 additions & 2 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ const globals = {
'@apollo/client/core': 'apolloClient.core',
'@apollo/client/utilities': 'apolloClient.utilities',
'@apollo/link-error': 'apolloLink.error',
'graphql-anywhere': 'graphqlAnywhere',
'graphql-anywhere/lib/async': 'graphqlAnywhere.async',
};

export default {
Expand Down
22 changes: 19 additions & 3 deletions src/restLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,27 @@ import {
checkDocument,
removeDirectivesFromDocument,
} from '@apollo/client/utilities';
import { graphql } from './utils/graphql';
import * as qs from 'qs';

import { graphql } from 'graphql-anywhere/lib/async';
import { Resolver, ExecInfo } from 'graphql-anywhere';
export type DirectiveInfo = {
[fieldName: string]: { [argName: string]: any };
};

import * as qs from 'qs';
export type ExecInfo = {
isLeaf: boolean;
resultKey: string;
directives: DirectiveInfo;
field: FieldNode;
};

export type Resolver = (
fieldName: string,
rootValue: any,
args: any,
context: any,
info: ExecInfo,
) => any;

export namespace RestLink {
export type URI = string;
Expand Down
264 changes: 264 additions & 0 deletions src/utils/graphql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
/*
This file is a port of async.ts from the now-deprecated graphql-anywhere
package, which itself was based on the graphql fn from graphql-js.
Original source: https://github.com/apollographql/apollo-client/blob/release-2.x/packages/graphql-anywhere/src/async.ts

Utils that were previously imported from apollo-utilities can now be imported
from @apollo/client/utilities with the remaining types inlined in restLink.ts.
*/

import {
DocumentNode,
SelectionSetNode,
FieldNode,
FragmentDefinitionNode,
InlineFragmentNode,
DirectiveNode,
} from 'graphql';

import {
getMainDefinition,
getFragmentDefinitions,
createFragmentMap,
shouldInclude,
isField,
isInlineFragment,
resultKeyNameFromField,
argumentsObjectFromField,
FragmentMap,
} from '@apollo/client/utilities';

import { DirectiveInfo, ExecInfo, Resolver } from '../restLink';

function getDirectiveInfoFromField(
field: FieldNode,
variables: Object,
): DirectiveInfo {
if (field.directives && field.directives.length) {
const directiveObj: DirectiveInfo = {};
field.directives.forEach((directive: DirectiveNode) => {
directiveObj[directive.name.value] = argumentsObjectFromField(
directive,
variables,
);
});
return directiveObj;
}
return null;
}

type ResultMapper = (
values: { [fieldName: string]: any },
rootValue: any,
) => any;

type FragmentMatcher = (
rootValue: any,
typeCondition: string,
context: any,
) => boolean;

export type ExecContext = {
fragmentMap: FragmentMap;
contextValue: any;
variableValues: VariableMap;
resultMapper: ResultMapper;
resolver: Resolver;
fragmentMatcher: FragmentMatcher;
};

type ExecOptions = {
resultMapper?: ResultMapper;
fragmentMatcher?: FragmentMatcher;
};

const hasOwn = Object.prototype.hasOwnProperty;

function merge(dest, src) {
if (src !== null && typeof src === 'object') {
Object.keys(src).forEach(key => {
const srcVal = src[key];
if (!hasOwn.call(dest, key)) {
dest[key] = srcVal;
} else {
merge(dest[key], srcVal);
}
});
}
}

type VariableMap = { [name: string]: any };

/* Based on graphql function from graphql-js:
*
* graphql(
* schema: GraphQLSchema,
* requestString: string,
* rootValue?: ?any,
* contextValue?: ?any,
* variableValues?: ?{[key: string]: any},
* operationName?: ?string
* ): Promise<GraphQLResult>
*
*/
export function graphql(
resolver: Resolver,
document: DocumentNode,
rootValue?: any,
contextValue?: any,
variableValues?: VariableMap,
execOptions: ExecOptions = {},
): Promise<null | Object> {
const mainDefinition = getMainDefinition(document);

const fragments = getFragmentDefinitions(document);
const fragmentMap = createFragmentMap(fragments);

const resultMapper = execOptions.resultMapper;

// Default matcher always matches all fragments
const fragmentMatcher = execOptions.fragmentMatcher || (() => true);

const execContext: ExecContext = {
fragmentMap,
contextValue,
variableValues,
resultMapper,
resolver,
fragmentMatcher,
};

return executeSelectionSet(
mainDefinition.selectionSet as SelectionSetNode,
rootValue,
execContext,
);
}

async function executeSelectionSet(
selectionSet: SelectionSetNode,
rootValue: any,
execContext: ExecContext,
) {
const { fragmentMap, contextValue, variableValues: variables } = execContext;

const result = {};

const execute = async selection => {
if (!shouldInclude(selection, variables)) {
// Skip this entirely
return;
}

if (isField(selection)) {
const fieldResult = await executeField(
selection as FieldNode,
rootValue,
execContext,
);

const resultFieldKey = resultKeyNameFromField(selection);

if (fieldResult !== undefined) {
if (result[resultFieldKey] === undefined) {
result[resultFieldKey] = fieldResult;
} else {
merge(result[resultFieldKey], fieldResult);
}
}

return;
}

let fragment: InlineFragmentNode | FragmentDefinitionNode;

if (isInlineFragment(selection)) {
fragment = selection as InlineFragmentNode;
} else {
// This is a named fragment
fragment = fragmentMap[selection.name.value] as FragmentDefinitionNode;

if (!fragment) {
throw new Error(`No fragment named ${selection.name.value}`);
}
}

const typeCondition = fragment.typeCondition.name.value;

if (execContext.fragmentMatcher(rootValue, typeCondition, contextValue)) {
const fragmentResult = await executeSelectionSet(
fragment.selectionSet,
rootValue,
execContext,
);

merge(result, fragmentResult);
}
};

await Promise.all(selectionSet.selections.map(execute));

if (execContext.resultMapper) {
return execContext.resultMapper(result, rootValue);
}

return result;
}

async function executeField(
field: FieldNode,
rootValue: any,
execContext: ExecContext,
): Promise<null | Object> {
const { variableValues: variables, contextValue, resolver } = execContext;

const fieldName = field.name.value;
const args = argumentsObjectFromField(field, variables);

const info: ExecInfo = {
isLeaf: !field.selectionSet,
resultKey: resultKeyNameFromField(field),
directives: getDirectiveInfoFromField(field, variables),
field,
};

const result = await resolver(fieldName, rootValue, args, contextValue, info);

// Handle all scalar types here
if (!field.selectionSet) {
return result;
}

// From here down, the field has a selection set, which means it's trying to
// query a GraphQLObjectType
if (result == null) {
// Basically any field in a GraphQL response can be null, or missing
return result;
}

if (Array.isArray(result)) {
return executeSubSelectedArray(field, result, execContext);
}

// Returned value is an object, and the query has a sub-selection. Recurse.
return executeSelectionSet(field.selectionSet, result, execContext);
}

function executeSubSelectedArray(field, result, execContext) {
return Promise.all(
result.map(item => {
// null value in array
if (item === null) {
return null;
}

// This is a nested array, recurse
if (Array.isArray(item)) {
return executeSubSelectedArray(field, item, execContext);
}

// This is an object, run the selection set on it
return executeSelectionSet(field.selectionSet, item, execContext);
}),
);
}