-
Notifications
You must be signed in to change notification settings - Fork 18
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
base: master
Are you sure you want to change the base?
Changes from all commits
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 |
---|---|---|
@@ -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'; | ||
|
||
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 { | ||
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. 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 }); | ||
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. taking |
||
} | ||
|
||
public async query(query: DocumentNode, options: GraphQLRequest) { | ||
return this.executeSingleOperation({ ...options, query }); | ||
} | ||
|
||
protected willSendRequest?(request: any): any; | ||
|
||
private composeLinks(): ApolloLink { | ||
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. 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 { | ||
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. please don't remove this per https://www.apollographql.com/docs/apollo-server/v2/features/data-sources.html#Resolving-URLs-dynamically. Should be renamed to |
||
const baseURL = this.baseURL; | ||
protected willSendRequest?(request: RequestOptions): ValueOrPromise<void>; | ||
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. I think these are the correct Type signatures. |
||
|
||
private async execute(operation: GraphQLRequest) { | ||
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. We probably need an operation build, ala, Apollo Client, where for example we can auto-set a default
|
||
try { | ||
return await makePromise(execute(this.link, operation)); | ||
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. 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) { | ||
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. using try/catch instead of |
||
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); | ||
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. async this.willSendRequest to be inline with the protocol/types of DataSource and RESTDataSource |
||
} | ||
|
||
return request; | ||
}); | ||
} | ||
|
||
private onErrorLink() { | ||
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 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}`); | ||
} | ||
}); | ||
} | ||
} |
There was a problem hiding this comment.
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.