Skip to content

Commit

Permalink
Merge cda69c9 into 1f579db
Browse files Browse the repository at this point in the history
  • Loading branch information
petercrona committed Apr 30, 2017
2 parents 1f579db + cda69c9 commit e00ac97
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 49 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"eslint-plugin-import": "^2.2.0",
"gitbook-cli": "^2.3.0",
"mocha": "^2.5.3",
"nyc": "^10.1.2",
"nyc": "10.2.0",
"sinon": "^1.17.7",
"sinon-chai": "^2.8.0",
"webpack": "^2.4.1"
Expand Down
40 changes: 31 additions & 9 deletions src/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,16 +102,38 @@ const setApiConfigDefaults = ec => {

return {
...ec,
api: ec.api ? mapValues(setDefaults, ec.api) : ec.api
api: mapValues(setDefaults, ec.api)
};
};

const createNotifyFunction = (operation) => {
const fn = (x) => Promise.resolve(x);
fn.operation = operation;
fn.isNotifier = true;
fn.invalidates = [];
return fn;
};

const addNotifyFunctions = (entityConfig) => {
if (!entityConfig.api) {
entityConfig.api = {};
}

entityConfig.api._notifyCreate = createNotifyFunction('CREATE');
entityConfig.api._notifyRead = createNotifyFunction('READ');
entityConfig.api._notifyUpdate = createNotifyFunction('UPDATE');
entityConfig.api._notifyDelete = createNotifyFunction('DELETE');

return entityConfig;
};

// Config -> Map String EntityConfig
export const getEntityConfigs = compose( // exported for testing
export const createEntityConfigs = compose( // exported for testing
toObject(prop('name')),
mapObject(toEntity),
mapValues(setApiConfigDefaults),
mapValues(setEntityConfigDefaults),
mapValues(addNotifyFunctions),
filterObject(compose(not, isEqual('__config')))
);

Expand All @@ -128,13 +150,13 @@ const applyPlugin = curry((addChangeListener, config, entityConfigs, plugin) =>
});

// Config -> Api
export const build = (c, ps = []) => {
const config = getGlobalConfig(c);
const entityConfigs = getEntityConfigs(c);
validateConfig(console, entityConfigs, config);
const listenerStore = createListenerStore(config);
const applyPlugin_ = applyPlugin(listenerStore.addChangeListener, config);
export const build = (config, plugins = []) => {
const globalConfig = getGlobalConfig(config);
const entityConfigs = createEntityConfigs(config);
validateConfig(console, entityConfigs, globalConfig);
const listenerStore = createListenerStore(globalConfig);
const applyPlugin_ = applyPlugin(listenerStore.addChangeListener, globalConfig);
const applyPlugins = reduce(applyPlugin_, entityConfigs);
const createApi = compose(toApi, applyPlugins);
return createApi([cachePlugin(listenerStore.onChange), ...ps, dedupPlugin]);
return createApi([cachePlugin(listenerStore.onChange), ...plugins, dedupPlugin]);
};
35 changes: 35 additions & 0 deletions src/builder.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,34 @@ describe('builder', () => {
.then(() => api.user.getUsers())
.then(expectOnlyOneApiCall);
});
it('Adds notifiers to APIs', () => {
const myConfig = config();
myConfig.user.api.getUsers = sinon.spy(myConfig.user.api.getUsers);
const api = build(myConfig);
expect(api.user._notifyCreate).to.not.be.undefined;
expect(api.user._notifyRead).to.not.be.undefined;
expect(api.user._notifyUpdate).to.not.be.undefined;
expect(api.user._notifyDelete).to.not.be.undefined;
});
it('Adds notifiers to APIs even if no API is specified', () => {
const myConfig = config();
myConfig.user.api.getUsers = sinon.spy(myConfig.user.api.getUsers);
const api = build(myConfig);
delete api.user.api;
expect(api.user._notifyCreate).to.not.be.undefined;
expect(api.user._notifyRead).to.not.be.undefined;
expect(api.user._notifyUpdate).to.not.be.undefined;
expect(api.user._notifyDelete).to.not.be.undefined;
});
it('Notifiers are just identity functions lifted to promises', (done) => {
const myConfig = config();
myConfig.user.api.getUsers = sinon.spy(myConfig.user.api.getUsers);
const api = build(myConfig);
api.user._notifyCreate('hello').then((value) => {
expect(value).to.be.equal('hello');
done();
});
});
it('Two read api calls will return the same output', (done) => {
const myConfig = config();
myConfig.user.api.getUsers = sinon.spy(myConfig.user.api.getUsers);
Expand Down Expand Up @@ -215,5 +243,12 @@ describe('builder', () => {
expect(spy).not.to.have.been.called;
});
});

it('works without global config', () => {
const conf = config();
delete conf.__config;
const api = build(conf);
expect(api).to.be.defined;
});
});
});
13 changes: 11 additions & 2 deletions src/plugins/cache/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,22 @@ const HANDLERS = {
READ: decorateRead,
UPDATE: decorateUpdate,
DELETE: decorateDelete,
NO_OPERATION: decorateNoOperation
NO_OPERATION: decorateNoOperation,
NOTIFIER: decorateNoOperation
};

const getHandler = (fn) => {
if (fn.isNotifier === true) {
return HANDLERS.NOTIFIER;
}

return HANDLERS[fn.operation];
};

export const cachePlugin = (onChange) => ({ config, entityConfigs }) => {
const cache = createCache(values(entityConfigs), onChange);
return ({ entity, fn }) => {
const handler = HANDLERS[fn.operation];
const handler = getHandler(fn);
return handler(config, cache, entity, fn);
};
};
9 changes: 9 additions & 0 deletions src/plugins/cache/operations/notifier.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {passThrough} from 'ladda-fp';
import {invalidateQuery} from '../cache';

export function decorateNotifier(c, cache, e, aFn) {
return (...args) => {
return aFn(...args)
.then(passThrough(() => invalidateQuery(cache, e, aFn)));
};
}
62 changes: 62 additions & 0 deletions src/plugins/cache/operations/notifier.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/* eslint-disable no-unused-expressions */

import sinon from 'sinon';
import {decorateNotifier} from './notifier';
import * as Cache from '../cache';
import {createSampleConfig} from '../test-helper';

const config = createSampleConfig();

describe('DecorateNotifier', () => {
it('Invalidates as if it was the specified operation', (done) => {
const cache = Cache.createCache(config);
const e = config[0];
e.invalidatesOn = ['READ'];
const xOrg = {__ladda__id: 1, name: 'Kalle'};
const aFn = sinon.spy(() => Promise.resolve({}));
const getUsers = () => Promise.resolve(xOrg);
aFn.operation = 'READ';
aFn.isNotifier = true;
aFn.invalidates = [];
Cache.storeQueryResponse(cache, e, getUsers, ['args'], xOrg);
const res = decorateNotifier({}, cache, e, aFn);
res(xOrg).then(() => {
const killedCache = !Cache.containsQueryResponse(cache, e, getUsers, ['args']);
expect(killedCache).to.be.true;
done();
});
});
it('Does not invalidate if operation does not cause invalidation', (done) => {
const cache = Cache.createCache(config);
const e = config[0];
e.invalidatesOn = ['READ'];
const xOrg = {__ladda__id: 1, name: 'Kalle'};
const aFn = sinon.spy(() => Promise.resolve({}));
const getUsers = () => Promise.resolve(xOrg);
aFn.operation = 'DELETE';
aFn.isNotifier = true;
aFn.invalidates = [];
Cache.storeQueryResponse(cache, e, getUsers, ['args'], xOrg);
const res = decorateNotifier({}, cache, e, aFn);
res(xOrg).then(() => {
const killedCache = !Cache.containsQueryResponse(cache, e, getUsers, ['args']);
expect(killedCache).to.be.false;
done();
});
});
it('Does not write to cache', (done) => {
const cache = Cache.createCache(config);
const e = config[0];
const xOrg = {__ladda__id: 1, name: 'Kalle'};
const aFn = sinon.spy(() => Promise.resolve({}));
aFn.operation = 'CREATE';
aFn.isNotifier = false;
aFn.invalidates = [];
const res = decorateNotifier({}, cache, e, aFn);
res(xOrg).then(() => {
const hasEntity = Cache.containsEntity(cache, e, 1);
expect(hasEntity).to.be.false;
done();
});
});
});
Loading

0 comments on commit e00ac97

Please sign in to comment.