From 20319e9f735584aff6b31987a3e8085430a6af8b Mon Sep 17 00:00:00 2001 From: LFDM <1986gh@gmail.com> Date: Wed, 26 Apr 2017 19:13:09 +0200 Subject: [PATCH 01/32] Apply dedup before and after plugins if need be --- src/builder.js | 8 +++++++- src/builder.spec.js | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/builder.js b/src/builder.js index 2d3c3e1..6312222 100644 --- a/src/builder.js +++ b/src/builder.js @@ -127,6 +127,12 @@ const applyPlugin = curry((addChangeListener, config, entityConfigs, plugin) => return mapApiFunctions(pluginDecorator, entityConfigs); }); +const createPluginList = (core, plugins) => { + return plugins.length ? + [core, dedupPlugin, ...plugins, dedupPlugin] : + [core, dedupPlugin]; +}; + // Config -> Api export const build = (c, ps = []) => { const config = getGlobalConfig(c); @@ -136,5 +142,5 @@ export const build = (c, ps = []) => { const applyPlugin_ = applyPlugin(listenerStore.addChangeListener, config); const applyPlugins = reduce(applyPlugin_, entityConfigs); const createApi = compose(toApi, applyPlugins); - return createApi([cachePlugin(listenerStore.onChange), ...ps, dedupPlugin]); + return createApi(createPluginList(cachePlugin(listenerStore.onChange), ps)); }; diff --git a/src/builder.spec.js b/src/builder.spec.js index 8ef8a3e..11fdedc 100644 --- a/src/builder.spec.js +++ b/src/builder.spec.js @@ -152,6 +152,23 @@ describe('builder', () => { .then(() => done()); }); + it('applies dedup before and after the plugins, if there are any', () => { + const getAll = sinon.stub().returns(Promise.resolve([])); + getAll.operation = 'READ'; + const conf = { test: { api: { getAll } } }; + const plugin = () => ({ fn }) => () => { + fn(); + fn(); + return fn(); + }; + const api = build(conf, [plugin]); + api.test.getAll(); + api.test.getAll(); + return api.test.getAll().then(() => { + expect(getAll).to.have.been.calledOnce; + }); + }); + describe('change listener', () => { it('exposes Ladda\'s listener/onChange interface to plugins', () => { const plugin = ({ addChangeListener }) => { From ae0ba4aac00cc29782d989f096aefc1c6511e87b Mon Sep 17 00:00:00 2001 From: LFDM <1986gh@gmail.com> Date: Sun, 30 Apr 2017 08:30:57 +0200 Subject: [PATCH 02/32] Remove hook from EntityStore --- src/plugins/cache/entity-store.js | 22 ++-------- src/plugins/cache/entity-store.spec.js | 56 -------------------------- 2 files changed, 4 insertions(+), 74 deletions(-) diff --git a/src/plugins/cache/entity-store.js b/src/plugins/cache/entity-store.js index f0772a2..0558c9d 100644 --- a/src/plugins/cache/entity-store.js +++ b/src/plugins/cache/entity-store.js @@ -11,9 +11,8 @@ * Of course, this also requiers the view to truly be a subset of the entity. */ -import {curry, reduce, map_, clone, noop} from 'ladda-fp'; +import {curry, reduce, map_, clone} from 'ladda-fp'; import {merge} from './merger'; -import {removeId} from './id-helper'; // Value -> StoreValue const toStoreValue = v => ({value: v, timestamp: Date.now()}); @@ -50,16 +49,6 @@ const createViewKey = (e, v) => { // Entity -> Bool const isView = e => !!e.viewOf; -// EntityStore -> Hook -const getHook = (es) => es[2]; - -// EntityStore -> Type -> [Entity] -> () -const triggerHook = curry((es, e, type, xs) => getHook(es)({ - type, - entity: e.name, // real name, not getEntityType, which takes views into account! - entities: removeId(xs) -})); - // Function -> Function -> EntityStore -> Entity -> Value -> a const handle = curry((viewHandler, entityHandler, s, e, v) => { if (isView(e)) { @@ -102,7 +91,6 @@ const setViewValue = (s, e, v) => { // EntityStore -> Entity -> [Value] -> () export const mPut = curry((es, e, xs) => { map_(handle(setViewValue, setEntityValue)(es, e))(xs); - triggerHook(es, e, 'UPDATE', xs); }); // EntityStore -> Entity -> Value -> () @@ -129,14 +117,12 @@ const getViewValue = (s, e, id) => { // EntityStore -> Entity -> String -> () export const get = handle(getViewValue, getEntityValue); -// EntityStore -> Entity -> String -> () +// EntityStore -> Entity -> String -> Value export const remove = (es, e, id) => { const x = get(es, e, id); rm(es, createEntityKey(e, {__ladda__id: id})); rmViews(es, e); - if (x) { - triggerHook(es, e, 'DELETE', [x.value]); - } + return x; }; // EntityStore -> Entity -> String -> Bool @@ -163,4 +149,4 @@ const registerEntity = ([eMap, ...other], e) => { const updateIndex = (m, e) => { return isView(e) ? registerView(m, e) : registerEntity(m, e); }; // [Entity] -> EntityStore -export const createEntityStore = (c, hook = noop) => reduce(updateIndex, [{}, {}, hook], c); +export const createEntityStore = (c) => reduce(updateIndex, [{}, {}], c); diff --git a/src/plugins/cache/entity-store.spec.js b/src/plugins/cache/entity-store.spec.js index 2f87cbc..087179b 100644 --- a/src/plugins/cache/entity-store.spec.js +++ b/src/plugins/cache/entity-store.spec.js @@ -1,6 +1,5 @@ /* eslint-disable no-unused-expressions */ -import sinon from 'sinon'; import {createEntityStore, put, mPut, get, contains, remove} from './entity-store'; import {addId} from './id-helper'; @@ -232,59 +231,4 @@ describe('EntityStore', () => { expect(fn).to.not.throw(); }); }); - - describe('with a hook', () => { - describe('put', () => { - it('notifies with the put entity as singleton list', () => { - const hook = sinon.spy(); - const s = createEntityStore(config, hook); - const v = {id: 'hello'}; - const e = { name: 'user'}; - put(s, e, addId({}, undefined, undefined, v)); - expect(hook).to.have.been.called; - - expect(hook).to.have.been.calledWith({ - type: 'UPDATE', - entity: 'user', - entities: [v] - }); - }); - }); - - describe('mPut', () => { - it('notifies with the put entities', () => { - const hook = sinon.spy(); - const s = createEntityStore(config, hook); - const v1 = {id: 'hello'}; - const v2 = {id: 'there'}; - const e = { name: 'user'}; - const v1WithId = addId({}, undefined, undefined, v1); - const v2WithId = addId({}, undefined, undefined, v2); - mPut(s, e, [v1WithId, v2WithId]); - - expect(hook).to.have.been.called; - - const arg = hook.args[0][0]; - expect(arg.type).to.equal('UPDATE'); - expect(arg.entities).to.deep.equal([v1, v2]); - }); - }); - - describe('rm', () => { - it('notifies with the removed entity as a singleton list', () => { - const hook = sinon.spy(); - const s = createEntityStore(config, hook); - const v = {id: 'hello'}; - const e = { name: 'user'}; - put(s, e, addId({}, undefined, undefined, v)); - remove(s, e, v.id); - - expect(hook).to.have.been.calledTwice; // we also put! - - const arg = hook.args[1][0]; - expect(arg.type).to.equal('DELETE'); - expect(arg.entities).to.deep.equal([v]); - }); - }); - }); }); From ab77604d78eae1cbd38ec7f498f2bff66b95e800 Mon Sep 17 00:00:00 2001 From: LFDM <1986gh@gmail.com> Date: Sun, 30 Apr 2017 08:36:41 +0200 Subject: [PATCH 03/32] Pass notify fn to all operations --- src/plugins/cache/index.js | 17 +++++++-- src/plugins/cache/operations/create.js | 2 +- src/plugins/cache/operations/create.spec.js | 4 ++- src/plugins/cache/operations/delete.js | 2 +- src/plugins/cache/operations/delete.spec.js | 4 ++- src/plugins/cache/operations/no-operation.js | 2 +- .../cache/operations/no-operation.spec.js | 8 +++-- src/plugins/cache/operations/read.js | 2 +- src/plugins/cache/operations/read.spec.js | 35 ++++++++++--------- src/plugins/cache/operations/update.js | 2 +- src/plugins/cache/operations/update.spec.js | 4 ++- 11 files changed, 51 insertions(+), 31 deletions(-) diff --git a/src/plugins/cache/index.js b/src/plugins/cache/index.js index ff26949..8301e23 100644 --- a/src/plugins/cache/index.js +++ b/src/plugins/cache/index.js @@ -1,4 +1,4 @@ -import {values} from 'ladda-fp'; +import {curry, values} from 'ladda-fp'; import {createCache} from './cache'; import {decorateCreate} from './operations/create'; import {decorateRead} from './operations/read'; @@ -14,10 +14,21 @@ const HANDLERS = { NO_OPERATION: decorateNoOperation }; +const notify = curry((onChange, entity, fn, changeType, args, payload) => { + onChange({ + type: changeType, + entity: entity.name, + apiFn: fn.name, + values: Array.isArray(payload) ? payload : [payload], + args + }); +}); + export const cachePlugin = (onChange) => ({ config, entityConfigs }) => { - const cache = createCache(values(entityConfigs), onChange); + const cache = createCache(values(entityConfigs)); return ({ entity, fn }) => { const handler = HANDLERS[fn.operation]; - return handler(config, cache, entity, fn); + const notify_ = notify(onChange, entity, fn); + return handler(config, cache, notify_, entity, fn); }; }; diff --git a/src/plugins/cache/operations/create.js b/src/plugins/cache/operations/create.js index 93ebf49..e153826 100644 --- a/src/plugins/cache/operations/create.js +++ b/src/plugins/cache/operations/create.js @@ -2,7 +2,7 @@ import {passThrough, compose} from 'ladda-fp'; import {storeEntity, invalidateQuery} from '../cache'; import {addId} from '../id-helper'; -export function decorateCreate(c, cache, e, aFn) { +export function decorateCreate(c, cache, notify, e, aFn) { return (...args) => { return aFn(...args) .then(passThrough(() => invalidateQuery(cache, e, aFn))) diff --git a/src/plugins/cache/operations/create.spec.js b/src/plugins/cache/operations/create.spec.js index 7d8bb6c..9f89652 100644 --- a/src/plugins/cache/operations/create.spec.js +++ b/src/plugins/cache/operations/create.spec.js @@ -3,6 +3,8 @@ import {decorateCreate} from './create'; import {createCache, getEntity} from '../cache'; import {createApiFunction} from '../test-helper'; +const curryNoop = () => () => {}; + const config = [ { name: 'user', @@ -46,7 +48,7 @@ describe('Create', () => { const response = {...xOrg, id: 1}; const aFnWithoutSpy = createApiFunction(() => Promise.resolve(response)); const aFn = sinon.spy(aFnWithoutSpy); - const res = decorateCreate({}, cache, e, aFn); + const res = decorateCreate({}, cache, curryNoop, e, aFn); res(xOrg).then((newX) => { expect(newX).to.equal(response); expect(getEntity(cache, e, 1).value).to.deep.equal({...response, __ladda__id: 1}); diff --git a/src/plugins/cache/operations/delete.js b/src/plugins/cache/operations/delete.js index 7c7727a..5df9983 100644 --- a/src/plugins/cache/operations/delete.js +++ b/src/plugins/cache/operations/delete.js @@ -2,7 +2,7 @@ import {passThrough} from 'ladda-fp'; import * as Cache from '../cache'; import {serialize} from '../serializer'; -export function decorateDelete(c, cache, e, aFn) { +export function decorateDelete(c, cache, notify, e, aFn) { return (...args) => { return aFn(...args) .then(passThrough(() => Cache.invalidateQuery(cache, e, aFn))) diff --git a/src/plugins/cache/operations/delete.spec.js b/src/plugins/cache/operations/delete.spec.js index 85e98da..c4c18cc 100644 --- a/src/plugins/cache/operations/delete.spec.js +++ b/src/plugins/cache/operations/delete.spec.js @@ -4,6 +4,8 @@ import * as Cache from '../cache'; import {addId} from '../id-helper'; import {createApiFunction} from '../test-helper'; +const curryNoop = () => () => {}; + const config = [ { name: 'user', @@ -47,7 +49,7 @@ describe('Delete', () => { const aFnWithoutSpy = createApiFunction(() => Promise.resolve({})); const aFn = sinon.spy(aFnWithoutSpy); Cache.storeEntity(cache, e, addId({}, undefined, undefined, xOrg)); - const res = decorateDelete({}, cache, e, aFn); + const res = decorateDelete({}, cache, curryNoop, e, aFn); res(1).then(() => { expect(Cache.getEntity(cache, e, 1)).to.equal(undefined); done(); diff --git a/src/plugins/cache/operations/no-operation.js b/src/plugins/cache/operations/no-operation.js index 75cc5a5..e482c44 100644 --- a/src/plugins/cache/operations/no-operation.js +++ b/src/plugins/cache/operations/no-operation.js @@ -1,7 +1,7 @@ import {passThrough} from 'ladda-fp'; import {invalidateQuery} from '../cache'; -export function decorateNoOperation(c, cache, e, aFn) { +export function decorateNoOperation(c, cache, notify, e, aFn) { return (...args) => { return aFn(...args) .then(passThrough(() => invalidateQuery(cache, e, aFn))); diff --git a/src/plugins/cache/operations/no-operation.spec.js b/src/plugins/cache/operations/no-operation.spec.js index 398d500..36a67d8 100644 --- a/src/plugins/cache/operations/no-operation.spec.js +++ b/src/plugins/cache/operations/no-operation.spec.js @@ -5,6 +5,8 @@ import {decorateNoOperation} from './no-operation'; import * as Cache from '../cache'; import {createSampleConfig, createApiFunction} from '../test-helper'; +const curryNoop = () => () => {}; + const config = createSampleConfig(); describe('DecorateNoOperation', () => { @@ -16,7 +18,7 @@ describe('DecorateNoOperation', () => { const getUsers = () => Promise.resolve(xOrg); aFn.invalidates = ['getUsers']; Cache.storeQueryResponse(cache, e, getUsers, ['args'], xOrg); - const res = decorateNoOperation({}, cache, e, aFn); + const res = decorateNoOperation({}, cache, curryNoop, e, aFn); res(xOrg).then(() => { const killedCache = !Cache.containsQueryResponse(cache, e, getUsers, ['args']); expect(killedCache).to.be.true; @@ -29,7 +31,7 @@ describe('DecorateNoOperation', () => { const aFn = sinon.spy(() => { return Promise.resolve({}); }); - decorateNoOperation({}, cache, e, aFn); + decorateNoOperation({}, cache, curryNoop, e, aFn); expect(aFn.operation).to.be.undefined; }); it('Ignored inherited invalidation config', (done) => { @@ -41,7 +43,7 @@ describe('DecorateNoOperation', () => { const getUsers = createApiFunction(() => Promise.resolve(xOrg)); aFn.hasOwnProperty = () => false; Cache.storeQueryResponse(cache, e, getUsers, ['args'], xOrg); - const res = decorateNoOperation({}, cache, e, aFn); + const res = decorateNoOperation({}, cache, curryNoop, e, aFn); res(xOrg).then(() => { const killedCache = !Cache.containsQueryResponse(cache, e, getUsers, ['args']); expect(killedCache).to.be.false; diff --git a/src/plugins/cache/operations/read.js b/src/plugins/cache/operations/read.js index 045cbc1..f0c6733 100644 --- a/src/plugins/cache/operations/read.js +++ b/src/plugins/cache/operations/read.js @@ -78,7 +78,7 @@ const decorateReadQuery = (c, cache, e, aFn) => { }; }; -export function decorateRead(c, cache, e, aFn) { +export function decorateRead(c, cache, notify, e, aFn) { if (aFn.byId) { return decorateReadSingle(c, cache, e, aFn); } diff --git a/src/plugins/cache/operations/read.spec.js b/src/plugins/cache/operations/read.spec.js index b7193ac..2c7385b 100644 --- a/src/plugins/cache/operations/read.spec.js +++ b/src/plugins/cache/operations/read.spec.js @@ -6,6 +6,7 @@ import {decorateRead} from './read'; import {createCache} from '../cache'; import {createSampleConfig, createApiFunction} from '../test-helper'; +const curryNoop = () => () => {}; const config = createSampleConfig(); describe('Read', () => { @@ -16,7 +17,7 @@ describe('Read', () => { const xOrg = [{name: 'Kalle'}, {name: 'Anka'}]; const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg), {idFrom: 'ARGS'}); const aFn = sinon.spy(aFnWithoutSpy); - const res = decorateRead({}, cache, e, aFn); + const res = decorateRead({}, cache, curryNoop, e, aFn); res(1).then(x => { expect(x).to.deep.equal(xOrg); done(); @@ -28,7 +29,7 @@ describe('Read', () => { const xOrg = {name: 'Kalle'}; const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg), {idFrom: 'ARGS'}); const aFn = sinon.spy(aFnWithoutSpy); - const res = decorateRead({}, cache, e, aFn); + const res = decorateRead({}, cache, curryNoop, e, aFn); res({hello: 'hej', other: 'svej'}).then(x => { expect(x).to.deep.equal({name: 'Kalle'}); done(); @@ -40,7 +41,7 @@ describe('Read', () => { const xOrg = {id: 1, name: 'Kalle'}; const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg), {byId: true}); const aFn = sinon.spy(aFnWithoutSpy); - const res = decorateRead({}, cache, e, aFn); + const res = decorateRead({}, cache, curryNoop, e, aFn); res(1).then(() => { expect(aFn.callCount).to.equal(1); done(); @@ -54,7 +55,7 @@ describe('Read', () => { const xOrg = {id: 1, name: 'Kalle'}; const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg), {byId: true}); const aFn = sinon.spy(aFnWithoutSpy); - const res = decorateRead({}, cache, e, aFn); + const res = decorateRead({}, cache, curryNoop, e, aFn); const delay = () => new Promise((resolve) => setTimeout(resolve, 1)); res(1).then(delay).then(res.bind(null, 1)).then(() => { expect(aFn.callCount).to.equal(2); @@ -67,7 +68,7 @@ describe('Read', () => { const xOrg = {id: 1, name: 'Kalle'}; const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg), {byId: true}); const aFn = sinon.spy(aFnWithoutSpy); - const res = decorateRead({}, cache, e, aFn); + const res = decorateRead({}, cache, curryNoop, e, aFn); res(1).then(res.bind(null, 1)).then(() => { expect(aFn.callCount).to.equal(1); done(); @@ -87,7 +88,7 @@ describe('Read', () => { const cache = createCache(config); const e = config[0]; const fnWithSpy = sinon.spy(decoratedFn); - const apiFn = decorateRead({}, cache, e, fnWithSpy); + const apiFn = decorateRead({}, cache, curryNoop, e, fnWithSpy); return apiFn(['a', 'b']).then((res) => { expect(res).to.deep.equal([users.a, users.b]); }); @@ -97,7 +98,7 @@ describe('Read', () => { const cache = createCache(config); const e = config[0]; const fnWithSpy = sinon.spy(decoratedFn); - const apiFn = decorateRead({}, cache, e, fnWithSpy); + const apiFn = decorateRead({}, cache, curryNoop, e, fnWithSpy); return apiFn(['a', 'b']).then((res) => { expect(res).to.deep.equal([users.a, users.b]); }); @@ -107,7 +108,7 @@ describe('Read', () => { const cache = createCache(config); const e = config[0]; const fnWithSpy = sinon.spy(decoratedFn); - const apiFn = decorateRead({}, cache, e, fnWithSpy); + const apiFn = decorateRead({}, cache, curryNoop, e, fnWithSpy); const args = ['a', 'b']; return apiFn(args).then(() => { return apiFn(args).then((res) => { @@ -121,7 +122,7 @@ describe('Read', () => { const cache = createCache(config); const e = config[0]; const fnWithSpy = sinon.spy(decoratedFn); - const apiFn = decorateRead({}, cache, e, fnWithSpy); + const apiFn = decorateRead({}, cache, curryNoop, e, fnWithSpy); return apiFn(['a', 'b']).then(() => { return apiFn(['b', 'c']).then(() => { expect(fnWithSpy).to.have.been.calledTwice; @@ -135,7 +136,7 @@ describe('Read', () => { const cache = createCache(config); const e = config[0]; const fnWithSpy = sinon.spy(decoratedFn); - const apiFn = decorateRead({}, cache, e, fnWithSpy); + const apiFn = decorateRead({}, cache, curryNoop, e, fnWithSpy); return apiFn(['a', 'b']).then(() => { return apiFn(['a', 'b', 'c']).then((res) => { expect(res).to.deep.equal([users.a, users.b, users.c]); @@ -149,7 +150,7 @@ describe('Read', () => { const xOrg = {id: 1, name: 'Kalle'}; const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg)); const aFn = sinon.spy(aFnWithoutSpy); - const res = decorateRead({}, cache, e, aFn); + const res = decorateRead({}, cache, curryNoop, e, aFn); res(1).then(() => { expect(aFn.callCount).to.equal(1); done(); @@ -161,7 +162,7 @@ describe('Read', () => { const xOrg = {id: 1, name: 'Kalle'}; const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg)); const aFn = sinon.spy(aFnWithoutSpy); - const res = decorateRead({}, cache, e, aFn); + const res = decorateRead({}, cache, curryNoop, e, aFn); const firstCall = res(1); @@ -178,7 +179,7 @@ describe('Read', () => { const xOrg = {id: 1, name: 'Kalle'}; const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg)); const aFn = sinon.spy(aFnWithoutSpy); - const res = decorateRead({}, cache, e, aFn); + const res = decorateRead({}, cache, curryNoop, e, aFn); const firstCall = res(1); @@ -195,7 +196,7 @@ describe('Read', () => { const xOrg = [{id: 1, name: 'Kalle'}]; const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg)); const aFn = sinon.spy(aFnWithoutSpy); - const res = decorateRead({}, cache, e, aFn); + const res = decorateRead({}, cache, curryNoop, e, aFn); res(1).then((x) => { expect(x).to.equal(xOrg); done(); @@ -207,7 +208,7 @@ describe('Read', () => { const xOrg = [{id: 1, name: 'Kalle'}]; const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg)); const aFn = sinon.spy(aFnWithoutSpy); - const res = decorateRead({}, cache, e, aFn); + const res = decorateRead({}, cache, curryNoop, e, aFn); const firstCall = res(1); @@ -224,7 +225,7 @@ describe('Read', () => { const xOrg = [{id: 1, name: 'Kalle'}]; const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg)); const aFn = sinon.spy(aFnWithoutSpy); - const res = decorateRead({}, cache, e, aFn); + const res = decorateRead({}, cache, curryNoop, e, aFn); const firstCall = res(1); @@ -241,7 +242,7 @@ describe('Read', () => { const xOrg = {name: 'Kalle'}; const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg)); const aFn = sinon.spy(aFnWithoutSpy); - const res = decorateRead({}, cache, e, aFn); + const res = decorateRead({}, cache, curryNoop, e, aFn); res().catch(err => { expect(err).to.be.a('Error'); diff --git a/src/plugins/cache/operations/update.js b/src/plugins/cache/operations/update.js index 0634f61..e39e371 100644 --- a/src/plugins/cache/operations/update.js +++ b/src/plugins/cache/operations/update.js @@ -2,7 +2,7 @@ import {passThrough} from 'ladda-fp'; import * as Cache from '../cache'; import {addId} from '../id-helper'; -export function decorateUpdate(c, cache, e, aFn) { +export function decorateUpdate(c, cache, notify, e, aFn) { return (eValue, ...args) => { return aFn(eValue, ...args) .then(passThrough(() => Cache.invalidateQuery(cache, e, aFn))) diff --git a/src/plugins/cache/operations/update.spec.js b/src/plugins/cache/operations/update.spec.js index 078f8b0..e52dd7d 100644 --- a/src/plugins/cache/operations/update.spec.js +++ b/src/plugins/cache/operations/update.spec.js @@ -3,6 +3,8 @@ import {decorateUpdate} from './update'; import * as Cache from '../cache'; import {createApiFunction} from '../test-helper'; +const curryNoop = () => () => {}; + const config = [ { name: 'user', @@ -46,7 +48,7 @@ describe('Update', () => { const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg)); const aFn = sinon.spy(aFnWithoutSpy); - const res = decorateUpdate({}, cache, e, aFn); + const res = decorateUpdate({}, cache, curryNoop, e, aFn); res(xOrg, 'other args').then(() => { expect(Cache.getEntity(cache, e, 1).value).to.deep.equal({...xOrg, __ladda__id: 1}); done(); From 2209db752bea8f0989134b1bb5a107f52d2282b4 Mon Sep 17 00:00:00 2001 From: LFDM <1986gh@gmail.com> Date: Sun, 30 Apr 2017 08:55:25 +0200 Subject: [PATCH 04/32] Normalize fn name for change objects --- src/plugins/cache/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/plugins/cache/index.js b/src/plugins/cache/index.js index 8301e23..70e8bdc 100644 --- a/src/plugins/cache/index.js +++ b/src/plugins/cache/index.js @@ -14,11 +14,13 @@ const HANDLERS = { NO_OPERATION: decorateNoOperation }; +const normalizeFnName = (fnName) => fnName.replace(/^bound /, ''); + const notify = curry((onChange, entity, fn, changeType, args, payload) => { onChange({ type: changeType, entity: entity.name, - apiFn: fn.name, + apiFn: normalizeFnName(fn.name), values: Array.isArray(payload) ? payload : [payload], args }); From 1d7887b0ff4cb2a5000713ce0984b1abbed12ab8 Mon Sep 17 00:00:00 2001 From: LFDM <1986gh@gmail.com> Date: Sun, 30 Apr 2017 08:56:05 +0200 Subject: [PATCH 05/32] Update build syntax --- src/builder.spec.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/builder.spec.js b/src/builder.spec.js index 11fdedc..133ca5b 100644 --- a/src/builder.spec.js +++ b/src/builder.spec.js @@ -193,8 +193,10 @@ describe('builder', () => { expect(spy).to.have.been.calledOnce; const changeObject = spy.args[0][0]; expect(changeObject.entity).to.equal('user'); - expect(changeObject.type).to.equal('UPDATE'); - expect(changeObject.entities).to.deep.equal(users); + expect(changeObject.apiFn).to.equal('getUsers'); + expect(changeObject.type).to.equal('CREATE'); + expect(changeObject.values).to.deep.equal(users); + expect(changeObject.args).to.deep.equal([]); }); }); From fbb560742226c19668e7dd3f75ece9de03e74a18 Mon Sep 17 00:00:00 2001 From: LFDM <1986gh@gmail.com> Date: Sun, 30 Apr 2017 08:57:14 +0200 Subject: [PATCH 06/32] More expressive spec naming --- src/builder.spec.js | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/builder.spec.js b/src/builder.spec.js index 133ca5b..867987b 100644 --- a/src/builder.spec.js +++ b/src/builder.spec.js @@ -179,24 +179,26 @@ describe('builder', () => { build(config(), [plugin]); }); - it('allows plugins to add a listener, which gets notified on all cache changes', () => { - const spy = sinon.spy(); + describe('allows plugins to add a listener, which gets notified on all cache changes', () => { + it('on READ operations', () => { + const spy = sinon.spy(); - const plugin = ({ addChangeListener }) => { - addChangeListener(spy); - return ({ fn }) => fn; - }; + const plugin = ({ addChangeListener }) => { + addChangeListener(spy); + return ({ fn }) => fn; + }; - const api = build(config(), [plugin]); + const api = build(config(), [plugin]); - return api.user.getUsers().then(() => { - expect(spy).to.have.been.calledOnce; - const changeObject = spy.args[0][0]; - expect(changeObject.entity).to.equal('user'); - expect(changeObject.apiFn).to.equal('getUsers'); - expect(changeObject.type).to.equal('CREATE'); - expect(changeObject.values).to.deep.equal(users); - expect(changeObject.args).to.deep.equal([]); + return api.user.getUsers().then(() => { + expect(spy).to.have.been.calledOnce; + const changeObject = spy.args[0][0]; + expect(changeObject.entity).to.equal('user'); + expect(changeObject.apiFn).to.equal('getUsers'); + expect(changeObject.type).to.equal('CREATE'); + expect(changeObject.values).to.deep.equal(users); + expect(changeObject.args).to.deep.equal([]); + }); }); }); From dfc98a1f40a6fadd07a3b5de967d08f91605ce48 Mon Sep 17 00:00:00 2001 From: LFDM <1986gh@gmail.com> Date: Sun, 30 Apr 2017 08:57:54 +0200 Subject: [PATCH 07/32] Trigger notify on READ operations --- src/plugins/cache/operations/read.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/plugins/cache/operations/read.js b/src/plugins/cache/operations/read.js index f0c6733..f758807 100644 --- a/src/plugins/cache/operations/read.js +++ b/src/plugins/cache/operations/read.js @@ -19,7 +19,7 @@ const readFromCache = curry((cache, e, aFn, id) => { return undefined; }); -const decorateReadSingle = (c, cache, e, aFn) => { +const decorateReadSingle = (c, cache, notify, e, aFn) => { return (id) => { const fromCache = readFromCache(cache, e, aFn, id); if (fromCache) { @@ -28,11 +28,12 @@ const decorateReadSingle = (c, cache, e, aFn) => { return aFn(id) .then(passThrough(compose(Cache.storeEntity(cache, e), addId(c, aFn, id)))) - .then(passThrough(() => Cache.invalidateQuery(cache, e, aFn))); + .then(passThrough(() => Cache.invalidateQuery(cache, e, aFn))) + .then(passThrough(notify('CREATE', [id]))); }; }; -const decorateReadSome = (c, cache, e, aFn) => { +const decorateReadSome = (c, cache, notify, e, aFn) => { return (ids) => { const readFromCache_ = readFromCache(cache, e, aFn); const [cached, remaining] = reduce(([c_, r], id) => { @@ -57,11 +58,12 @@ const decorateReadSome = (c, cache, e, aFn) => { .then((other) => { const asMap = compose(toIdMap, concat)(cached, other); return map((id) => asMap[id], ids); - }); + }) + .then(passThrough(notify('CREATE', [remaining]))); }; }; -const decorateReadQuery = (c, cache, e, aFn) => { +const decorateReadQuery = (c, cache, notify, e, aFn) => { return (...args) => { if (Cache.containsQueryResponse(cache, e, aFn, args) && !aFn.alwaysGetFreshData) { const v = Cache.getQueryResponseWithMeta(cache, e, aFn, args); @@ -74,16 +76,17 @@ const decorateReadQuery = (c, cache, e, aFn) => { .then(passThrough( compose(Cache.storeQueryResponse(cache, e, aFn, args), addId(c, aFn, args)))) - .then(passThrough(() => Cache.invalidateQuery(cache, e, aFn))); + .then(passThrough(() => Cache.invalidateQuery(cache, e, aFn))) + .then(passThrough(notify('CREATE', args))); }; }; export function decorateRead(c, cache, notify, e, aFn) { if (aFn.byId) { - return decorateReadSingle(c, cache, e, aFn); + return decorateReadSingle(c, cache, notify, e, aFn); } if (aFn.byIds) { - return decorateReadSome(c, cache, e, aFn); + return decorateReadSome(c, cache, notify, e, aFn); } - return decorateReadQuery(c, cache, e, aFn); + return decorateReadQuery(c, cache, notify, e, aFn); } From 0f367e07328fc31a5a5ceb6136b5aae653f11ba2 Mon Sep 17 00:00:00 2001 From: LFDM <1986gh@gmail.com> Date: Sun, 30 Apr 2017 08:59:55 +0200 Subject: [PATCH 08/32] Better spec organization --- src/plugins/cache/operations/read.spec.js | 84 +++++++++++++---------- 1 file changed, 49 insertions(+), 35 deletions(-) diff --git a/src/plugins/cache/operations/read.spec.js b/src/plugins/cache/operations/read.spec.js index 2c7385b..b469ff1 100644 --- a/src/plugins/cache/operations/read.spec.js +++ b/src/plugins/cache/operations/read.spec.js @@ -23,6 +23,7 @@ describe('Read', () => { done(); }); }); + it('does set id to serialized args if idFrom ARGS', (done) => { const cache = createCache(config); const e = config[0]; @@ -35,45 +36,51 @@ describe('Read', () => { done(); }); }); - it('calls api fn if not in cache with byId set', (done) => { - const cache = createCache(config); - const e = config[0]; - const xOrg = {id: 1, name: 'Kalle'}; - const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg), {byId: true}); - const aFn = sinon.spy(aFnWithoutSpy); - const res = decorateRead({}, cache, curryNoop, e, aFn); - res(1).then(() => { - expect(aFn.callCount).to.equal(1); - done(); + + describe('with byId set', () => { + it('calls api fn if not in cache', (done) => { + const cache = createCache(config); + const e = config[0]; + const xOrg = {id: 1, name: 'Kalle'}; + const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg), {byId: true}); + const aFn = sinon.spy(aFnWithoutSpy); + const res = decorateRead({}, cache, curryNoop, e, aFn); + res(1).then(() => { + expect(aFn.callCount).to.equal(1); + done(); + }); }); - }); - it('calls api fn if in cache, but expired, with byId set', (done) => { - const myConfig = createSampleConfig(); - myConfig[0].ttl = 0; - const cache = createCache(myConfig); - const e = myConfig[0]; - const xOrg = {id: 1, name: 'Kalle'}; - const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg), {byId: true}); - const aFn = sinon.spy(aFnWithoutSpy); - const res = decorateRead({}, cache, curryNoop, e, aFn); - const delay = () => new Promise((resolve) => setTimeout(resolve, 1)); - res(1).then(delay).then(res.bind(null, 1)).then(() => { - expect(aFn.callCount).to.equal(2); - done(); + + it('calls api fn if in cache, but expired', (done) => { + const myConfig = createSampleConfig(); + myConfig[0].ttl = 0; + const cache = createCache(myConfig); + const e = myConfig[0]; + const xOrg = {id: 1, name: 'Kalle'}; + const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg), {byId: true}); + const aFn = sinon.spy(aFnWithoutSpy); + const res = decorateRead({}, cache, curryNoop, e, aFn); + const delay = () => new Promise((resolve) => setTimeout(resolve, 1)); + res(1).then(delay).then(res.bind(null, 1)).then(() => { + expect(aFn.callCount).to.equal(2); + done(); + }); }); - }); - it('does not call api fn if in cache with byId set', (done) => { - const cache = createCache(config); - const e = config[0]; - const xOrg = {id: 1, name: 'Kalle'}; - const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg), {byId: true}); - const aFn = sinon.spy(aFnWithoutSpy); - const res = decorateRead({}, cache, curryNoop, e, aFn); - res(1).then(res.bind(null, 1)).then(() => { - expect(aFn.callCount).to.equal(1); - done(); + + it('does not call api fn if in cache', (done) => { + const cache = createCache(config); + const e = config[0]; + const xOrg = {id: 1, name: 'Kalle'}; + const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg), {byId: true}); + const aFn = sinon.spy(aFnWithoutSpy); + const res = decorateRead({}, cache, curryNoop, e, aFn); + res(1).then(res.bind(null, 1)).then(() => { + expect(aFn.callCount).to.equal(1); + done(); + }); }); }); + describe('with byIds', () => { const users = { a: { id: 'a' }, @@ -144,6 +151,7 @@ describe('Read', () => { }); }); }); + it('calls api fn if not in cache', (done) => { const cache = createCache(config); const e = config[0]; @@ -156,6 +164,7 @@ describe('Read', () => { done(); }); }); + it('does not call api fn if in cache', (done) => { const cache = createCache(config); const e = config[0]; @@ -173,6 +182,7 @@ describe('Read', () => { }); }); }); + it('does call api fn if in cache but expired', (done) => { const cache = createCache(config); const e = {...config[0], ttl: -1}; @@ -190,6 +200,7 @@ describe('Read', () => { }); }); }); + it('calls api fn if not in cache (plural)', (done) => { const cache = createCache(config); const e = config[0]; @@ -202,6 +213,7 @@ describe('Read', () => { done(); }); }); + it('does not call api fn if in cache (plural)', (done) => { const cache = createCache(config); const e = config[0]; @@ -219,6 +231,7 @@ describe('Read', () => { }); }); }); + it('does call api fn if in cache but expired (plural)', (done) => { const cache = createCache(config); const e = {...config[0], ttl: -1}; @@ -236,6 +249,7 @@ describe('Read', () => { }); }); }); + it('throws if id is missing', (done) => { const cache = createCache(config); const e = {...config[0], ttl: 300}; From 489cac8a9333e76c565d7ad9345733d7fd6207e0 Mon Sep 17 00:00:00 2001 From: LFDM <1986gh@gmail.com> Date: Sun, 30 Apr 2017 09:01:42 +0200 Subject: [PATCH 09/32] Use return values, not done callback in READ specs --- src/plugins/cache/operations/read.spec.js | 68 ++++++++++------------- 1 file changed, 28 insertions(+), 40 deletions(-) diff --git a/src/plugins/cache/operations/read.spec.js b/src/plugins/cache/operations/read.spec.js index b469ff1..079d05e 100644 --- a/src/plugins/cache/operations/read.spec.js +++ b/src/plugins/cache/operations/read.spec.js @@ -11,47 +11,44 @@ const config = createSampleConfig(); describe('Read', () => { describe('decorateRead', () => { - it('stores and returns an array with elements that lack id', (done) => { + it('stores and returns an array with elements that lack id', () => { const cache = createCache(config); const e = config[0]; const xOrg = [{name: 'Kalle'}, {name: 'Anka'}]; const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg), {idFrom: 'ARGS'}); const aFn = sinon.spy(aFnWithoutSpy); const res = decorateRead({}, cache, curryNoop, e, aFn); - res(1).then(x => { + return res(1).then(x => { expect(x).to.deep.equal(xOrg); - done(); }); }); - it('does set id to serialized args if idFrom ARGS', (done) => { + it('does set id to serialized args if idFrom ARGS', () => { const cache = createCache(config); const e = config[0]; const xOrg = {name: 'Kalle'}; const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg), {idFrom: 'ARGS'}); const aFn = sinon.spy(aFnWithoutSpy); const res = decorateRead({}, cache, curryNoop, e, aFn); - res({hello: 'hej', other: 'svej'}).then(x => { + return res({hello: 'hej', other: 'svej'}).then(x => { expect(x).to.deep.equal({name: 'Kalle'}); - done(); }); }); describe('with byId set', () => { - it('calls api fn if not in cache', (done) => { + it('calls api fn if not in cache', () => { const cache = createCache(config); const e = config[0]; const xOrg = {id: 1, name: 'Kalle'}; const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg), {byId: true}); const aFn = sinon.spy(aFnWithoutSpy); const res = decorateRead({}, cache, curryNoop, e, aFn); - res(1).then(() => { + return res(1).then(() => { expect(aFn.callCount).to.equal(1); - done(); }); }); - it('calls api fn if in cache, but expired', (done) => { + it('calls api fn if in cache, but expired', () => { const myConfig = createSampleConfig(); myConfig[0].ttl = 0; const cache = createCache(myConfig); @@ -61,22 +58,20 @@ describe('Read', () => { const aFn = sinon.spy(aFnWithoutSpy); const res = decorateRead({}, cache, curryNoop, e, aFn); const delay = () => new Promise((resolve) => setTimeout(resolve, 1)); - res(1).then(delay).then(res.bind(null, 1)).then(() => { + return res(1).then(delay).then(res.bind(null, 1)).then(() => { expect(aFn.callCount).to.equal(2); - done(); }); }); - it('does not call api fn if in cache', (done) => { + it('does not call api fn if in cache', () => { const cache = createCache(config); const e = config[0]; const xOrg = {id: 1, name: 'Kalle'}; const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg), {byId: true}); const aFn = sinon.spy(aFnWithoutSpy); const res = decorateRead({}, cache, curryNoop, e, aFn); - res(1).then(res.bind(null, 1)).then(() => { + return res(1).then(res.bind(null, 1)).then(() => { expect(aFn.callCount).to.equal(1); - done(); }); }); }); @@ -152,20 +147,19 @@ describe('Read', () => { }); }); - it('calls api fn if not in cache', (done) => { + it('calls api fn if not in cache', () => { const cache = createCache(config); const e = config[0]; const xOrg = {id: 1, name: 'Kalle'}; const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg)); const aFn = sinon.spy(aFnWithoutSpy); const res = decorateRead({}, cache, curryNoop, e, aFn); - res(1).then(() => { + return res(1).then(() => { expect(aFn.callCount).to.equal(1); - done(); }); }); - it('does not call api fn if in cache', (done) => { + it('does not call api fn if in cache', () => { const cache = createCache(config); const e = config[0]; const xOrg = {id: 1, name: 'Kalle'}; @@ -175,15 +169,14 @@ describe('Read', () => { const firstCall = res(1); - firstCall.then(() => { - res(1).then(() => { + return firstCall.then(() => { + return res(1).then(() => { expect(aFn.callCount).to.equal(1); - done(); }); }); }); - it('does call api fn if in cache but expired', (done) => { + it('does call api fn if in cache but expired', () => { const cache = createCache(config); const e = {...config[0], ttl: -1}; const xOrg = {id: 1, name: 'Kalle'}; @@ -193,28 +186,26 @@ describe('Read', () => { const firstCall = res(1); - firstCall.then(() => { - res(1).then(() => { + return firstCall.then(() => { + return res(1).then(() => { expect(aFn.callCount).to.equal(2); - done(); }); }); }); - it('calls api fn if not in cache (plural)', (done) => { + it('calls api fn if not in cache (plural)', () => { const cache = createCache(config); const e = config[0]; const xOrg = [{id: 1, name: 'Kalle'}]; const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg)); const aFn = sinon.spy(aFnWithoutSpy); const res = decorateRead({}, cache, curryNoop, e, aFn); - res(1).then((x) => { + return res(1).then((x) => { expect(x).to.equal(xOrg); - done(); }); }); - it('does not call api fn if in cache (plural)', (done) => { + it('does not call api fn if in cache (plural)', () => { const cache = createCache(config); const e = config[0]; const xOrg = [{id: 1, name: 'Kalle'}]; @@ -224,15 +215,14 @@ describe('Read', () => { const firstCall = res(1); - firstCall.then(() => { - res(1).then(() => { + return firstCall.then(() => { + return res(1).then(() => { expect(aFn.callCount).to.equal(1); - done(); }); }); }); - it('does call api fn if in cache but expired (plural)', (done) => { + it('does call api fn if in cache but expired (plural)', () => { const cache = createCache(config); const e = {...config[0], ttl: -1}; const xOrg = [{id: 1, name: 'Kalle'}]; @@ -242,15 +232,14 @@ describe('Read', () => { const firstCall = res(1); - firstCall.then(() => { - res(1).then(() => { + return firstCall.then(() => { + return res(1).then(() => { expect(aFn.callCount).to.equal(2); - done(); }); }); }); - it('throws if id is missing', (done) => { + it('throws if id is missing', () => { const cache = createCache(config); const e = {...config[0], ttl: 300}; const xOrg = {name: 'Kalle'}; @@ -258,9 +247,8 @@ describe('Read', () => { const aFn = sinon.spy(aFnWithoutSpy); const res = decorateRead({}, cache, curryNoop, e, aFn); - res().catch(err => { + return res().catch(err => { expect(err).to.be.a('Error'); - done(); }); }); }); From 4f7fb59f94eca863a02d93e4e55452e93c09eb63 Mon Sep 17 00:00:00 2001 From: LFDM <1986gh@gmail.com> Date: Sun, 30 Apr 2017 09:35:04 +0200 Subject: [PATCH 10/32] Add specs for notify calls on READ --- src/plugins/cache/operations/read.spec.js | 110 +++++++++++++++++++++- 1 file changed, 109 insertions(+), 1 deletion(-) diff --git a/src/plugins/cache/operations/read.spec.js b/src/plugins/cache/operations/read.spec.js index 079d05e..21a3df4 100644 --- a/src/plugins/cache/operations/read.spec.js +++ b/src/plugins/cache/operations/read.spec.js @@ -1,7 +1,7 @@ /* eslint-disable no-unused-expressions */ import sinon from 'sinon'; -import {map} from 'ladda-fp'; +import {curry, map} from 'ladda-fp'; import {decorateRead} from './read'; import {createCache} from '../cache'; import {createSampleConfig, createApiFunction} from '../test-helper'; @@ -74,6 +74,34 @@ describe('Read', () => { expect(aFn.callCount).to.equal(1); }); }); + + it('triggers notify when not in cache', () => { + const spy = sinon.spy(); + const n = curry((a, b, c) => spy(a, b, c)); + const cache = createCache(config); + const e = config[0]; + const xOrg = {id: 1, name: 'Kalle'}; + const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg), {byId: true}); + const aFn = sinon.spy(aFnWithoutSpy); + const res = decorateRead({}, cache, n, e, aFn); + return res(1).then((r) => { + expect(spy).to.have.been.calledWith('CREATE', [1], r); + }); + }); + + it('does not trigger notify when in cache', () => { + const spy = sinon.spy(); + const n = curry((a, b, c) => spy(a, b, c)); + const cache = createCache(config); + const e = config[0]; + const xOrg = {id: 1, name: 'Kalle'}; + const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg), {byId: true}); + const aFn = sinon.spy(aFnWithoutSpy); + const res = decorateRead({}, cache, n, e, aFn); + return res(1).then(res.bind(null, 1)).then(() => { + expect(spy).to.have.been.calledOnce; // and not a second time for the cache hit + }); + }); }); describe('with byIds', () => { @@ -145,6 +173,50 @@ describe('Read', () => { }); }); }); + + it('triggers notify when not in cache', () => { + const spy = sinon.spy(); + const n = curry((a, b, c) => spy(a, b, c)); + const cache = createCache(config); + const e = config[0]; + const fnWithSpy = sinon.spy(decoratedFn); + const apiFn = decorateRead({}, cache, n, e, fnWithSpy); + return apiFn(['a', 'b']).then((r) => { + expect(spy).to.have.been.calledOnce; + expect(spy).to.have.been.calledWith('CREATE', [['a', 'b']], r); + }); + }); + + it('triggers notify when not in cache for partial request', () => { + const spy = sinon.spy(); + const n = curry((a, b, c) => spy(a, b, c)); + const cache = createCache(config); + const e = config[0]; + const fnWithSpy = sinon.spy(decoratedFn); + const apiFn = decorateRead({}, cache, n, e, fnWithSpy); + return apiFn(['a', 'b']).then(() => { + spy.reset(); + return apiFn(['a', 'b', 'c']).then((r) => { + expect(spy).to.have.been.calledOnce; + expect(spy).to.have.been.calledWith('CREATE', [['c']], r); + }); + }); + }); + + it('does not trigger notify when in cache', () => { + const spy = sinon.spy(); + const n = curry((a, b, c) => spy(a, b, c)); + const cache = createCache(config); + const e = config[0]; + const fnWithSpy = sinon.spy(decoratedFn); + const apiFn = decorateRead({}, cache, n, e, fnWithSpy); + return apiFn(['a', 'b']).then(() => { + spy.reset(); + return apiFn(['a', 'b']).then(() => { + expect(spy).not.to.have.been.called; + }); + }); + }); }); it('calls api fn if not in cache', () => { @@ -251,5 +323,41 @@ describe('Read', () => { expect(err).to.be.a('Error'); }); }); + + it('triggers notify when not in cache', () => { + const spy = sinon.spy(); + const n = curry((a, b, c) => spy(a, b, c)); + const cache = createCache(config); + const e = config[0]; + const xOrg = [{id: 1, name: 'Kalle'}]; + const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg)); + const aFn = sinon.spy(aFnWithoutSpy); + const res = decorateRead({}, cache, n, e, aFn); + + return res(1).then((r) => { + expect(spy).to.have.been.calledOnce; + expect(spy).to.have.been.calledWith('CREATE', [1], r); + }); + }); + + it('does not trigger notify when in cache', () => { + const spy = sinon.spy(); + const n = curry((a, b, c) => spy(a, b, c)); + const cache = createCache(config); + const e = config[0]; + const xOrg = [{id: 1, name: 'Kalle'}]; + const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg)); + const aFn = sinon.spy(aFnWithoutSpy); + const res = decorateRead({}, cache, n, e, aFn); + + const firstCall = res(1); + + return firstCall.then(() => { + spy.reset(); + return res(1).then(() => { + expect(spy).not.to.have.been.called; + }); + }); + }); }); }); From bb0c531b75fffe0bb83eeb56cd9a0ee8ece28019 Mon Sep 17 00:00:00 2001 From: LFDM <1986gh@gmail.com> Date: Sun, 30 Apr 2017 09:39:34 +0200 Subject: [PATCH 11/32] Add notify spec for update --- src/plugins/cache/operations/update.js | 3 ++- src/plugins/cache/operations/update.spec.js | 25 ++++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/plugins/cache/operations/update.js b/src/plugins/cache/operations/update.js index e39e371..3c6d78d 100644 --- a/src/plugins/cache/operations/update.js +++ b/src/plugins/cache/operations/update.js @@ -6,6 +6,7 @@ export function decorateUpdate(c, cache, notify, e, aFn) { return (eValue, ...args) => { return aFn(eValue, ...args) .then(passThrough(() => Cache.invalidateQuery(cache, e, aFn))) - .then(passThrough(() => Cache.storeEntity(cache, e, addId(c, undefined, undefined, eValue)))); + .then(passThrough(() => Cache.storeEntity(cache, e, addId(c, undefined, undefined, eValue)))) + .then(passThrough(() => notify('UPDATE', [eValue, ...args], eValue))); }; } diff --git a/src/plugins/cache/operations/update.spec.js b/src/plugins/cache/operations/update.spec.js index e52dd7d..ec2a103 100644 --- a/src/plugins/cache/operations/update.spec.js +++ b/src/plugins/cache/operations/update.spec.js @@ -1,4 +1,7 @@ +/* eslint-disable no-unused-expressions */ + import sinon from 'sinon'; +import {curry} from 'ladda-fp'; import {decorateUpdate} from './update'; import * as Cache from '../cache'; import {createApiFunction} from '../test-helper'; @@ -41,7 +44,7 @@ const config = [ describe('Update', () => { describe('decorateUpdate', () => { - it('Updates cache based on argument', (done) => { + it('Updates cache based on argument', () => { const cache = Cache.createCache(config); const e = config[0]; const xOrg = {id: 1, name: 'Kalle'}; @@ -49,9 +52,25 @@ describe('Update', () => { const aFn = sinon.spy(aFnWithoutSpy); const res = decorateUpdate({}, cache, curryNoop, e, aFn); - res(xOrg, 'other args').then(() => { + return res(xOrg, 'other args').then(() => { expect(Cache.getEntity(cache, e, 1).value).to.deep.equal({...xOrg, __ladda__id: 1}); - done(); + }); + }); + + it('triggers an UPDATE notification', () => { + const spy = sinon.spy(); + const n = curry((a, b, c) => spy(a, b, c)); + + const cache = Cache.createCache(config); + const e = config[0]; + const xOrg = {id: 1, name: 'Kalle'}; + const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg)); + const aFn = sinon.spy(aFnWithoutSpy); + + const res = decorateUpdate({}, cache, n, e, aFn); + return res(xOrg, 'other args').then(() => { + expect(spy).to.have.been.calledOnce; + expect(spy).to.have.been.calledWith('UPDATE', [xOrg, 'other args'], xOrg); }); }); }); From 2e0389ac4116e87e3a586788a7f0d0f28a2a5823 Mon Sep 17 00:00:00 2001 From: LFDM <1986gh@gmail.com> Date: Sun, 30 Apr 2017 09:41:21 +0200 Subject: [PATCH 12/32] Use returned promise instead of done callback --- src/plugins/cache/operations/create.spec.js | 5 ++--- src/plugins/cache/operations/delete.spec.js | 5 ++--- src/plugins/cache/operations/no-operation.spec.js | 10 ++++------ 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/plugins/cache/operations/create.spec.js b/src/plugins/cache/operations/create.spec.js index 9f89652..e8eb0a0 100644 --- a/src/plugins/cache/operations/create.spec.js +++ b/src/plugins/cache/operations/create.spec.js @@ -41,7 +41,7 @@ const config = [ describe('Create', () => { describe('decorateCreate', () => { - it('Adds value to entity store', (done) => { + it('Adds value to entity store', () => { const cache = createCache(config); const e = config[0]; const xOrg = {name: 'Kalle'}; @@ -49,10 +49,9 @@ describe('Create', () => { const aFnWithoutSpy = createApiFunction(() => Promise.resolve(response)); const aFn = sinon.spy(aFnWithoutSpy); const res = decorateCreate({}, cache, curryNoop, e, aFn); - res(xOrg).then((newX) => { + return res(xOrg).then((newX) => { expect(newX).to.equal(response); expect(getEntity(cache, e, 1).value).to.deep.equal({...response, __ladda__id: 1}); - done(); }); }); }); diff --git a/src/plugins/cache/operations/delete.spec.js b/src/plugins/cache/operations/delete.spec.js index c4c18cc..2733835 100644 --- a/src/plugins/cache/operations/delete.spec.js +++ b/src/plugins/cache/operations/delete.spec.js @@ -42,7 +42,7 @@ const config = [ describe('Delete', () => { describe('decorateDelete', () => { - it('Removes cache', (done) => { + it('Removes cache', () => { const cache = Cache.createCache(config); const e = config[0]; const xOrg = {id: 1, name: 'Kalle'}; @@ -50,9 +50,8 @@ describe('Delete', () => { const aFn = sinon.spy(aFnWithoutSpy); Cache.storeEntity(cache, e, addId({}, undefined, undefined, xOrg)); const res = decorateDelete({}, cache, curryNoop, e, aFn); - res(1).then(() => { + return res(1).then(() => { expect(Cache.getEntity(cache, e, 1)).to.equal(undefined); - done(); }); }); }); diff --git a/src/plugins/cache/operations/no-operation.spec.js b/src/plugins/cache/operations/no-operation.spec.js index 36a67d8..63ab393 100644 --- a/src/plugins/cache/operations/no-operation.spec.js +++ b/src/plugins/cache/operations/no-operation.spec.js @@ -10,7 +10,7 @@ const curryNoop = () => () => {}; const config = createSampleConfig(); describe('DecorateNoOperation', () => { - it('Invalidates based on what is specified in the original function', (done) => { + it('Invalidates based on what is specified in the original function', () => { const cache = Cache.createCache(config); const e = config[0]; const xOrg = {__ladda__id: 1, name: 'Kalle'}; @@ -19,10 +19,9 @@ describe('DecorateNoOperation', () => { aFn.invalidates = ['getUsers']; Cache.storeQueryResponse(cache, e, getUsers, ['args'], xOrg); const res = decorateNoOperation({}, cache, curryNoop, e, aFn); - res(xOrg).then(() => { + return res(xOrg).then(() => { const killedCache = !Cache.containsQueryResponse(cache, e, getUsers, ['args']); expect(killedCache).to.be.true; - done(); }); }); it('Does not change original function', () => { @@ -34,7 +33,7 @@ describe('DecorateNoOperation', () => { decorateNoOperation({}, cache, curryNoop, e, aFn); expect(aFn.operation).to.be.undefined; }); - it('Ignored inherited invalidation config', (done) => { + it('Ignored inherited invalidation config', () => { const cache = Cache.createCache(config); const e = config[0]; const xOrg = {__ladda__id: 1, name: 'Kalle'}; @@ -44,10 +43,9 @@ describe('DecorateNoOperation', () => { aFn.hasOwnProperty = () => false; Cache.storeQueryResponse(cache, e, getUsers, ['args'], xOrg); const res = decorateNoOperation({}, cache, curryNoop, e, aFn); - res(xOrg).then(() => { + return res(xOrg).then(() => { const killedCache = !Cache.containsQueryResponse(cache, e, getUsers, ['args']); expect(killedCache).to.be.false; - done(); }); }); }); From b42526d4aab3edd865aa73f08170d2dd7d3a5b23 Mon Sep 17 00:00:00 2001 From: LFDM <1986gh@gmail.com> Date: Sun, 30 Apr 2017 09:53:55 +0200 Subject: [PATCH 13/32] Add notification specs for DELETE --- src/plugins/cache/entity-store.js | 6 +++- src/plugins/cache/operations/delete.js | 7 ++++- src/plugins/cache/operations/delete.spec.js | 34 +++++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/plugins/cache/entity-store.js b/src/plugins/cache/entity-store.js index 0558c9d..0e53e5b 100644 --- a/src/plugins/cache/entity-store.js +++ b/src/plugins/cache/entity-store.js @@ -13,6 +13,7 @@ import {curry, reduce, map_, clone} from 'ladda-fp'; import {merge} from './merger'; +import {removeId} from './id-helper'; // Value -> StoreValue const toStoreValue = v => ({value: v, timestamp: Date.now()}); @@ -122,7 +123,10 @@ export const remove = (es, e, id) => { const x = get(es, e, id); rm(es, createEntityKey(e, {__ladda__id: id})); rmViews(es, e); - return x; + if (x) { + return removeId(x.value); + } + return undefined; }; // EntityStore -> Entity -> String -> Bool diff --git a/src/plugins/cache/operations/delete.js b/src/plugins/cache/operations/delete.js index 5df9983..4689520 100644 --- a/src/plugins/cache/operations/delete.js +++ b/src/plugins/cache/operations/delete.js @@ -6,6 +6,11 @@ export function decorateDelete(c, cache, notify, e, aFn) { return (...args) => { return aFn(...args) .then(passThrough(() => Cache.invalidateQuery(cache, e, aFn))) - .then(() => Cache.removeEntity(cache, e, serialize(args))); + .then(() => { + const removed = Cache.removeEntity(cache, e, serialize(args)); + if (removed) { + notify('DELETE', args, removed); + } + }); }; } diff --git a/src/plugins/cache/operations/delete.spec.js b/src/plugins/cache/operations/delete.spec.js index 2733835..2982b1a 100644 --- a/src/plugins/cache/operations/delete.spec.js +++ b/src/plugins/cache/operations/delete.spec.js @@ -1,4 +1,7 @@ +/* eslint-disable no-unused-expressions */ + import sinon from 'sinon'; +import {curry} from 'ladda-fp'; import {decorateDelete} from './delete'; import * as Cache from '../cache'; import {addId} from '../id-helper'; @@ -54,5 +57,36 @@ describe('Delete', () => { expect(Cache.getEntity(cache, e, 1)).to.equal(undefined); }); }); + + it('triggers DELETE notification', () => { + const spy = sinon.spy(); + const n = curry((a, b, c) => spy(a, b, c)); + const cache = Cache.createCache(config); + const e = config[0]; + const xOrg = {id: 1, name: 'Kalle'}; + const aFnWithoutSpy = createApiFunction(() => Promise.resolve({})); + const aFn = sinon.spy(aFnWithoutSpy); + Cache.storeEntity(cache, e, addId({}, undefined, undefined, xOrg)); + const res = decorateDelete({}, cache, n, e, aFn); + return res(1).then(() => { + expect(spy).to.have.been.calledOnce; + expect(spy).to.have.been.calledWith('DELETE', [1], xOrg); + }); + }); + + it('does not trigger notification when item was not in cache', () => { + const spy = sinon.spy(); + const n = curry((a, b, c) => spy(a, b, c)); + const cache = Cache.createCache(config); + const e = config[0]; + const xOrg = {id: 1, name: 'Kalle'}; + const aFnWithoutSpy = createApiFunction(() => Promise.resolve({})); + const aFn = sinon.spy(aFnWithoutSpy); + Cache.storeEntity(cache, e, addId({}, undefined, undefined, xOrg)); + const res = decorateDelete({}, cache, n, e, aFn); + return res(2).then(() => { + expect(spy).not.to.have.been.called; + }); + }); }); }); From 08b321d7abb352b45d903e4596736e5bcef7a219 Mon Sep 17 00:00:00 2001 From: LFDM <1986gh@gmail.com> Date: Sun, 30 Apr 2017 09:54:06 +0200 Subject: [PATCH 14/32] Add notification specs for NO_OPERATION --- .../cache/operations/no-operation.spec.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/plugins/cache/operations/no-operation.spec.js b/src/plugins/cache/operations/no-operation.spec.js index 63ab393..3a9e960 100644 --- a/src/plugins/cache/operations/no-operation.spec.js +++ b/src/plugins/cache/operations/no-operation.spec.js @@ -24,6 +24,7 @@ describe('DecorateNoOperation', () => { expect(killedCache).to.be.true; }); }); + it('Does not change original function', () => { const cache = Cache.createCache(config); const e = config[0]; @@ -33,6 +34,7 @@ describe('DecorateNoOperation', () => { decorateNoOperation({}, cache, curryNoop, e, aFn); expect(aFn.operation).to.be.undefined; }); + it('Ignored inherited invalidation config', () => { const cache = Cache.createCache(config); const e = config[0]; @@ -48,4 +50,19 @@ describe('DecorateNoOperation', () => { expect(killedCache).to.be.false; }); }); + + it('does not trigger notify', () => { + const spy = sinon.spy(); + const cache = Cache.createCache(config); + const e = config[0]; + const xOrg = {__ladda__id: 1, name: 'Kalle'}; + const aFn = sinon.spy(() => Promise.resolve({})); + const getUsers = () => Promise.resolve(xOrg); + aFn.invalidates = ['getUsers']; + Cache.storeQueryResponse(cache, e, getUsers, ['args'], xOrg); + const res = decorateNoOperation({}, cache, spy, e, aFn); + return res(xOrg).then(() => { + expect(spy).not.to.have.been.called; + }); + }); }); From b2e3caff0542346f957ba82d08e7dbdaaa5dd01d Mon Sep 17 00:00:00 2001 From: LFDM <1986gh@gmail.com> Date: Sun, 30 Apr 2017 10:00:27 +0200 Subject: [PATCH 15/32] Add notification on CREATE --- src/plugins/cache/operations/create.js | 3 ++- src/plugins/cache/operations/create.spec.js | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/plugins/cache/operations/create.js b/src/plugins/cache/operations/create.js index e153826..69daaa7 100644 --- a/src/plugins/cache/operations/create.js +++ b/src/plugins/cache/operations/create.js @@ -6,6 +6,7 @@ export function decorateCreate(c, cache, notify, e, aFn) { return (...args) => { return aFn(...args) .then(passThrough(() => invalidateQuery(cache, e, aFn))) - .then(passThrough(compose(storeEntity(cache, e), addId(c, aFn, args)))); + .then(passThrough(compose(storeEntity(cache, e), addId(c, aFn, args)))) + .then(passThrough(notify('CREATE', args))); }; } diff --git a/src/plugins/cache/operations/create.spec.js b/src/plugins/cache/operations/create.spec.js index e8eb0a0..bca2f1c 100644 --- a/src/plugins/cache/operations/create.spec.js +++ b/src/plugins/cache/operations/create.spec.js @@ -1,4 +1,7 @@ +/* eslint-disable no-unused-expressions */ + import sinon from 'sinon'; +import {curry} from 'ladda-fp'; import {decorateCreate} from './create'; import {createCache, getEntity} from '../cache'; import {createApiFunction} from '../test-helper'; @@ -54,5 +57,21 @@ describe('Create', () => { expect(getEntity(cache, e, 1).value).to.deep.equal({...response, __ladda__id: 1}); }); }); + + it('triggers a CREATE notification', () => { + const spy = sinon.spy(); + const n = curry((a, b, c) => spy(a, b, c)); + const cache = createCache(config); + const e = config[0]; + const xOrg = {name: 'Kalle'}; + const response = {...xOrg, id: 1}; + const aFnWithoutSpy = createApiFunction(() => Promise.resolve(response)); + const aFn = sinon.spy(aFnWithoutSpy); + const res = decorateCreate({}, cache, n, e, aFn); + return res(xOrg).then((newX) => { + expect(spy).to.have.been.calledOnce; + expect(spy).to.have.been.calledWith('CREATE', [xOrg], newX); + }); + }); }); }); From 5fbcb78b0719544935a6ed80e87dafa98003655a Mon Sep 17 00:00:00 2001 From: LFDM <1986gh@gmail.com> Date: Sun, 30 Apr 2017 10:09:34 +0200 Subject: [PATCH 16/32] Remove changeObject type, rely on operation --- src/builder.spec.js | 2 +- src/plugins/cache/index.js | 4 ++-- src/plugins/cache/operations/create.js | 2 +- src/plugins/cache/operations/create.spec.js | 4 ++-- src/plugins/cache/operations/delete.js | 2 +- src/plugins/cache/operations/delete.spec.js | 6 +++--- src/plugins/cache/operations/read.js | 6 +++--- src/plugins/cache/operations/read.spec.js | 22 ++++++++++----------- src/plugins/cache/operations/update.js | 2 +- src/plugins/cache/operations/update.spec.js | 4 ++-- 10 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/builder.spec.js b/src/builder.spec.js index 867987b..f321004 100644 --- a/src/builder.spec.js +++ b/src/builder.spec.js @@ -195,7 +195,7 @@ describe('builder', () => { const changeObject = spy.args[0][0]; expect(changeObject.entity).to.equal('user'); expect(changeObject.apiFn).to.equal('getUsers'); - expect(changeObject.type).to.equal('CREATE'); + expect(changeObject.operation).to.equal('READ'); expect(changeObject.values).to.deep.equal(users); expect(changeObject.args).to.deep.equal([]); }); diff --git a/src/plugins/cache/index.js b/src/plugins/cache/index.js index 70e8bdc..88733fb 100644 --- a/src/plugins/cache/index.js +++ b/src/plugins/cache/index.js @@ -16,9 +16,9 @@ const HANDLERS = { const normalizeFnName = (fnName) => fnName.replace(/^bound /, ''); -const notify = curry((onChange, entity, fn, changeType, args, payload) => { +const notify = curry((onChange, entity, fn, args, payload) => { onChange({ - type: changeType, + operation: fn.operation, entity: entity.name, apiFn: normalizeFnName(fn.name), values: Array.isArray(payload) ? payload : [payload], diff --git a/src/plugins/cache/operations/create.js b/src/plugins/cache/operations/create.js index 69daaa7..27a0867 100644 --- a/src/plugins/cache/operations/create.js +++ b/src/plugins/cache/operations/create.js @@ -7,6 +7,6 @@ export function decorateCreate(c, cache, notify, e, aFn) { return aFn(...args) .then(passThrough(() => invalidateQuery(cache, e, aFn))) .then(passThrough(compose(storeEntity(cache, e), addId(c, aFn, args)))) - .then(passThrough(notify('CREATE', args))); + .then(passThrough(notify(args))); }; } diff --git a/src/plugins/cache/operations/create.spec.js b/src/plugins/cache/operations/create.spec.js index bca2f1c..3ba9368 100644 --- a/src/plugins/cache/operations/create.spec.js +++ b/src/plugins/cache/operations/create.spec.js @@ -60,7 +60,7 @@ describe('Create', () => { it('triggers a CREATE notification', () => { const spy = sinon.spy(); - const n = curry((a, b, c) => spy(a, b, c)); + const n = curry((a, b) => spy(a, b)); const cache = createCache(config); const e = config[0]; const xOrg = {name: 'Kalle'}; @@ -70,7 +70,7 @@ describe('Create', () => { const res = decorateCreate({}, cache, n, e, aFn); return res(xOrg).then((newX) => { expect(spy).to.have.been.calledOnce; - expect(spy).to.have.been.calledWith('CREATE', [xOrg], newX); + expect(spy).to.have.been.calledWith([xOrg], newX); }); }); }); diff --git a/src/plugins/cache/operations/delete.js b/src/plugins/cache/operations/delete.js index 4689520..3565189 100644 --- a/src/plugins/cache/operations/delete.js +++ b/src/plugins/cache/operations/delete.js @@ -9,7 +9,7 @@ export function decorateDelete(c, cache, notify, e, aFn) { .then(() => { const removed = Cache.removeEntity(cache, e, serialize(args)); if (removed) { - notify('DELETE', args, removed); + notify(args, removed); } }); }; diff --git a/src/plugins/cache/operations/delete.spec.js b/src/plugins/cache/operations/delete.spec.js index 2982b1a..5f4bb01 100644 --- a/src/plugins/cache/operations/delete.spec.js +++ b/src/plugins/cache/operations/delete.spec.js @@ -60,7 +60,7 @@ describe('Delete', () => { it('triggers DELETE notification', () => { const spy = sinon.spy(); - const n = curry((a, b, c) => spy(a, b, c)); + const n = curry((a, b) => spy(a, b)); const cache = Cache.createCache(config); const e = config[0]; const xOrg = {id: 1, name: 'Kalle'}; @@ -70,13 +70,13 @@ describe('Delete', () => { const res = decorateDelete({}, cache, n, e, aFn); return res(1).then(() => { expect(spy).to.have.been.calledOnce; - expect(spy).to.have.been.calledWith('DELETE', [1], xOrg); + expect(spy).to.have.been.calledWith([1], xOrg); }); }); it('does not trigger notification when item was not in cache', () => { const spy = sinon.spy(); - const n = curry((a, b, c) => spy(a, b, c)); + const n = curry((a, b) => spy(a, b)); const cache = Cache.createCache(config); const e = config[0]; const xOrg = {id: 1, name: 'Kalle'}; diff --git a/src/plugins/cache/operations/read.js b/src/plugins/cache/operations/read.js index f758807..fab9fa8 100644 --- a/src/plugins/cache/operations/read.js +++ b/src/plugins/cache/operations/read.js @@ -29,7 +29,7 @@ const decorateReadSingle = (c, cache, notify, e, aFn) => { return aFn(id) .then(passThrough(compose(Cache.storeEntity(cache, e), addId(c, aFn, id)))) .then(passThrough(() => Cache.invalidateQuery(cache, e, aFn))) - .then(passThrough(notify('CREATE', [id]))); + .then(passThrough(notify([id]))); }; }; @@ -59,7 +59,7 @@ const decorateReadSome = (c, cache, notify, e, aFn) => { const asMap = compose(toIdMap, concat)(cached, other); return map((id) => asMap[id], ids); }) - .then(passThrough(notify('CREATE', [remaining]))); + .then(passThrough(notify([remaining]))); }; }; @@ -77,7 +77,7 @@ const decorateReadQuery = (c, cache, notify, e, aFn) => { compose(Cache.storeQueryResponse(cache, e, aFn, args), addId(c, aFn, args)))) .then(passThrough(() => Cache.invalidateQuery(cache, e, aFn))) - .then(passThrough(notify('CREATE', args))); + .then(passThrough(notify(args))); }; }; diff --git a/src/plugins/cache/operations/read.spec.js b/src/plugins/cache/operations/read.spec.js index 21a3df4..9d057a1 100644 --- a/src/plugins/cache/operations/read.spec.js +++ b/src/plugins/cache/operations/read.spec.js @@ -77,7 +77,7 @@ describe('Read', () => { it('triggers notify when not in cache', () => { const spy = sinon.spy(); - const n = curry((a, b, c) => spy(a, b, c)); + const n = curry((a, b) => spy(a, b)); const cache = createCache(config); const e = config[0]; const xOrg = {id: 1, name: 'Kalle'}; @@ -85,13 +85,13 @@ describe('Read', () => { const aFn = sinon.spy(aFnWithoutSpy); const res = decorateRead({}, cache, n, e, aFn); return res(1).then((r) => { - expect(spy).to.have.been.calledWith('CREATE', [1], r); + expect(spy).to.have.been.calledWith([1], r); }); }); it('does not trigger notify when in cache', () => { const spy = sinon.spy(); - const n = curry((a, b, c) => spy(a, b, c)); + const n = curry((a, b) => spy(a, b)); const cache = createCache(config); const e = config[0]; const xOrg = {id: 1, name: 'Kalle'}; @@ -176,20 +176,20 @@ describe('Read', () => { it('triggers notify when not in cache', () => { const spy = sinon.spy(); - const n = curry((a, b, c) => spy(a, b, c)); + const n = curry((a, b) => spy(a, b)); const cache = createCache(config); const e = config[0]; const fnWithSpy = sinon.spy(decoratedFn); const apiFn = decorateRead({}, cache, n, e, fnWithSpy); return apiFn(['a', 'b']).then((r) => { expect(spy).to.have.been.calledOnce; - expect(spy).to.have.been.calledWith('CREATE', [['a', 'b']], r); + expect(spy).to.have.been.calledWith([['a', 'b']], r); }); }); it('triggers notify when not in cache for partial request', () => { const spy = sinon.spy(); - const n = curry((a, b, c) => spy(a, b, c)); + const n = curry((a, b) => spy(a, b)); const cache = createCache(config); const e = config[0]; const fnWithSpy = sinon.spy(decoratedFn); @@ -198,14 +198,14 @@ describe('Read', () => { spy.reset(); return apiFn(['a', 'b', 'c']).then((r) => { expect(spy).to.have.been.calledOnce; - expect(spy).to.have.been.calledWith('CREATE', [['c']], r); + expect(spy).to.have.been.calledWith([['c']], r); }); }); }); it('does not trigger notify when in cache', () => { const spy = sinon.spy(); - const n = curry((a, b, c) => spy(a, b, c)); + const n = curry((a, b) => spy(a, b)); const cache = createCache(config); const e = config[0]; const fnWithSpy = sinon.spy(decoratedFn); @@ -326,7 +326,7 @@ describe('Read', () => { it('triggers notify when not in cache', () => { const spy = sinon.spy(); - const n = curry((a, b, c) => spy(a, b, c)); + const n = curry((a, b) => spy(a, b)); const cache = createCache(config); const e = config[0]; const xOrg = [{id: 1, name: 'Kalle'}]; @@ -336,13 +336,13 @@ describe('Read', () => { return res(1).then((r) => { expect(spy).to.have.been.calledOnce; - expect(spy).to.have.been.calledWith('CREATE', [1], r); + expect(spy).to.have.been.calledWith([1], r); }); }); it('does not trigger notify when in cache', () => { const spy = sinon.spy(); - const n = curry((a, b, c) => spy(a, b, c)); + const n = curry((a, b) => spy(a, b)); const cache = createCache(config); const e = config[0]; const xOrg = [{id: 1, name: 'Kalle'}]; diff --git a/src/plugins/cache/operations/update.js b/src/plugins/cache/operations/update.js index 3c6d78d..af391da 100644 --- a/src/plugins/cache/operations/update.js +++ b/src/plugins/cache/operations/update.js @@ -7,6 +7,6 @@ export function decorateUpdate(c, cache, notify, e, aFn) { return aFn(eValue, ...args) .then(passThrough(() => Cache.invalidateQuery(cache, e, aFn))) .then(passThrough(() => Cache.storeEntity(cache, e, addId(c, undefined, undefined, eValue)))) - .then(passThrough(() => notify('UPDATE', [eValue, ...args], eValue))); + .then(passThrough(() => notify([eValue, ...args], eValue))); }; } diff --git a/src/plugins/cache/operations/update.spec.js b/src/plugins/cache/operations/update.spec.js index ec2a103..506309a 100644 --- a/src/plugins/cache/operations/update.spec.js +++ b/src/plugins/cache/operations/update.spec.js @@ -59,7 +59,7 @@ describe('Update', () => { it('triggers an UPDATE notification', () => { const spy = sinon.spy(); - const n = curry((a, b, c) => spy(a, b, c)); + const n = curry((a, b) => spy(a, b)); const cache = Cache.createCache(config); const e = config[0]; @@ -70,7 +70,7 @@ describe('Update', () => { const res = decorateUpdate({}, cache, n, e, aFn); return res(xOrg, 'other args').then(() => { expect(spy).to.have.been.calledOnce; - expect(spy).to.have.been.calledWith('UPDATE', [xOrg, 'other args'], xOrg); + expect(spy).to.have.been.calledWith([xOrg, 'other args'], xOrg); }); }); }); From a3024b56e12f12587f0a4b8bf1fca3865992f287 Mon Sep 17 00:00:00 2001 From: LFDM <1986gh@gmail.com> Date: Sun, 30 Apr 2017 10:21:42 +0200 Subject: [PATCH 17/32] Update documentation of change object --- docs/advanced/Plugins.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/advanced/Plugins.md b/docs/advanced/Plugins.md index 7af4223..51b6dd9 100644 --- a/docs/advanced/Plugins.md +++ b/docs/advanced/Plugins.md @@ -126,16 +126,18 @@ following shape: ```javascript { - type: 'UPDATE' | 'REMOVE', + operation: 'CREATE' | 'READ' | 'UPDATE' | 'DELETE', entity: EntityName, - entities: EntityValue[] + apiFn: ApiFunctionName, + args: Any[] + values: EntityValue[], } ``` -At this point in time there is no difference made between adding new -EntityValues and updating already present ones: Both events lead to a -change of the type `UPDATE`. -The `entities` field is guaranteed to be a list of EntityValues, even if +It provides all information about which call triggered a change, +including the arguments array. + +The `values` field is guaranteed to be a list of EntityValues, even if a change only affects a single entity. `addChangeListener` returns a deregistration function. Call it to stop From e95859c948d1701cb91e7951ac6f89d5ad39372a Mon Sep 17 00:00:00 2001 From: LFDM <1986gh@gmail.com> Date: Sun, 30 Apr 2017 10:26:01 +0200 Subject: [PATCH 18/32] Update version for testing --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 20e530c..fbfec3d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ladda-cache", - "version": "0.2.1", + "version": "0.2.3", "description": "Data fetching layer with support for caching", "main": "dist/bundle.js", "dependencies": { From 0d45fc24d623c867b2437394fd63e8bda8652f2c Mon Sep 17 00:00:00 2001 From: LFDM <1986gh@gmail.com> Date: Sun, 30 Apr 2017 10:48:05 +0200 Subject: [PATCH 19/32] Trigger notification on NO_OPERTATION --- src/plugins/cache/index.js | 8 +++++++- src/plugins/cache/operations/no-operation.js | 3 ++- src/plugins/cache/operations/no-operation.spec.js | 9 ++++++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/plugins/cache/index.js b/src/plugins/cache/index.js index 88733fb..b7545e8 100644 --- a/src/plugins/cache/index.js +++ b/src/plugins/cache/index.js @@ -15,13 +15,19 @@ const HANDLERS = { }; const normalizeFnName = (fnName) => fnName.replace(/^bound /, ''); +const normalizePayload = payload => { + if (payload === null) { + return payload; + } + return Array.isArray(payload) ? payload : [payload]; +}; const notify = curry((onChange, entity, fn, args, payload) => { onChange({ operation: fn.operation, entity: entity.name, apiFn: normalizeFnName(fn.name), - values: Array.isArray(payload) ? payload : [payload], + values: normalizePayload(payload), args }); }); diff --git a/src/plugins/cache/operations/no-operation.js b/src/plugins/cache/operations/no-operation.js index e482c44..b562770 100644 --- a/src/plugins/cache/operations/no-operation.js +++ b/src/plugins/cache/operations/no-operation.js @@ -4,6 +4,7 @@ import {invalidateQuery} from '../cache'; export function decorateNoOperation(c, cache, notify, e, aFn) { return (...args) => { return aFn(...args) - .then(passThrough(() => invalidateQuery(cache, e, aFn))); + .then(passThrough(() => invalidateQuery(cache, e, aFn))) + .then(passThrough(() => notify(args, null))); }; } diff --git a/src/plugins/cache/operations/no-operation.spec.js b/src/plugins/cache/operations/no-operation.spec.js index 3a9e960..c11ab7e 100644 --- a/src/plugins/cache/operations/no-operation.spec.js +++ b/src/plugins/cache/operations/no-operation.spec.js @@ -1,6 +1,7 @@ /* eslint-disable no-unused-expressions */ import sinon from 'sinon'; +import {curry} from 'ladda-fp'; import {decorateNoOperation} from './no-operation'; import * as Cache from '../cache'; import {createSampleConfig, createApiFunction} from '../test-helper'; @@ -51,8 +52,9 @@ describe('DecorateNoOperation', () => { }); }); - it('does not trigger notify', () => { + it('trigger notification', () => { const spy = sinon.spy(); + const n = curry((a, b) => spy(a, b)); const cache = Cache.createCache(config); const e = config[0]; const xOrg = {__ladda__id: 1, name: 'Kalle'}; @@ -60,9 +62,10 @@ describe('DecorateNoOperation', () => { const getUsers = () => Promise.resolve(xOrg); aFn.invalidates = ['getUsers']; Cache.storeQueryResponse(cache, e, getUsers, ['args'], xOrg); - const res = decorateNoOperation({}, cache, spy, e, aFn); + const res = decorateNoOperation({}, cache, n, e, aFn); return res(xOrg).then(() => { - expect(spy).not.to.have.been.called; + expect(spy).to.have.been.calledOnce; + expect(spy).to.have.been.calledWith([xOrg], null); }); }); }); From 27a2aab86ed8609a4818b99adc083c6c5e40b631 Mon Sep 17 00:00:00 2001 From: LFDM <1986gh@gmail.com> Date: Sun, 30 Apr 2017 10:48:59 +0200 Subject: [PATCH 20/32] Update docs again --- docs/advanced/Plugins.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/advanced/Plugins.md b/docs/advanced/Plugins.md index 51b6dd9..adad3d5 100644 --- a/docs/advanced/Plugins.md +++ b/docs/advanced/Plugins.md @@ -126,10 +126,10 @@ following shape: ```javascript { - operation: 'CREATE' | 'READ' | 'UPDATE' | 'DELETE', + operation: 'CREATE' | 'READ' | 'UPDATE' | 'DELETE' | 'NO_OPERATION', entity: EntityName, apiFn: ApiFunctionName, - args: Any[] + args: Any[] | null values: EntityValue[], } ``` @@ -138,7 +138,8 @@ It provides all information about which call triggered a change, including the arguments array. The `values` field is guaranteed to be a list of EntityValues, even if -a change only affects a single entity. +a change only affects a single entity. The only expection are +`NO_OPERATION` operations, which will always return `null` here. `addChangeListener` returns a deregistration function. Call it to stop listening for changes. From bed673d4626fb15d32edc0e14cf80625f3ee9bc9 Mon Sep 17 00:00:00 2001 From: LFDM <1986gh@gmail.com> Date: Sun, 30 Apr 2017 11:13:14 +0200 Subject: [PATCH 21/32] Add spec for addChangeListener deregistration --- src/builder.spec.js | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/builder.spec.js b/src/builder.spec.js index f321004..8898e27 100644 --- a/src/builder.spec.js +++ b/src/builder.spec.js @@ -11,12 +11,16 @@ getUsers.operation = 'READ'; const deleteUser = () => Promise.resolve(); deleteUser.operation = 'DELETE'; +const noopUser = () => Promise.resolve(['a', 'b']); +noopUser.operation = 'NO_OPERATION'; + const config = () => ({ user: { ttl: 300, api: { getUsers, - deleteUser + deleteUser, + noopUser }, invalidates: ['alles'] }, @@ -179,6 +183,22 @@ describe('builder', () => { build(config(), [plugin]); }); + it('returns a deregistration fn', () => { + const spy = sinon.spy(); + + const plugin = ({ addChangeListener }) => { + const deregister = addChangeListener(spy); + deregister(); + return ({ fn }) => fn; + }; + + const api = build(config(), [plugin]); + + return api.user.getUsers().then(() => { + expect(spy).not.to.have.been.called; + }); + }); + describe('allows plugins to add a listener, which gets notified on all cache changes', () => { it('on READ operations', () => { const spy = sinon.spy(); @@ -200,6 +220,27 @@ describe('builder', () => { expect(changeObject.args).to.deep.equal([]); }); }); + + it('on NO_OPERATION operations', () => { + const spy = sinon.spy(); + + const plugin = ({ addChangeListener }) => { + addChangeListener(spy); + return ({ fn }) => fn; + }; + + const api = build(config(), [plugin]); + + return api.user.noopUser('x').then(() => { + expect(spy).to.have.been.calledOnce; + const changeObject = spy.args[0][0]; + expect(changeObject.entity).to.equal('user'); + expect(changeObject.apiFn).to.equal('noopUser'); + expect(changeObject.operation).to.equal('NO_OPERATION'); + expect(changeObject.values).to.deep.equal(null); + expect(changeObject.args).to.deep.equal(['x']); + }); + }); }); it('does not trigger when a pure cache hit is made', () => { From aa7d9e36e50b23a5d9eb3b5a0344bd7acc42b955 Mon Sep 17 00:00:00 2001 From: LFDM <1986gh@gmail.com> Date: Sun, 30 Apr 2017 18:12:50 +0200 Subject: [PATCH 22/32] More branches to test --- src/builder.spec.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/builder.spec.js b/src/builder.spec.js index 8898e27..1628fdd 100644 --- a/src/builder.spec.js +++ b/src/builder.spec.js @@ -199,6 +199,24 @@ describe('builder', () => { }); }); + it('can call deregistration fn several times without harm', () => { + const spy = sinon.spy(); + + const plugin = ({ addChangeListener }) => { + const deregister = addChangeListener(spy); + deregister(); + deregister(); + deregister(); + return ({ fn }) => fn; + }; + + const api = build(config(), [plugin]); + + return api.user.getUsers().then(() => { + expect(spy).not.to.have.been.called; + }); + }); + describe('allows plugins to add a listener, which gets notified on all cache changes', () => { it('on READ operations', () => { const spy = sinon.spy(); From 02a07a4c38426044ac0525c85ddb3338d3b8a0c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timur=20=C3=87elikel?= Date: Mon, 15 May 2017 11:46:46 +0200 Subject: [PATCH 23/32] Update package.json: new homepage URL --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fbfec3d..e07526b 100644 --- a/package.json +++ b/package.json @@ -43,5 +43,5 @@ "type": "git", "url": "https://github.com/ladda-js/ladda.git" }, - "homepage": "https://github.com/ladda-js/ladda" + "homepage": "https://www.ladda.io/" } From 78011979e9a66cda1da03dafe9131e5b8a562590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timur=20=C3=87elikel?= Date: Mon, 15 May 2017 11:57:08 +0200 Subject: [PATCH 24/32] Update readme: redirected links to new pages --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 58cf5b1..3f3e5dc 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,11 @@ The easiest way to get a glimpse of what Ladda can do is checking out our [demos # Get Started -Check out the [guide](/docs/GettingStarted.md) for getting started. In addition, you can have a look in the [examples folder](https://github.com/petercrona/ladda/tree/master/examples). These are standalone examples where you only need to follow the README.md to setup the project. There is an addtional minimal example, where you can find everything in one file, that you can clone and run: Check out [ladda-example-mini-project](https://github.com/petercrona/ladda-example-mini-project) ([code](https://github.com/petercrona/ladda-example-mini-project/blob/master/script.js)). +Check out the [guide](/docs/GettingStarted.md) for getting started. In addition, you can have a look in the [examples folder](https://github.com/ladda-js/ladda/tree/master/examples). These are standalone examples where you only need to follow the README.md to setup the project. There is an addtional minimal example, where you can find everything in one file, that you can clone and run: Check out [ladda-example-mini-project](https://github.com/petercrona/ladda-example-mini-project) ([code](https://github.com/petercrona/ladda-example-mini-project/blob/master/script.js)). # Documentation -The documentation gives you an [exhaustive overview of Ladda](https://petercrona.gitbooks.io/ladda/content/). +The documentation gives you an [exhaustive overview of Ladda](https://www.ladda.io/). # Why Use Ladda? @@ -35,7 +35,7 @@ Ladda is a lightweight library and comes with no additional dependencies. The li ## Quality -Ladda has a high test coverage (**100%** line coverage) with tests constantly being added. And yes, we know that high test coverage is a "feel good" number, our focus is still on meaningful and good tests. It has a reasonably simple architecture and often tries to stay [tacit](https://www.youtube.com/watch?v=seVSlKazsNk&feature=youtu.be) and concise by taking inspiration from [functional programming](https://drboolean.gitbooks.io/mostly-adequate-guide/content/). We urge you to check out the [source code](https://github.com/petercrona/ladda/tree/master/src). You can help us to improve it further or just enjoy reading functional JavaScript. +Ladda has a high test coverage (**100%** line coverage) with tests constantly being added. And yes, we know that high test coverage is a "feel good" number, our focus is still on meaningful and good tests. It has a reasonably simple architecture and often tries to stay [tacit](https://www.youtube.com/watch?v=seVSlKazsNk&feature=youtu.be) and concise by taking inspiration from [functional programming](https://drboolean.gitbooks.io/mostly-adequate-guide/content/). We urge you to check out the [source code](https://github.com/ladda-js/ladda/tree/master/src). You can help us to improve it further or just enjoy reading functional JavaScript. ## Standalone From 361d9e2ff3b73db1146a7517f23c4ba0f8866bd9 Mon Sep 17 00:00:00 2001 From: Peter Crona Date: Fri, 26 May 2017 15:40:20 +0200 Subject: [PATCH 25/32] remove bound prefix from functions when creating keys --- src/plugins/cache/query-cache.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/plugins/cache/query-cache.js b/src/plugins/cache/query-cache.js index 90c7345..6391335 100644 --- a/src/plugins/cache/query-cache.js +++ b/src/plugins/cache/query-cache.js @@ -33,9 +33,11 @@ const getFromCache = (qc, e, k) => { }; }; +const stripBound = (fnName) => fnName.replace(/^bound /, ''); + // QueryCache -> Entity -> ApiFunction -> [a] -> [b] -> [b] export const put = curry((qc, e, aFn, args, xs) => { - const k = createKey(e, [aFn.name, ...filter(identity, args)]); + const k = createKey(e, [stripBound(aFn.name), ...filter(identity, args)]); if (Array.isArray(xs)) { qc.cache[k] = toCacheValue(map(prop('__ladda__id'), xs)); } else { @@ -52,13 +54,13 @@ export const getValue = (v) => { // QueryCache -> Entity -> ApiFunction -> [a] -> Bool export const contains = (qc, e, aFn, args) => { - const k = createKey(e, [aFn.name, ...filter(identity, args)]); + const k = createKey(e, [stripBound(aFn.name), ...filter(identity, args)]); return inCache(qc, k); }; // QueryCache -> Entity -> ApiFunction -> [a] -> Bool export const get = (qc, e, aFn, args) => { - const k = createKey(e, [aFn.name, ...filter(identity, args)]); + const k = createKey(e, [stripBound(aFn.name), ...filter(identity, args)]); if (!inCache(qc, k)) { throw new Error( `Tried to access ${e.name} with key ${k} which doesn't exist. From d36f87b531dfe28eecf6a750b3fc935b14dd1fee Mon Sep 17 00:00:00 2001 From: Peter Crona Date: Fri, 26 May 2017 15:54:04 +0200 Subject: [PATCH 26/32] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e07526b..b9721a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ladda-cache", - "version": "0.2.3", + "version": "0.2.4", "description": "Data fetching layer with support for caching", "main": "dist/bundle.js", "dependencies": { From ac050478e3a75a435d8d19b146f7b4fabcd42c30 Mon Sep 17 00:00:00 2001 From: Peter Crona Date: Fri, 26 May 2017 15:57:24 +0200 Subject: [PATCH 27/32] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b9721a1..7b98272 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ladda-cache", - "version": "0.2.4", + "version": "0.2.5", "description": "Data fetching layer with support for caching", "main": "dist/bundle.js", "dependencies": { From 82bd692dc067a8ae16283f666d655bfb205948ce Mon Sep 17 00:00:00 2001 From: LFDM <1986gh@gmail.com> Date: Mon, 29 May 2017 10:58:15 +0200 Subject: [PATCH 28/32] Make sure we are allowed to write to the name property of our apiFns --- package.json | 2 +- src/builder.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 7b98272..125b51f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ladda-cache", - "version": "0.2.5", + "version": "0.2.6", "description": "Data fetching layer with support for caching", "main": "dist/bundle.js", "dependencies": { diff --git a/src/builder.js b/src/builder.js index 6312222..e9f7002 100644 --- a/src/builder.js +++ b/src/builder.js @@ -23,7 +23,7 @@ const KNOWN_STATICS = { }; const setFnName = curry((name, fn) => { - Object.defineProperty(fn, 'name', { writable: true }); + Object.defineProperty(fn, 'name', { writable: true, configurable: true }); fn.name = name; Object.defineProperty(fn, 'name', { writable: false }); return fn; From c02502c69a430a301e2530f0f1c7330227dca3fa Mon Sep 17 00:00:00 2001 From: LFDM <1986gh@gmail.com> Date: Mon, 29 May 2017 14:18:50 +0200 Subject: [PATCH 29/32] Do NOT rely on the name property of functions, choose own property fnName instead --- docs/advanced/Plugins.md | 14 ++++----- package.json | 2 +- src/builder.js | 30 ++++++++++--------- src/builder.spec.js | 2 +- src/plugins/cache/index.js | 3 +- .../cache/operations/no-operation.spec.js | 1 + src/plugins/cache/query-cache.js | 8 ++--- 7 files changed, 30 insertions(+), 30 deletions(-) diff --git a/docs/advanced/Plugins.md b/docs/advanced/Plugins.md index adad3d5..74ee63b 100644 --- a/docs/advanced/Plugins.md +++ b/docs/advanced/Plugins.md @@ -231,14 +231,14 @@ export const logger = (pluginConfig = {}) => { return ({ entity, fn }) => { return (...args) => { - console.log(`Ladda: Calling ${entity.name}.${fn.name} with args`, args); + console.log(`Ladda: Calling ${entity.name}.${fn.fnName} with args`, args); return fn(...args).then( (res) => { - console.log(`Ladda: Resolved ${entity.name}.${fn.name} with`, res); + console.log(`Ladda: Resolved ${entity.name}.${fn.fnName} with`, res); return res; }, (err) => { - console.log(`Ladda: Rejected ${entity.name}.${fn.name} with`, err) + console.log(`Ladda: Rejected ${entity.name}.${fn.fnName} with`, err) return Promise.reject(err); } ); @@ -252,7 +252,7 @@ We issue a first log statement immediately when the function is invoked and print out the arguments we received. By using the entity configuration we got passed in and the meta data of the ApiFunction we can produce a nice string to reveal which function just got called: -`${entity.name}.${fn.name}`. This could for example produce +`${entity.name}.${fn.fnName}`. This could for example produce something like `user.getAll`. We then use Promise chaining to intercept the result of our original @@ -285,14 +285,14 @@ export const logger = (pluginConfig = {}) => { return ({ entity, fn }) => { return (...args) => { - console.log(`Ladda: Calling ${entity.name}.${fn.name} with args`, args); + console.log(`Ladda: Calling ${entity.name}.${fn.fnName} with args`, args); return fn(...args).then( (res) => { - console.log(`Ladda: Resolved ${entity.name}.${fn.name} with`, res); + console.log(`Ladda: Resolved ${entity.name}.${fn.fnName} with`, res); return res; }, (err) => { - console.log(`Ladda: Rejected ${entity.name}.${fn.name} with`, err) + console.log(`Ladda: Rejected ${entity.name}.${fn.fnName} with`, err) return Promise.reject(err); } ); diff --git a/package.json b/package.json index 125b51f..60eec79 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ladda-cache", - "version": "0.2.6", + "version": "0.2.7", "description": "Data fetching layer with support for caching", "main": "dist/bundle.js", "dependencies": { diff --git a/src/builder.js b/src/builder.js index e9f7002..adc34e3 100644 --- a/src/builder.js +++ b/src/builder.js @@ -1,5 +1,5 @@ -import {mapObject, mapValues, compose, toObject, reduce, toPairs, - prop, filterObject, isEqual, not, curry, copyFunction +import {map, mapObject, mapValues, compose, toObject, reduce, fromPairs, + toPairs, prop, filterObject, isEqual, not, curry, copyFunction } from 'ladda-fp'; import {cachePlugin} from './plugins/cache'; @@ -22,13 +22,6 @@ const KNOWN_STATICS = { arity: true }; -const setFnName = curry((name, fn) => { - Object.defineProperty(fn, 'name', { writable: true, configurable: true }); - fn.name = name; - Object.defineProperty(fn, 'name', { writable: false }); - return fn; -}); - const hoistMetaData = (a, b) => { const keys = Object.getOwnPropertyNames(a); for (let i = keys.length - 1; i >= 0; i--) { @@ -37,7 +30,6 @@ const hoistMetaData = (a, b) => { b[k] = a[k]; } } - setFnName(a.name, b); return b; }; @@ -52,9 +44,7 @@ export const mapApiFunctions = (fn, entityConfigs) => { // containing a "bound" prefix. (apiM, [apiFnName, apiFn]) => { const getFn = compose(prop(apiFnName), prop('api')); - const nextFn = hoistMetaData(getFn(entity), fn({ entity, fn: apiFn })); - setFnName(apiFnName, nextFn); - apiM[apiFnName] = nextFn; + apiM[apiFnName] = hoistMetaData(getFn(entity), fn({ entity, fn: apiFn })); return apiM; }, {}, @@ -100,9 +90,21 @@ const setApiConfigDefaults = ec => { return copy; }; + const setFnName = ([name, apiFn]) => { + apiFn.fnName = apiFn.fnName || name; + return [name, apiFn]; + }; + + const mapApi = compose( + fromPairs, + map(setFnName), + toPairs, + mapValues(setDefaults) + ); + return { ...ec, - api: ec.api ? mapValues(setDefaults, ec.api) : ec.api + api: ec.api ? mapApi(ec.api) : ec.api }; }; diff --git a/src/builder.spec.js b/src/builder.spec.js index 1628fdd..b6184f2 100644 --- a/src/builder.spec.js +++ b/src/builder.spec.js @@ -143,7 +143,7 @@ describe('builder', () => { const pName = pConfig.name; pluginTracker[pName] = {}; return curry(({ config: c, entityConfigs }, { fn }) => { - pluginTracker[pName][fn.name] = true; + pluginTracker[pName][fn.fnName] = true; return fn; }); }; diff --git a/src/plugins/cache/index.js b/src/plugins/cache/index.js index b7545e8..3fb4005 100644 --- a/src/plugins/cache/index.js +++ b/src/plugins/cache/index.js @@ -14,7 +14,6 @@ const HANDLERS = { NO_OPERATION: decorateNoOperation }; -const normalizeFnName = (fnName) => fnName.replace(/^bound /, ''); const normalizePayload = payload => { if (payload === null) { return payload; @@ -26,7 +25,7 @@ const notify = curry((onChange, entity, fn, args, payload) => { onChange({ operation: fn.operation, entity: entity.name, - apiFn: normalizeFnName(fn.name), + apiFn: fn.fnName, values: normalizePayload(payload), args }); diff --git a/src/plugins/cache/operations/no-operation.spec.js b/src/plugins/cache/operations/no-operation.spec.js index c11ab7e..7738dc0 100644 --- a/src/plugins/cache/operations/no-operation.spec.js +++ b/src/plugins/cache/operations/no-operation.spec.js @@ -17,6 +17,7 @@ describe('DecorateNoOperation', () => { const xOrg = {__ladda__id: 1, name: 'Kalle'}; const aFn = sinon.spy(() => Promise.resolve({})); const getUsers = () => Promise.resolve(xOrg); + getUsers.fnName = 'getUsers'; aFn.invalidates = ['getUsers']; Cache.storeQueryResponse(cache, e, getUsers, ['args'], xOrg); const res = decorateNoOperation({}, cache, curryNoop, e, aFn); diff --git a/src/plugins/cache/query-cache.js b/src/plugins/cache/query-cache.js index 6391335..34777ca 100644 --- a/src/plugins/cache/query-cache.js +++ b/src/plugins/cache/query-cache.js @@ -33,11 +33,9 @@ const getFromCache = (qc, e, k) => { }; }; -const stripBound = (fnName) => fnName.replace(/^bound /, ''); - // QueryCache -> Entity -> ApiFunction -> [a] -> [b] -> [b] export const put = curry((qc, e, aFn, args, xs) => { - const k = createKey(e, [stripBound(aFn.name), ...filter(identity, args)]); + const k = createKey(e, [aFn.fnName, ...filter(identity, args)]); if (Array.isArray(xs)) { qc.cache[k] = toCacheValue(map(prop('__ladda__id'), xs)); } else { @@ -54,13 +52,13 @@ export const getValue = (v) => { // QueryCache -> Entity -> ApiFunction -> [a] -> Bool export const contains = (qc, e, aFn, args) => { - const k = createKey(e, [stripBound(aFn.name), ...filter(identity, args)]); + const k = createKey(e, [aFn.fnName, ...filter(identity, args)]); return inCache(qc, k); }; // QueryCache -> Entity -> ApiFunction -> [a] -> Bool export const get = (qc, e, aFn, args) => { - const k = createKey(e, [stripBound(aFn.name), ...filter(identity, args)]); + const k = createKey(e, [aFn.fnName, ...filter(identity, args)]); if (!inCache(qc, k)) { throw new Error( `Tried to access ${e.name} with key ${k} which doesn't exist. From 07bd2d2c9d6040b24452cefca70b392e5f57ba50 Mon Sep 17 00:00:00 2001 From: LFDM <1986gh@gmail.com> Date: Mon, 29 May 2017 17:04:33 +0200 Subject: [PATCH 30/32] Add info about fnName to docs --- docs/advanced/Plugins.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/advanced/Plugins.md b/docs/advanced/Plugins.md index 74ee63b..8e0fca4 100644 --- a/docs/advanced/Plugins.md +++ b/docs/advanced/Plugins.md @@ -212,7 +212,8 @@ with two fields: applied and an additional `name` property is present to identify it. - `fn` is the original __ApiFunction__ we want to act on. It has all meta data attached, that was defined in the build configuration, -including defaults. +including defaults. In addition Ladda's `build` function also added the +property `fnName`, so that we can easily identify it. With this comprehensive information we can easily add additional behavior to an ApiFunction. From 987185bb2601db3c569ebbaa0a7881b288a5dbfe Mon Sep 17 00:00:00 2001 From: LFDM <1986gh@gmail.com> Date: Mon, 29 May 2017 17:05:31 +0200 Subject: [PATCH 31/32] Do not allow users to specify the fnName through an annotation --- src/builder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/builder.js b/src/builder.js index adc34e3..6bdfd68 100644 --- a/src/builder.js +++ b/src/builder.js @@ -91,7 +91,7 @@ const setApiConfigDefaults = ec => { }; const setFnName = ([name, apiFn]) => { - apiFn.fnName = apiFn.fnName || name; + apiFn.fnName = name; return [name, apiFn]; }; From ddfc0ec3ed4a500e9674240d98b9f37f0143e990 Mon Sep 17 00:00:00 2001 From: Dominik Rodler Date: Wed, 19 Jul 2017 10:49:57 +0200 Subject: [PATCH 32/32] Fixed typo: performPostRequst -> performPostRequest --- docs/basics/Operations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basics/Operations.md b/docs/basics/Operations.md index 8c09a55..c096499 100644 --- a/docs/basics/Operations.md +++ b/docs/basics/Operations.md @@ -17,7 +17,7 @@ An example of how a function of operation CREATE might look is: ```javascript createUser.operation = 'CREATE'; function createUser(user) { - return performPostRequst('/api/user', user); + return performPostRequest('/api/user', user); } ```