Skip to content

Commit

Permalink
Merge pull request #31 from mringer/mr-feature-composeApi
Browse files Browse the repository at this point in the history
Compose functionality and tests
  • Loading branch information
mringer authored Jun 23, 2018
2 parents 0cbe329 + 42083af commit 7d49d06
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 5 deletions.
5 changes: 3 additions & 2 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"presets": ["es2015"],
"sourceMaps": true
"presets": ["env"],
"sourceMaps": true,
"plugins": ["transform-object-rest-spread"]
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"babel-cli": "^6.18.0",
"babel-core": "^6.17.0",
"babel-eslint": "^7.0.0",
"babel-preset-es2015": "^6.16.0",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-preset-env": "^1.7.0",
"babel-register": "^6.18.0",
"bluebird": "^3.5.0",
"chai": "^3.5.0",
Expand Down
5 changes: 4 additions & 1 deletion src/helper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { createResolver, ResultFunction, ErrorFunction, Resolver } from "./resolver";
import * as merge from "deepmerge";
import { isFunction } from "./util";

// Helper function to combine multiple resolver definition hashes into a single hash for consumption by Apollostack's graphql-server
export const combineResolvers = (resolvers = []) => resolvers
Expand Down Expand Up @@ -26,4 +28,5 @@ export const or = (...conditions) => resolver => (...query) => {
});
attempt(0);
});
}
}

19 changes: 19 additions & 0 deletions src/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,14 @@ export interface CreateResolverFunction {
<R, E>(resFn: ResultFunction<R>, errFn?: ErrorFunction<E>): Resolver<R>
}

export interface ComposeResolversFunction {
( resolvers: any ): {} // { [name: string]: Resolver<R> | {} }
}

export interface Resolver<ResulType> {
(root, args: {}, context: {}, info: {}): Promise<ResulType>
createResolver?: CreateResolverFunction
compose?: ComposeResolversFunction
}

export const createResolver: CreateResolverFunction = <R, E>(resFn: ResultFunction<R>, errFn: ErrorFunction<E>) => {
Expand All @@ -38,6 +43,7 @@ export const createResolver: CreateResolverFunction = <R, E>(resFn: ResultFuncti
});
});
};

baseResolver.createResolver = (cResFn, cErrFn) => {
const Promise = getPromise();

Expand Down Expand Up @@ -74,5 +80,18 @@ export const createResolver: CreateResolverFunction = <R, E>(resFn: ResultFuncti
return createResolver(childResFn, childErrFn);
}

baseResolver.compose = ( resolvers: {} ) => {
const composed = {};
Object.keys(resolvers).forEach(key => {
const _resolver = resolvers[key];
composed[key] = (_resolver.resolve || _resolver.error)
// supports syntax: compose( { myResolver: { resolve: resFn, error: errFn } } )
? baseResolver.createResolver(_resolver.resolve, _resolver.error)
// supports syntax: compose( { myResolver: resolver } )
: baseResolver.createResolver(_resolver);
});
return composed;
}

return baseResolver;
};
3 changes: 2 additions & 1 deletion test/unit/helper_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { expect } from 'chai';
import { stub } from 'sinon';

import {
combineResolvers, and, or,
combineResolvers, and, or, compose, composable
} from '../../dist/helper';
import { createResolver } from '../../dist/resolver';
import { resolveAll } from 'jspm-config';

describe('(unit) src/helper.js', () => {
describe('combineResolvers', () => {
Expand Down
161 changes: 161 additions & 0 deletions test/unit/resolver_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,4 +197,165 @@ describe('(unit) dist/resolver.js', () => {
childResolver(null, null, null, { info: 'info' })
})
})

describe('Compose resolvers', () => {
const compositionErr = new Error('composition error');
const successResolver = createResolver(() => null, () => null);
const failureResolver = createResolver(() => { throw compositionErr; }, () => null);

it('composed resolvers are chained, and base resolver is called for each', () => {

const b = {
resolve: () => {},
error: d => compositionErr
};

stub(b, 'resolve', b.resolve);

const base = createResolver(b.resolve, b.error);
const comp = base.compose({
r1: () => true,
r2: () => true,
r3: () => true,
});

return Promise.all([

comp.r1().then(r => {
expect(b.resolve.calledThrice).to.be.true;
expect(r).to.be.true;
}),

comp.r2().then(r => {
expect(b.resolve.calledThrice).to.be.true;
expect(r).to.be.true;
}),

comp.r3().then(r => {
expect(r).to.be.true;
expect(b.resolve.calledThrice).to.be.true;
})

]);
});

it('when base throws, child is not called ', () => {

const b = {
resolve: null,
error: d => compositionErr
};

const r1 = {
resolve: () => true,
error: () => compositionErr
};

stub(b, 'error', b.error);
stub(r1, 'error', r1.error);

const base = createResolver(b.resolve, b.error);
const comp = base.compose( { r1: r1 } );

comp.r1()
.catch( e => {
expect(b.error.calledOnce).to.be.true;
expect(r1.resolve.notCalled).to.be.true;
expect(r1.error.notCalled).to.be.true;
expect(e).to.equal(compositionErr);
});
});

it('when child throws, parent error is called ', () => {
const b = {
resolve: null,
error: d => null
};

const r1 = {
resolve: () => true,
error: () => compositionErr
};

stub(b, 'error', b.error);
stub(r1, 'error', r1.error);

const base = createResolver(b.resolve, b.error);
const comp = base.compose( { r1: r1 } );

comp.r1()
.catch( e => {
expect(b.error.calledOnce).to.be.true;
expect(r1.error.calledOnce).to.be.true;
expect(e).to.equal(compositionErr);
});
});

it('composed resolvers with { resolve: resFn, error: resFn } syntax, resolve and bubble errors correctly', () => {

const b = {
resolve: () => {},
error: d => compositionErr
};

const r1 = {
resolve: () => { throw Error('some other error') },
error: () => compositionErr };

const r2 = { resolve: () => 'r2Result', error: () => compositionErr };

stub(b, 'resolve', b.resolve);
stub(r1, 'error', r1.error);
stub(r1, 'resolve', r1.resolve);
stub(r2, 'resolve', r2.resolve);
stub(r2, 'error', r2.error);

const base = createResolver(b.resolve, b.error);
const comp = base.compose({
r1: r1,
r2: r2,
});

return Promise.all([
comp.r1().catch(e => {
expect(e).to.equal(compositionErr);
}),
comp.r2().then(r => {
expect(r).to.equal('r2Result');
}),

]).then(()=> {
expect(r1.resolve.calledOnce).to.be.true;
expect(r1.error.calledOnce).to.be.true;
expect(r2.resolve.calledOnce).to.be.true;
expect(r2.error.notCalled).to.be.true;
});
});

it('composed result has correct structure', () => {

const b = {
resolve: () => {},
error: d => compositionErr
};

stub(b, 'resolve', b.resolve);

const base = createResolver(b.resolve, b.error);
const comp = base.compose({
r1: { resolve: () => { throw Error('some other error') }, error: () => compositionErr },
r2: { resolve: () => 'r2Result', error: () => compositionErr },
r3: {} // should we throw an exception since it is not a resolver or createResolver params?
});

const composed = { r0: () => {}, ...comp };

expect(composed.r0).to.be.a(typeof Function);
expect(composed.r1).to.be.a(typeof Function);
expect(composed.r2).to.be.a(typeof Function);
expect(composed.r3).to.be.a(typeof Function);

});

});
});

0 comments on commit 7d49d06

Please sign in to comment.