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

Proposal to change the API #4

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
116 changes: 38 additions & 78 deletions src/GraphQLDataSource.ts
Original file line number Diff line number Diff line change
@@ -1,107 +1,67 @@
import { DataSourceConfig } from 'apollo-datasource';
import { DataSource, DataSourceConfig } from 'apollo-datasource';
import { ApolloLink, execute, GraphQLRequest, makePromise } from 'apollo-link';
import { setContext } from 'apollo-link-context';
import { onError } from 'apollo-link-error';
import { createHttpLink } from 'apollo-link-http';
import { ApolloError, AuthenticationError, ForbiddenError } from 'apollo-server-errors';
import to from 'await-to-js';
import { DocumentNode } from 'graphql';
import fetch from 'isomorphic-fetch';
Copy link
Author

Choose a reason for hiding this comment

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

Oh, and I've made this change because people may want to bring their own fetch() (e.g., if you're using this in a serverless or Cloudflare-worker environment, you will already have a fetch method available.


export class GraphQLDataSource<TContext = any> {
public baseURL!: string;
type ValueOrPromise<T> = T | Promise<T>;
type RequestOptions = Record<string, any>;

export class GraphQLDataSource<TContext = any> extends DataSource {
Copy link
Author

Choose a reason for hiding this comment

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

extending DataSource for inheritance and compatibility

public context!: TContext;
public link!: ApolloLink;

public initialize(config: DataSourceConfig<TContext>): void {
initialize(config: DataSourceConfig<TContext>): void {
this.context = config.context;
this.link = ApolloLink.from([
this.onRequestLink(),
this.link
]);
}

public async mutation(mutation: DocumentNode, options: GraphQLRequest) {
// GraphQL request requires the DocumentNode property to be named query
return this.executeSingleOperation({ ...options, query: mutation });
return this.execute({ ...options, query: mutation });
Copy link
Author

Choose a reason for hiding this comment

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

taking request from Apollo Client's code

}

public async query(query: DocumentNode, options: GraphQLRequest) {
return this.executeSingleOperation({ ...options, query });
}

protected willSendRequest?(request: any): any;

private composeLinks(): ApolloLink {
Copy link
Author

Choose a reason for hiding this comment

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

no need to do this every request, as the ApolloLink doesn't persist any state, so we can do it once per construction, rather than every request.

const uri = this.resolveUri();

return ApolloLink.from([
this.onErrorLink(),
this.onRequestLink(),
createHttpLink({ fetch, uri }),
]);
}

private didEncounterError(error: any) {
const status = error.statusCode ? error.statusCode : null;
const message = error.bodyText ? error.bodyText : null;

let apolloError: ApolloError;

switch (status) {
case 401:
apolloError = new AuthenticationError(message);
break;
case 403:
apolloError = new ForbiddenError(message);
break;
default:
apolloError = new ApolloError(message);
}

throw apolloError;
return this.execute({ ...options, query });
}

private async executeSingleOperation(operation: GraphQLRequest) {
const link = this.composeLinks();

const [error, response] = await to(makePromise(execute(link, operation)));

if (error) {
this.didEncounterError(error);
}

return response;
}

private resolveUri(): string {

Choose a reason for hiding this comment

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

const baseURL = this.baseURL;
protected willSendRequest?(request: RequestOptions): ValueOrPromise<void>;
Copy link
Author

Choose a reason for hiding this comment

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

I think these are the correct Type signatures.


private async execute(operation: GraphQLRequest) {
Copy link
Author

Choose a reason for hiding this comment

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

We probably need an operation build, ala, Apollo Client, where for example we can auto-set a default operationNamehttps://github.com/apollographql/apollo-client/blob/master/packages/apollo-client/src/core/QueryManager.ts#L1255-L1284

      operationName: getOperationName(document) || undefined,

try {
return await makePromise(execute(this.link, operation));
Copy link
Author

Choose a reason for hiding this comment

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

Ideally once we go towards caching, we could do something like what Apollo Client does here: https://github.com/apollographql/apollo-client/blob/master/packages/apollo-client/src/core/QueryManager.ts#L265-L307

} catch (error) {
Copy link
Author

Choose a reason for hiding this comment

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

using try/catch instead of await-to-js lightens the dependencies and keeps the code still readable

const status = error.statusCode ? error.statusCode : null;
const message = error.bodyText ? error.bodyText : null;

let apolloError: ApolloError;

switch (status) {
case 401:
apolloError = new AuthenticationError(message);
break;
case 403:
apolloError = new ForbiddenError(message);
break;
default:
apolloError = new ApolloError(message);
}

if (!baseURL) {
throw new ApolloError('Cannot make request to GraphQL API, missing baseURL');
throw apolloError;
}

return baseURL;
}

private onRequestLink() {
return setContext((_, request) => {
// QUESTION: Is that first argument needed? Code for apollo mentions it's actually `request, prevContext`
return setContext(async (_, request: RequestOptions) => {
if (this.willSendRequest) {
this.willSendRequest(request);
await this.willSendRequest(request);
Copy link
Author

Choose a reason for hiding this comment

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

async this.willSendRequest to be inline with the protocol/types of DataSource and RESTDataSource

}

return request;
});
}

private onErrorLink() {
Copy link
Author

Choose a reason for hiding this comment

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

This should probably be handled by the user, as each user is like to have different error reporting requirements.

return onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.map(graphqlError =>
console.error(
`[GraphQL error]: ${graphqlError}`,
),
);
}

if (networkError) {
console.log(`[Network Error]: ${networkError}`);
}
});
}
}