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

http-fetcher with basic tests #5

Merged
merged 51 commits into from Jul 18, 2017
Merged

http-fetcher with basic tests #5

merged 51 commits into from Jul 18, 2017

Conversation

evans
Copy link
Contributor

@evans evans commented Jun 15, 2017

This includes the most basic fetcher, http-fetcher with a couple of tests. There are a couple constructs that illustrate possible additions to the specification and a bunch of notes for additions that need to be made to make this http-fetcher a canonical example.

@evans evans requested a review from helfer June 15, 2017 07:01
Copy link
Contributor

@helfer helfer left a comment

Choose a reason for hiding this comment

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

This looks great @evanshauser! I left quite a few comments, but most of them are minor. 🙂
Before we merge this in, we should also add at least one test with a mutation, which should be sent via POST. I'd also say that it should actually be configurable whether queries are sent over GET, but let's leave that for the future (maybe just leave a comment in the code about that). The reason for not wanting to send over GET sometimes is because many servers don't allow query strings to have more than 2000 characters or so.

@@ -32,16 +32,21 @@
"clean": "npm run clean:dist && npm run clean:coverage",
"clean:dist": "rimraf dist/*",
"clean:coverage": "rimraf coverage/*",
"prepublish": "npm run clean && npm run build"
"prepublishOnly": "npm run clean && npm run build"
Copy link
Contributor

Choose a reason for hiding this comment

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

What does this do? Is this a new npm thing?

@@ -0,0 +1,175 @@
import { ApolloFetcher, Observable, FetchResult, Subscriber, Operation, State } from './types';
Copy link
Contributor

Choose a reason for hiding this comment

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

We usually break these over multiple lines if there's more than 2 or so.

import { ApolloFetcher, Observable, FetchResult, Subscriber, Operation, State } from './types';
import { toSubscriber } from './subscriber';
import { print } from 'graphql';
import { DocumentNode, DefinitionNode, OperationDefinitionNode } from 'graphql/language/ast';
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here, multiple lines.


public start() {
if (this.state !== State.COLD) {
throw Error('Observer already started');
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't know if we should really throw here. It might be better to just do nothing.

}

public stop() {
if (this.state === State.COMPLETED || this.state === State.ERRORED) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Again, I'd just ignore it if we already stopped. That will make the operation idempotent which is easier to reason about.

import { Subscriber } from './types';

function isSubscriber<T>(object: any): object is Subscriber<T> {
return 'next' in object;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is next always required for a subscriber? Or could it just have error? Maybe the safer thing to check would be to see if it's a function and if it is, treat it as the next function.

src/types.ts Outdated
}

export interface Operation {
query: DocumentNode;
Copy link
Contributor

Choose a reason for hiding this comment

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

This is good for now, but I think eventually we'll also allow sending just the operation name (for persisted queries). Speaking of persisted queries, that's a use-case we should add to our list!!!

src/types.ts Outdated
context?: object;
}

export type UnsubscribeHandler = () => void;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is now different in the TC39 proposal. Do we want to try to align here?

tests/index.ts Outdated
observable.subscribe({
next,
error: (error) => assert(false),
complete: () => { assert(next.calledOnce); done(); },
Copy link
Contributor

Choose a reason for hiding this comment

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

Have you tried if this test can fail at all?

tests/index.ts Outdated
const next = sinon.spy();
const data = {hello: 'world'};
fetchMock.get('*', data);
const fetcher = new HttpFetcher('', fetch);
Copy link
Contributor

Choose a reason for hiding this comment

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

We should make the fetch argument optional. We'll add a separate test for adding the argument later, which can use sinon.spy to check that the function is getting called.

@apollographql apollographql deleted a comment from coveralls Jun 21, 2017
src/link.ts Outdated
}

private static buildLinkChain(links: ApolloLink[]): NextLink {
const _links = [...links];
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This way of creating the connections between the links makes a new array for each request. It mirrors the middleware and afterware code from the netowrk interface of apollo-client(also in apollo-fetch) the next function on demand. We could create a closure and keep an index that we increment

Copy link
Contributor

@jbaxleyiii jbaxleyiii left a comment

Choose a reason for hiding this comment

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

@evanshauser I think we are missing some clarity around how forward works / what the user has to return. Lets chat more about this

super();
}

public request(operation: Operation, forward: NextLink): Observable<FetchResult> {
Copy link
Contributor

Choose a reason for hiding this comment

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

@evanshauser I'm concerned about the current forward pattern since it could be optional? I'm a little confused at forward and how we even do things like enforcing passing a forward into a forward?

src/link.ts Outdated
if (operation.variables === undefined) {
operation.variables = {};
}
if (operation.query === undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

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

what is this for?

src/link.ts Outdated
}
const _operation = transformOperation(operation);

return link.request(_operation) || Observable.of();
Copy link
Contributor

Choose a reason for hiding this comment

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

should there be a forward passed to this request?

Copy link
Contributor

@stubailo stubailo left a comment

Choose a reason for hiding this comment

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

A few general concerns:

  1. A good number of unused files - we can move them to some folder, or remove them entirely
  2. We aren't consistently using the utilities we define, like toLink. We should make sure to use them everywhere.

src/cacheLink.ts Outdated
@@ -0,0 +1,44 @@
import {
Copy link
Contributor

Choose a reason for hiding this comment

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

This file is unused - can we move it into the tests, or an unused folder, or remove it?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd say lets remove it for now as to not confuse early adopters with a potential spec for caching

}

public request(operation: Operation): Observable<FetchResult> | null {
this.headers = operation.context && operation.context.headers || {};
Copy link
Contributor

Choose a reason for hiding this comment

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

For every link, we should make sure to document what it expects on the context.

Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed, until we settle on a formal contextTypes kind of API (and on improving the types), I think we could just note it above the class definition as inline docs?

src/httpUtils.ts Outdated
@@ -0,0 +1,46 @@
import {
Copy link
Contributor

Choose a reason for hiding this comment

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

This file is not used. Let's remove it?

src/link.ts Outdated
operation.variables = {};
}
if (!operation.query) {
console.warn(`query should either be a sting or GraphQL AST`);
Copy link
Contributor

Choose a reason for hiding this comment

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

sting -> string

src/link.ts Outdated
operation.query = <DocumentNode>{};
}

const _operation: Operation = transformOperation(operation);
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe transformedOperation? _operation isn't a super descriptive name.

Copy link
Contributor

Choose a reason for hiding this comment

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

lets just move transformOperation into the request call:

return link.request(transformOperation(operation)) || Observable.of(); 

@@ -0,0 +1,41 @@
import {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we move toLink, isTerminating, etc, here?

Copy link
Contributor

Choose a reason for hiding this comment

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

yes please!

src/mockLink.ts Outdated
@@ -0,0 +1,22 @@
import {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is not used


constructor(pollInterval?: number) {
super();
this.pollInterval = pollInterval || 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd say we should just throw if the poll interval is not set. 0 doesn't seem like a reasonable default.


const poll = (() => {
forward(operation).subscribe(subscriber);
}).bind(this);
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think the bind here is necessary.

src/retryLink.ts Outdated
observer.complete();
},
// This causes an error, not sure why
// complete: observer.complete
Copy link
Contributor

Choose a reason for hiding this comment

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

You probably need observer.complete.bind(observer).


constructor(pollInterval?: number) {
super();
this.pollInterval = pollInterval || 0;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

should be interval

Copy link
Contributor

Choose a reason for hiding this comment

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

@evanshauser are you open to it being a function that given the operation, can determine the interval?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good!

if (!operation.context) {
operation.context = {};
}
operation.context = {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This should be a recursive merge, since a link could overwrite an entire object, such as headers

Copy link
Contributor

Choose a reason for hiding this comment

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

fwiw, recursive merges are really expensive :( I would say that this link overwrites and previous context of the same keys when being added.

@evanshauser when we call request within the ApolloLink interface, thoughts on creating a copy of the operation and passing it down?

Copy link
Contributor

@jbaxleyiii jbaxleyiii left a comment

Choose a reason for hiding this comment

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

@evanshauser this is getting close! A few changes / question in here.

I would also really like to see some inline documentation above methods on the Link abstract. That way when we start iterating on it, we can make sure we stick / understand the original decisions!

Great stuff!

src/cacheLink.ts Outdated
@@ -0,0 +1,44 @@
import {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd say lets remove it for now as to not confuse early adopters with a potential spec for caching

}

public request(operation: Operation): Observable<FetchResult> | null {
this.headers = operation.context && operation.context.headers || {};
Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed, until we settle on a formal contextTypes kind of API (and on improving the types), I think we could just note it above the class definition as inline docs?

return ApolloLink.empty();
}

return links
Copy link
Contributor

Choose a reason for hiding this comment

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

@evanshauser this is nitpicky (and totally not necessary to change), but with reduce you can specify the starting point.

i.e.

links.map(toLink).reduce((x, y) => x.concat(y)), ApolloLink.empty())

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jbaxleyiii The issue I ran into was that reduce is applied, so that the initial argument is the first in the chain, so the function calls became:

ApolloLink.empty().concat(first).concat(second)... 

We could do an ApolloLink.passthrough() as the initial value

Copy link
Contributor

Choose a reason for hiding this comment

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

ah shoot, yep! thank you! Lets keep it as is

src/link.ts Outdated
// join two Links together
public concat(link: ApolloLink | RequestHandler): ApolloLink {
if (this.request.length <= 1) {
const warning = Object.assign(
Copy link
Contributor

Choose a reason for hiding this comment

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

should we also use isTerminating(this) to be consistent in checking?

src/link.ts Outdated
}
link = ApolloLink.toLink(link);

return link.request.length <= 1 ? new TerminatedConcat(this, link) : new ConcatLink(this, link);
Copy link
Contributor

Choose a reason for hiding this comment

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

Same question here about using the method instead of rewriting the check

error: (error) => assert(false),
complete: () => {
const body = JSON.parse(fetchMock.lastCall()[1]['body']);
assert.equal(body['query'], print(sampleMutation));
Copy link
Contributor

Choose a reason for hiding this comment

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

any reason for not using dot notation here?

describe('concat', () => {
it('should concat a function', (done) => {
const returnOne = new SetContextLink({ add: 1 });
const link = returnOne.concat((operation, forward) => Observable.of({ data: operation.context.add }));
Copy link
Contributor

Choose a reason for hiding this comment

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

@evanshauser this shows my potential fear with forward :(

I'm assuming the library would not see this as a terminating link, yet forward is not used so it in essence is terminating. I think with copy paste / boilerplate, this could really get people confused

return forward(operation);
});
const mock2 = new MockLink((op, forward) => Observable.of({ data: op.context.add + 2 }));
const mock3 = new MockLink((op, forward) => Observable.of({ data: op.context.add + 3 }));
Copy link
Contributor

Choose a reason for hiding this comment

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

lets move MockLink to the /tests folder so it won't be confused as an external link

import SingleRequestLink from '../src/singleRequestLink';


describe('SingleRequestLink', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this is needed since the link won't be shipped. If it is, this needs a lot more tests

@@ -0,0 +1,7 @@
import './httpLink';
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this file needed? Can you not glob test?

return ApolloLink.empty();
}

return links
Copy link
Contributor

Choose a reason for hiding this comment

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

ah shoot, yep! thank you! Lets keep it as is

@jbaxleyiii jbaxleyiii merged commit 2cd697a into master Jul 18, 2017
@jbaxleyiii jbaxleyiii deleted the http-fetcher branch July 18, 2017 19:49
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants