Skip to content
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

Feat/notify from outside #27

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
});
});
});
5 changes: 0 additions & 5 deletions src/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@ const isValidLogger = (logger) => logger && typeof logger.error === 'function';
const getEntityNames = (entityConfigs) => Object.keys(entityConfigs);

const checkApiDeclaration = (logger, entityConfigs, entityName, entity) => {
if (typeof entity.api !== 'object') {
warn(logger, `No api definition found for entity ${entityName}`);
return;
}

const warnApi = (msg, ...args) => warn(
logger,
`Invalid api config. ${msg}`,
Expand Down
Loading