Skip to content

Commit

Permalink
Merge pull request #25 from rynobax/master
Browse files Browse the repository at this point in the history
Adding conditional (and/or) helpers
  • Loading branch information
thebigredgeek authored May 9, 2018
2 parents 47ce04a + 1a06a0e commit 2c777e9
Show file tree
Hide file tree
Showing 10 changed files with 196 additions and 6 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,28 @@ const resolvers = combineResolvers([
export default resolvers;
```

Conditional resovlers:
```javascript
import { and, or } from 'apollo-resolvers';

import isFooResolver from './foo';
import isBarResolver from './bar';

const banResolver = (root, { input }, { models: { UserModel } })=> UserModel.ban(input);

// Will execute banResolver if either isFooResolver or isBarResolver successfully resolve
// If none of the resolvers succeed, the error from the last conditional resolver will
// be returned
const orBanResolver = or(isFooResolver, isBarResolver)(banResolver);

// Will execute banResolver if both isFooResolver and isBarResolver successfully resolve
// If one of the condition resolvers throws an error, it will stop the execution and
// return the error
const andBanResolver = and(isFooResolver, isBarResolver)(banResolver);

// In both cases, conditions are evaluated from left to right
```

## Resolver context

Resolvers are provided a mutable context object that is shared between all resolvers for a given request. A common pattern with GraphQL is inject request-specific model instances into the resolver context for each request. Models frequently reference one another, and unbinding circular references can be a pain. `apollo-resolvers` provides a request context factory that allows you to bind context disposal to server responses, calling a `dispose` method on each model instance attached to the context to do any sort of required reference cleanup necessary to avoid memory leaks:
Expand Down
2 changes: 1 addition & 1 deletion dist/context.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions dist/helper.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export declare const combineResolvers: (resolvers?: any[]) => any;
export declare const and: (...conditions: any[]) => (resolver: any) => any;
export declare const or: (...conditions: any[]) => (resolver: any) => (...query: any[]) => Promise<{}>;
19 changes: 19 additions & 0 deletions dist/helper.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/helper.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/promise.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/resolver.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/util.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions src/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,27 @@ import * as merge from "deepmerge";
// Helper function to combine multiple resolver definition hashes into a single hash for consumption by Apollostack's graphql-server
export const combineResolvers = (resolvers = []) => resolvers
.reduce((combined, resolver) => merge(combined, resolver));

// Accepts multiple authentication resolvers and returns a function which will be called
// if all of the authentication resolvers succeed, or throw an error if one of them fails
export const and = (...conditions) => resolver => {
return conditions.reduceRight((p, c) => {
return c.createResolver(p);
}, resolver)
}

// Accepts multiple authentication resolvers and returns a function which will be called
// if any of the authentication resolvers succeed, or throw an error if all of them fail
export const or = (...conditions) => resolver => (...query) => {
return new Promise((resolve, reject) => {
let limit = conditions.length - 1;
const attempt = (i) =>
conditions[i].createResolver(resolver)(...query)
.then(res => resolve(res))
.catch(err => {
if(i === limit) reject(err);
else attempt(i + 1);
});
attempt(0);
});
}
125 changes: 124 additions & 1 deletion test/unit/helper_spec.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { expect } from 'chai';
import { stub } from 'sinon';

import {
combineResolvers
combineResolvers, and, or,
} from '../../dist/helper';
import { createResolver } from '../../dist/resolver';

describe('(unit) src/helper.js', () => {
describe('combineResolvers', () => {
Expand Down Expand Up @@ -51,4 +53,125 @@ describe('(unit) src/helper.js', () => {
});
})
});

describe('Conditional resolvers', () => {
const conditionalErr = new Error('conditional error');
const successResolver = createResolver(() => null, () => null);
const failureResolver = createResolver(() => { throw conditionalErr; }, () => null);

describe('and', () => {
it('(true, true) succeeds', () => {
const resolver = and(successResolver, successResolver)(() => true);
return resolver().then(res => expect(res).to.be.true);
});

it('(false, true) throws', () => {
const resolver = and(failureResolver, successResolver)(() => true);
return resolver().catch(err => expect(err).to.equal(conditionalErr));
});

it('(false, false) throws', () => {
const resolver = and(failureResolver, failureResolver)(() => true);
return resolver().catch(err => expect(err).to.equal(conditionalErr));
});

it('stops evaluating resolvers after first failure', () => {
const r1 = {
handle: () => { throw conditionalErr },
error: () => null,
};

const r2 = {
handle: () => null,
error: () => null,
};

stub(r1, 'handle', r1.handle);
stub(r2, 'handle', r2.handle);

const resolver = or(
createResolver(r1.handle, r1.error),
createResolver(r2.handle, r2.error)
)(() => true);
return resolver().catch(err => {
expect(err).to.equal(conditionalErr)
expect(r1.handle.calledOnce).to.be.true;
expect(r2.handle.notCalled).to.be.true;
});
});

it('only calls the result resolver once', () => {
const r1 = {
handle: () => true,
error: () => null,
};

stub(r1, 'handle', r1.handle);

const resolver = and(successResolver, successResolver)(createResolver(r1.handle, r1.error));
return resolver().then(res => {
expect(res).to.be.true;
expect(r1.handle.calledOnce).to.be.true;
});
});
});

describe('or', () => {
it('(true, true) succeeds', () => {
const resolver = or(successResolver, successResolver)(() => true);
return resolver().then(res => expect(res).to.be.true);
});

it('(false, true) succeeds', () => {
const resolver = or(failureResolver, successResolver)(() => true);
return resolver().then(res => expect(res).to.be.true);
});

it('(false, false) throws', () => {
const resolver = or(failureResolver, failureResolver)(() => true);
return resolver().catch(err => expect(err).to.equal(conditionalErr));
});

it('stops evaluating resolvers after first success', () => {
const r1 = {
handle: () => null,
error: () => null,
};

const r2 = {
handle: () => null,
error: () => null,
};

stub(r1, 'handle', r1.handle);
stub(r2, 'handle', r2.handle);

const resolver = or(
createResolver(r1.handle, r1.error),
createResolver(r2.handle, r2.error)
)(() => true);
return resolver().then(res => {
expect(res).to.be.true;
expect(r1.handle.calledOnce).to.be.true;
expect(r2.handle.notCalled).to.be.true;
});
});

it('only calls the result resolver once', () => {
const r1 = {
handle: () => true,
error: () => null,
};

stub(r1, 'handle', r1.handle);

const resolver = or(failureResolver, successResolver)(createResolver(r1.handle, r1.error));
return resolver().then(res => {
expect(res).to.be.true;
expect(r1.handle.calledOnce).to.be.true;
});
});
});

});
});

0 comments on commit 2c777e9

Please sign in to comment.