ladda-cache
Advanced tools
Comparing version 0.1.4 to 0.2.0
@@ -5,2 +5,8 @@ # Demos | ||
## CRUD Example | ||
This application showcases Ladda's CRUD caching capabilities, including | ||
updating Entities, as well as invalidation across different Entities. | ||
[Check it out](http://opensource.small-improvements.com/ladda-example-crud/) and make sure you also take a look at the [source code](https://github.com/SmallImprovements/ladda-example-crud), which contains a more detailed description. | ||
## Searching Hacker News | ||
@@ -7,0 +13,0 @@ |
@@ -26,2 +26,4 @@ { | ||
"babel-preset-stage-2": "^6.22.0", | ||
"exports-loader": "^0.6.4", | ||
"imports-loader": "^0.7.1", | ||
"react-hot-loader": "^1.3.1", | ||
@@ -32,6 +34,8 @@ "webpack": "^2.2.1", | ||
"dependencies": { | ||
"es6-promise-promise": "^1.0.0", | ||
"ladda-cache": "^0.1.2", | ||
"react": "^15.4.2", | ||
"react-dom": "^15.4.2" | ||
"react-dom": "^15.4.2", | ||
"whatwg-fetch": "^2.0.3" | ||
} | ||
} |
@@ -0,1 +1,3 @@ | ||
var webpack = require('webpack'); | ||
module.exports = { | ||
@@ -25,3 +27,11 @@ entry: [ | ||
hot: true | ||
} | ||
}, | ||
plugins: [ | ||
new webpack.ProvidePlugin({ | ||
Promise: 'es6-promise-promise' | ||
}), | ||
new webpack.ProvidePlugin({ | ||
'fetch': 'imports-loader?this=>global!exports-loader?global.fetch!whatwg-fetch' | ||
}), | ||
] | ||
}; |
@@ -23,2 +23,4 @@ { | ||
"babel-preset-stage-2": "^6.22.0", | ||
"exports-loader": "^0.6.4", | ||
"imports-loader": "^0.7.1", | ||
"webpack": "^2.2.1", | ||
@@ -28,5 +30,7 @@ "webpack-dev-server": "^2.4.1" | ||
"dependencies": { | ||
"ladda-cache": "^0.1.2" | ||
"es6-promise-promise": "^1.0.0", | ||
"ladda-cache": "^0.1.2", | ||
"whatwg-fetch": "^2.0.2" | ||
}, | ||
"description": "" | ||
} |
@@ -0,1 +1,3 @@ | ||
var webpack = require('webpack'); | ||
module.exports = { | ||
@@ -20,3 +22,11 @@ entry: './src/index.js', | ||
contentBase: './dist' | ||
} | ||
}, | ||
plugins: [ | ||
new webpack.ProvidePlugin({ | ||
Promise: 'es6-promise-promise' | ||
}), | ||
new webpack.ProvidePlugin({ | ||
'fetch': 'imports-loader?this=>global!exports-loader?global.fetch!whatwg-fetch' | ||
}), | ||
] | ||
}; |
@@ -23,2 +23,4 @@ { | ||
"babel-preset-stage-2": "^6.22.0", | ||
"exports-loader": "^0.6.4", | ||
"imports-loader": "^0.7.1", | ||
"webpack": "^2.2.1", | ||
@@ -28,6 +30,8 @@ "webpack-dev-server": "^2.4.1" | ||
"dependencies": { | ||
"es6-promise-promise": "^1.0.0", | ||
"ladda-cache": "^0.1.2", | ||
"vue": "^2.2.1" | ||
"vue": "^2.2.1", | ||
"whatwg-fetch": "^2.0.3" | ||
}, | ||
"description": "" | ||
} |
@@ -1,2 +0,2 @@ | ||
# Ladda in React | ||
# Ladda in Vue | ||
@@ -3,0 +3,0 @@ * `git clone git@github.com:petercrona/ladda.git` |
@@ -11,4 +11,6 @@ import Vue from 'vue'; | ||
<p>There shouldn't be a second network request, when you search for something twice.</p> | ||
<input v-model="query"> | ||
<button v-on:click="onSearch">Search</button> | ||
<form type="submit" v-on:submit.prevent="onSearch"> | ||
<input type="text" v-model="query"/> | ||
<button type="text">Search</button> | ||
</form> | ||
<div v-for="item in list"> | ||
@@ -15,0 +17,0 @@ {{item.title}} |
@@ -0,1 +1,3 @@ | ||
var webpack = require('webpack'); | ||
module.exports = { | ||
@@ -26,3 +28,11 @@ entry: './src/index.js', | ||
contentBase: './dist' | ||
} | ||
}, | ||
plugins: [ | ||
new webpack.ProvidePlugin({ | ||
Promise: 'es6-promise-promise' | ||
}), | ||
new webpack.ProvidePlugin({ | ||
'fetch': 'imports-loader?this=>global!exports-loader?global.fetch!whatwg-fetch' | ||
}), | ||
] | ||
}; |
@@ -1,5 +0,8 @@ | ||
import { expect } from 'chai'; | ||
import chai, { expect } from 'chai'; | ||
import sinonChai from 'sinon-chai'; | ||
chai.use(sinonChai); | ||
global.fdescribe = (...args) => describe.only(...args); | ||
global.fit = (...args) => it.only(...args); | ||
global.expect = expect; |
{ | ||
"name": "ladda-cache", | ||
"version": "0.1.4", | ||
"version": "0.2.0", | ||
"description": "Data fetching layer with support for caching", | ||
@@ -16,5 +16,12 @@ "main": "dist/bundle.js", | ||
"chai": "^3.5.0", | ||
"coveralls": "^2.12.0", | ||
"eslint": "^3.17.1", | ||
"eslint-config-airbnb-base": "^11.1.1", | ||
"eslint-plugin-import": "^2.2.0", | ||
"gitbook-cli": "^2.3.0", | ||
"mocha": "^2.5.3", | ||
"sinon": "^1.17.7" | ||
"nyc": "^10.1.2", | ||
"sinon": "^1.17.7", | ||
"sinon-chai": "^2.8.0", | ||
"webpack": "^1.14.0" | ||
}, | ||
@@ -24,12 +31,17 @@ "scripts": { | ||
"docs:watch": "npm run docs:prepare && gitbook serve", | ||
"test": "env NODE_PATH=$NODE_PATH:$PWD/src ./node_modules/.bin/mocha --compilers js:babel-register --reporter spec src/*.spec.js src/**/*.spec.js --require mocha.config", | ||
"coverage": "env NODE_PATH=$NODE_PATH:$PWD/src nyc -x '**/*.spec.js' -x '**/*.config.js' --reporter=lcov --reporter=text mocha --compilers js:babel-register --reporter spec src/*.spec.js src/**/*.spec.js --require mocha.config" | ||
"test": "env NODE_PATH=$NODE_PATH:$PWD/src ./node_modules/.bin/mocha --compilers js:babel-register --reporter spec src/*.spec.js 'src/**/*.spec.js' --require mocha.config", | ||
"coverage": "env NODE_PATH=$NODE_PATH:$PWD/src nyc -x '**/*.spec.js' -x '**/*.config.js' --reporter=lcov --reporter=text mocha --compilers js:babel-register --reporter spec src/*.spec.js 'src/**/*.spec.js' --require mocha.config", | ||
"lint": "eslint src", | ||
"prepublish": "npm run lint && npm test && webpack" | ||
}, | ||
"author": "Peter Crona <petercrona89@gmail.com> (http://www.icecoldcode.com)", | ||
"author": [ | ||
"Peter Crona <petercrona89@gmail.com> (http://www.icecoldcode.com)", | ||
"Gernot Hoeflechner <1986gh@gmail.com> (http://github.com/lfdm)" | ||
], | ||
"license": "MIT", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/petercrona/ladda.git" | ||
"url": "https://github.com/ladda-js/ladda.git" | ||
}, | ||
"homepage": "https://github.com/petercrona/ladda" | ||
"homepage": "https://github.com/ladda-js/ladda" | ||
} |
@@ -1,5 +0,8 @@ | ||
# Ladda | ||
![Ladda](https://smallimprovementstech.files.wordpress.com/2017/03/laddalogo-horiz-color-21.png) | ||
Ladda is a library that helps you with caching, invalidation of caches and to handle different representations of the same data in a **performant** and **memory efficient** way. | ||
![Build status](https://api.travis-ci.org/petercrona/ladda.svg?branch=master) | ||
[![Coverage Status](https://coveralls.io/repos/github/petercrona/ladda/badge.svg?branch=master&cache=1)](https://coveralls.io/github/petercrona/ladda?branch=master) | ||
Ladda is a library that helps you with caching, invalidation of caches and to handle different representations of the same data in a **performant** and **memory efficient** way. It is written in **JavaScript** (ES2015) and designed to be used by **single-page applications**. It is framework agnostic, so it works equally well with React, Vue, Angular or just vanilla JavaScript. | ||
The main goal with Ladda is to make it easy for you to add sophisticated caching **without making your application code more complex**. Ladda will take care of logic that would otherwise increase the complexity of your application code, and it will do so in a corner outside of your application. | ||
@@ -45,4 +48,7 @@ | ||
## Browser Support | ||
All the major modern browsers are supported. However, note that for old browsers, such as Internet Explorer 11, **you will need a polyfill for Promises**. | ||
# Contribute | ||
Please let us know if you have any feedback. Fork the repo, create Pull Requests and Issues. Have a look into [how to contribute](/docs/Contribute.md). |
@@ -1,31 +0,129 @@ | ||
import {mapObject, mapValues, compose, map, toObject, prop} from './fp'; | ||
import {createEntityStore} from './entity-store'; | ||
import {createQueryCache} from './query-cache'; | ||
import {decorate} from './decorator'; | ||
import {mapObject, mapValues, compose, toObject, reduce, toPairs, | ||
prop, filterObject, isEqual, not, curry, copyFunction, | ||
set | ||
} from './fp'; | ||
import {decorator} from './decorator'; | ||
import {dedup} from './dedup'; | ||
import {createListenerStore} from './listener-store'; | ||
// [[EntityName, EntityConfig]] -> Entity | ||
const toEntity = ([name, c]) => ({ | ||
name, | ||
...c | ||
name, | ||
...c | ||
}); | ||
// [Entity] -> Api | ||
const toApi = compose(mapValues(prop('api')), toObject(prop('name'))); | ||
const KNOWN_STATICS = { | ||
name: true, | ||
length: true, | ||
prototype: true, | ||
caller: true, | ||
arguments: true, | ||
arity: true | ||
}; | ||
const getEntityConfigs = c => { | ||
const cCopy = {...c}; | ||
delete cCopy.__config; | ||
return cCopy; | ||
const setFnName = curry((name, fn) => { | ||
Object.defineProperty(fn, 'name', { writable: 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--) { | ||
const k = keys[i]; | ||
if (!KNOWN_STATICS[k]) { | ||
b[k] = a[k]; | ||
} | ||
} | ||
setFnName(a.name, b); | ||
return b; | ||
}; | ||
// Config -> Api | ||
export const build = (c) => { | ||
const config = c.__config || {idField: 'id'}; | ||
const entityConfigs = getEntityConfigs(c); | ||
const entities = mapObject(toEntity, entityConfigs); | ||
const entityStore = createEntityStore(entities); | ||
const queryCache = createQueryCache(entityStore); | ||
const createApi = compose(toApi, map(decorate(config, entityStore, queryCache))); | ||
export const mapApiFunctions = (fn, entityConfigs) => { | ||
return mapValues((entity) => { | ||
return { | ||
...entity, | ||
api: reduce( | ||
// As apiFn name we use key of the api field and not the name of the | ||
// fn directly. This is controversial. Decision was made because | ||
// the original function name might be polluted at this point, e.g. | ||
// 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; | ||
return apiM; | ||
}, | ||
{}, | ||
toPairs(entity.api) | ||
) | ||
}; | ||
}, entityConfigs); | ||
}; | ||
return createApi(entities); | ||
// EntityConfig -> Api | ||
const toApi = mapValues(prop('api')); | ||
// EntityConfig -> EntityConfig | ||
const setEntityConfigDefaults = ec => { | ||
return { | ||
ttl: 300, | ||
invalidates: [], | ||
invalidatesOn: ['CREATE', 'UPDATE', 'DELETE'], | ||
...ec | ||
}; | ||
}; | ||
// EntityConfig -> EntityConfig | ||
const setApiConfigDefaults = ec => { | ||
const defaults = { | ||
operation: 'NO_OPERATION', | ||
invalidates: [], | ||
idFrom: 'ENTITY', | ||
byId: false, | ||
byIds: false | ||
}; | ||
const writeToObjectIfNotSet = curry((o, [k, v]) => { | ||
if (!o.hasOwnProperty(k)) { | ||
o[k] = v; | ||
} | ||
}); | ||
const setDefaults = apiConfig => { | ||
const copy = copyFunction(apiConfig); | ||
mapObject(writeToObjectIfNotSet(copy), defaults); | ||
return copy; | ||
}; | ||
return { | ||
...ec, | ||
api: mapValues(setDefaults, ec.api) | ||
}; | ||
}; | ||
// Config -> Map String EntityConfig | ||
const getEntityConfigs = compose( | ||
toObject(prop('name')), | ||
mapObject(toEntity), | ||
mapValues(setApiConfigDefaults), | ||
mapValues(setEntityConfigDefaults), | ||
filterObject(compose(not, isEqual('__config'))) | ||
); | ||
const applyPlugin = curry((addListener, config, entityConfigs, plugin) => { | ||
const pluginDecorator = plugin({ addListener, config, entityConfigs }); | ||
return mapApiFunctions(pluginDecorator, entityConfigs); | ||
}); | ||
// Config -> Api | ||
export const build = (c, ps = []) => { | ||
const config = c.__config || {idField: 'id'}; | ||
const listenerStore = createListenerStore(config); | ||
const addListener = set(['__addListener'], listenerStore.addListener); | ||
const applyPlugins = reduce(applyPlugin(listenerStore.addListener, config), getEntityConfigs(c)); | ||
const createApi = compose(addListener, toApi, applyPlugins); | ||
return createApi([decorator(listenerStore.onChange), ...ps, dedup]); | ||
}; |
@@ -0,5 +1,9 @@ | ||
/* eslint-disable no-unused-expressions */ | ||
import sinon from 'sinon'; | ||
import {build} from './builder'; | ||
import sinon from 'sinon'; | ||
import {curry} from './fp'; | ||
const getUsers = () => Promise.resolve([{id: 1}, {id: 2}]); | ||
const users = [{ id: 1 }, { id: 2 }]; | ||
const getUsers = () => Promise.resolve(users); | ||
getUsers.operation = 'READ'; | ||
@@ -11,99 +15,184 @@ | ||
const config = () => ({ | ||
user: { | ||
ttl: 300, | ||
api: { | ||
getUsers, | ||
deleteUser | ||
}, | ||
invalidates: ['alles'] | ||
} | ||
user: { | ||
ttl: 300, | ||
api: { | ||
getUsers, | ||
deleteUser | ||
}, | ||
invalidates: ['alles'] | ||
} | ||
}); | ||
describe('builder', () => { | ||
it('Builds the API', () => { | ||
const api = build(config()); | ||
expect(api).to.be.ok; | ||
}); | ||
it('Two read api calls will only require one api request to be made', (done) => { | ||
const myConfig = config(); | ||
myConfig.user.api.getUsers = sinon.spy(myConfig.user.api.getUsers); | ||
const api = build(myConfig); | ||
it('Builds the API', () => { | ||
const api = build(config()); | ||
expect(api).to.be.ok; | ||
}); | ||
it('Two read api calls will only require one api request to be made', (done) => { | ||
const myConfig = config(); | ||
myConfig.user.api.getUsers = sinon.spy(myConfig.user.api.getUsers); | ||
const api = build(myConfig); | ||
const expectOnlyOneApiCall = () => { | ||
expect(myConfig.user.api.getUsers.callCount).to.equal(1); | ||
done(); | ||
}; | ||
const expectOnlyOneApiCall = () => { | ||
expect(myConfig.user.api.getUsers.callCount).to.equal(1); | ||
done(); | ||
}; | ||
Promise.resolve() | ||
.then(() => api.user.getUsers()) | ||
.then(() => api.user.getUsers()) | ||
.then(expectOnlyOneApiCall); | ||
}); | ||
it('Two read api calls will return the same output', (done) => { | ||
const myConfig = config(); | ||
myConfig.user.api.getUsers = sinon.spy(myConfig.user.api.getUsers); | ||
const api = build(myConfig); | ||
Promise.resolve() | ||
.then(() => api.user.getUsers()) | ||
.then(() => api.user.getUsers()) | ||
.then(expectOnlyOneApiCall); | ||
}); | ||
it('Two read api calls will return the same output', (done) => { | ||
const myConfig = config(); | ||
myConfig.user.api.getUsers = sinon.spy(myConfig.user.api.getUsers); | ||
const api = build(myConfig); | ||
const expectOnlyOneApiCall = (xs) => { | ||
expect(xs).to.be.deep.equal([{id: 1}, {id: 2}]); | ||
done(); | ||
}; | ||
const expectOnlyOneApiCall = (xs) => { | ||
expect(xs).to.be.deep.equal([{id: 1}, {id: 2}]); | ||
done(); | ||
}; | ||
Promise.resolve() | ||
.then(() => api.user.getUsers()) | ||
.then(() => api.user.getUsers()) | ||
.then(expectOnlyOneApiCall); | ||
}); | ||
it('1000 calls is not slow', (done) => { | ||
const myConfig = config(); | ||
myConfig.user.api.getUsers = sinon.spy(myConfig.user.api.getUsers); | ||
myConfig.user.api.getUsers.idFrom = 'ARGS'; | ||
const api = build(myConfig); | ||
const start = Date.now(); | ||
const checkTimeConstraint = (xs) => { | ||
expect(Date.now() - start < 1000).to.be.true; | ||
done(); | ||
}; | ||
Promise.resolve() | ||
.then(() => api.user.getUsers()) | ||
.then(() => api.user.getUsers()) | ||
.then(expectOnlyOneApiCall); | ||
}); | ||
it('1000 calls is not slow', (done) => { | ||
const myConfig = config(); | ||
myConfig.user.api.getUsers = sinon.spy(myConfig.user.api.getUsers); | ||
myConfig.user.api.getUsers.idFrom = 'ARGS'; | ||
const api = build(myConfig); | ||
const start = Date.now(); | ||
const checkTimeConstraint = () => { | ||
expect(Date.now() - start < 1000).to.be.true; | ||
done(); | ||
}; | ||
let bc = Promise.resolve(); | ||
for (let i = 0; i < 1000; i++) { | ||
bc = bc.then(() => api.user.getUsers('wei')); | ||
} | ||
bc.then(checkTimeConstraint); | ||
let bc = Promise.resolve(); | ||
for (let i = 0; i < 1000; i++) { | ||
bc = bc.then(() => api.user.getUsers('wei')); | ||
} | ||
bc.then(checkTimeConstraint); | ||
}); | ||
it('Works with non default id set', (done) => { | ||
const myConfig = config(); | ||
myConfig.__config = {idField: 'mySecretId'}; | ||
myConfig.user.api.getUsers = sinon.spy( | ||
() => Promise.resolve([{mySecretId: 1}, {mySecretId: 2}]) | ||
); | ||
myConfig.user.api.getUsers.operation = 'READ'; | ||
const api = build(myConfig); | ||
const expectOnlyOneApiCall = (xs) => { | ||
expect(myConfig.user.api.getUsers.callCount).to.equal(1); | ||
expect(xs).to.be.deep.equal([{mySecretId: 1}, {mySecretId: 2}]); | ||
done(); | ||
}; | ||
Promise.resolve() | ||
.then(() => api.user.getUsers()) | ||
.then(() => api.user.getUsers()) | ||
.then(expectOnlyOneApiCall); | ||
}); | ||
it('Delete removes value from cached array', (done) => { | ||
const myConfig = config(); | ||
myConfig.user.api.getUsers = sinon.spy(() => Promise.resolve([{id: 1}, {id: 2}])); | ||
myConfig.user.api.getUsers.operation = 'READ'; | ||
const api = build(myConfig); | ||
const expectUserToBeRemoved = (xs) => { | ||
expect(xs).to.be.deep.equal([{id: 2}]); | ||
done(); | ||
}; | ||
Promise.resolve() | ||
.then(() => api.user.getUsers()) | ||
.then(() => api.user.deleteUser(1)) | ||
.then(() => api.user.getUsers()) | ||
.then(expectUserToBeRemoved); | ||
}); | ||
it('TTL set to zero means we never get a cache hit', (done) => { | ||
const myConfig = config(); | ||
myConfig.user.ttl = 0; | ||
myConfig.user.api.getUsers = sinon.spy(myConfig.user.api.getUsers); | ||
const api = build(myConfig); | ||
const expectOnlyOneApiCall = () => { | ||
expect(myConfig.user.api.getUsers.callCount).to.equal(2); | ||
done(); | ||
}; | ||
const delay = () => new Promise(res => setTimeout(() => res(), 1)); | ||
Promise.resolve() | ||
.then(() => api.user.getUsers()) | ||
.then(delay) | ||
.then(() => api.user.getUsers()) | ||
.then(expectOnlyOneApiCall); | ||
}); | ||
it('takes plugins as second argument', (done) => { | ||
const myConfig = config(); | ||
const pluginTracker = {}; | ||
const plugin = (pConfig) => { | ||
const pName = pConfig.name; | ||
pluginTracker[pName] = {}; | ||
return curry(({ config: c, entityConfigs }, { fn }) => { | ||
pluginTracker[pName][fn.name] = true; | ||
return fn; | ||
}); | ||
}; | ||
const pluginName = 'X'; | ||
const expectACall = () => expect(pluginTracker[pluginName].getUsers).to.be.true; | ||
const api = build(myConfig, [plugin({ name: pluginName })]); | ||
api.user.getUsers() | ||
.then(expectACall) | ||
.then(() => done()); | ||
}); | ||
it('exposes Ladda\'s listener/onChange interface', () => { | ||
const api = build(config()); | ||
expect(api.__addListener).to.be; | ||
}); | ||
describe('__addListener', () => { | ||
it('allows to add a listener, which gets notified on all cache changes', () => { | ||
const api = build(config()); | ||
const spy = sinon.spy(); | ||
api.__addListener(spy); | ||
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.type).to.equal('UPDATE'); | ||
expect(changeObject.entities).to.deep.equal(users); | ||
}); | ||
}); | ||
it('Works with non default id set', (done) => { | ||
const myConfig = config(); | ||
myConfig.__config = {idField: 'mySecretId'}; | ||
myConfig.user.api.getUsers = sinon.spy(() => | ||
Promise.resolve([{mySecretId: 1}, {mySecretId: 2}])); | ||
myConfig.user.api.getUsers.operation = 'READ'; | ||
const api = build(myConfig); | ||
const expectOnlyOneApiCall = (xs) => { | ||
expect(myConfig.user.api.getUsers.callCount).to.equal(1); | ||
expect(xs).to.be.deep.equal([{mySecretId: 1}, {mySecretId: 2}]); | ||
done(); | ||
}; | ||
Promise.resolve() | ||
.then(() => api.user.getUsers()) | ||
.then(() => api.user.getUsers()) | ||
.then(expectOnlyOneApiCall); | ||
it('does not trigger when a pure cache hit is made', () => { | ||
const api = build(config()); | ||
const spy = sinon.spy(); | ||
api.__addListener(spy); | ||
return api.user.getUsers().then(() => { | ||
expect(spy).to.have.been.calledOnce; | ||
return api.user.getUsers().then(() => { | ||
expect(spy).to.have.been.calledOnce; | ||
}); | ||
}); | ||
}); | ||
it('Delete removes value for query cache in array', (done) => { | ||
const myConfig = config(); | ||
myConfig.user.api.getUsers = sinon.spy(() => | ||
Promise.resolve([{id: 1}, {id: 2}])); | ||
myConfig.user.api.getUsers.operation = 'READ'; | ||
const api = build(myConfig); | ||
const expectOnlyOneApiCall = (xs) => { | ||
expect(xs).to.be.deep.equal([{id: 2}]); | ||
done(); | ||
}; | ||
Promise.resolve() | ||
.then(() => api.user.getUsers()) | ||
.then(() => api.user.deleteUser(1)) | ||
.then(() => api.user.getUsers()) | ||
.then(expectOnlyOneApiCall); | ||
it('returns a deregistration function to remove the listener', () => { | ||
const api = build(config()); | ||
const spy = sinon.spy(); | ||
const deregister = api.__addListener(spy); | ||
deregister(); | ||
return api.user.getUsers().then(() => { | ||
expect(spy).not.to.have.been.called; | ||
}); | ||
}); | ||
}); | ||
}); |
@@ -1,12 +0,12 @@ | ||
import {put} from 'entity-store'; | ||
import {invalidate} from 'query-cache'; | ||
import {passThrough, compose} from 'fp'; | ||
import {addId} from 'id-helper'; | ||
import {put} from '../entity-store'; | ||
import {invalidate} from '../query-cache'; | ||
import {passThrough, compose} from '../fp'; | ||
import {addId} from '../id-helper'; | ||
export function decorateCreate(c, es, qc, e, aFn) { | ||
return (...args) => { | ||
return aFn(...args) | ||
.then(passThrough(compose(put(es, e), addId(c, aFn, args)))) | ||
.then(passThrough(() => invalidate(qc, e, aFn))); | ||
}; | ||
return (...args) => { | ||
return aFn(...args) | ||
.then(passThrough(() => invalidate(qc, e, aFn))) | ||
.then(passThrough(compose(put(es, e), addId(c, aFn, args)))); | ||
}; | ||
} |
@@ -0,59 +1,59 @@ | ||
import sinon from 'sinon'; | ||
import {decorateCreate} from './create'; | ||
import {createEntityStore, get, put} from 'entity-store'; | ||
import {createQueryCache} from 'query-cache'; | ||
import sinon from 'sinon'; | ||
import {createEntityStore, get} from '../entity-store'; | ||
import {createQueryCache} from '../query-cache'; | ||
import {createApiFunction} from '../test-helper'; | ||
const config = [ | ||
{ | ||
name: 'user', | ||
ttl: 300, | ||
api: { | ||
getUsers: (x) => x, | ||
getUsers2: (x) => x, | ||
deleteUser: (x) => x, | ||
}, | ||
invalidates: ['user'], | ||
invalidatesOn: ['GET'] | ||
{ | ||
name: 'user', | ||
ttl: 300, | ||
api: { | ||
getUsers: (x) => x, | ||
getUsers2: (x) => x, | ||
deleteUser: (x) => x | ||
}, | ||
{ | ||
name: 'userPreview', | ||
ttl: 200, | ||
api: { | ||
getPreviews: (x) => x, | ||
updatePreview: (x) => x, | ||
}, | ||
invalidates: ['fda'], | ||
viewOf: 'user' | ||
invalidates: ['user'], | ||
invalidatesOn: ['GET'] | ||
}, | ||
{ | ||
name: 'userPreview', | ||
ttl: 200, | ||
api: { | ||
getPreviews: (x) => x, | ||
updatePreview: (x) => x | ||
}, | ||
{ | ||
name: 'listUser', | ||
ttl: 200, | ||
api: { | ||
getPreviews: (x) => x, | ||
updatePreview: (x) => x, | ||
}, | ||
invalidates: ['fda'], | ||
viewOf: 'user' | ||
} | ||
invalidates: ['fda'], | ||
viewOf: 'user' | ||
}, | ||
{ | ||
name: 'listUser', | ||
ttl: 200, | ||
api: { | ||
getPreviews: (x) => x, | ||
updatePreview: (x) => x | ||
}, | ||
invalidates: ['fda'], | ||
viewOf: 'user' | ||
} | ||
]; | ||
describe('Create', () => { | ||
describe('decorateCreate', () => { | ||
it('Adds value to entity store', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const xOrg = {name: 'Kalle'}; | ||
const response = {...xOrg, id: 1}; | ||
const aFn = sinon.spy(() => { | ||
return Promise.resolve(response); | ||
}); | ||
const res = decorateCreate({}, es, qc, e, aFn); | ||
res(xOrg).then((newX) => { | ||
expect(newX).to.equal(response); | ||
expect(get(es, e, 1).value).to.deep.equal({...response, __ladda__id: 1}); | ||
done(); | ||
}); | ||
}); | ||
describe('decorateCreate', () => { | ||
it('Adds value to entity store', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
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({}, es, qc, e, aFn); | ||
res(xOrg).then((newX) => { | ||
expect(newX).to.equal(response); | ||
expect(get(es, e, 1).value).to.deep.equal({...response, __ladda__id: 1}); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
}); |
@@ -1,12 +0,12 @@ | ||
import {remove} from 'entity-store'; | ||
import {invalidate} from 'query-cache'; | ||
import {passThrough} from 'fp'; | ||
import {serialize} from 'serializer'; | ||
import {remove} from '../entity-store'; | ||
import {invalidate} from '../query-cache'; | ||
import {passThrough} from '../fp'; | ||
import {serialize} from '../serializer'; | ||
export function decorateDelete(c, es, qc, e, aFn) { | ||
return (...args) => { | ||
remove(es, e, serialize(args)); | ||
return aFn(...args) | ||
.then(passThrough(() => invalidate(qc, e, aFn))); | ||
}; | ||
return (...args) => { | ||
return aFn(...args) | ||
.then(passThrough(() => invalidate(qc, e, aFn))) | ||
.then(() => remove(es, e, serialize(args))); | ||
}; | ||
} |
@@ -0,59 +1,59 @@ | ||
import sinon from 'sinon'; | ||
import {decorateDelete} from './delete'; | ||
import {createEntityStore, get, put} from 'entity-store'; | ||
import {createQueryCache} from 'query-cache'; | ||
import {addId} from 'id-helper'; | ||
import sinon from 'sinon'; | ||
import {createEntityStore, get, put} from '../entity-store'; | ||
import {createQueryCache} from '../query-cache'; | ||
import {addId} from '../id-helper'; | ||
import {createApiFunction} from '../test-helper'; | ||
const config = [ | ||
{ | ||
name: 'user', | ||
ttl: 300, | ||
api: { | ||
getUsers: (x) => x, | ||
getUsers2: (x) => x, | ||
deleteUser: (x) => x, | ||
}, | ||
invalidates: ['user'], | ||
invalidatesOn: ['GET'] | ||
{ | ||
name: 'user', | ||
ttl: 300, | ||
api: { | ||
getUsers: (x) => x, | ||
getUsers2: (x) => x, | ||
deleteUser: (x) => x | ||
}, | ||
{ | ||
name: 'userPreview', | ||
ttl: 200, | ||
api: { | ||
getPreviews: (x) => x, | ||
updatePreview: (x) => x, | ||
}, | ||
invalidates: ['fda'], | ||
viewOf: 'user' | ||
invalidates: ['user'], | ||
invalidatesOn: ['GET'] | ||
}, | ||
{ | ||
name: 'userPreview', | ||
ttl: 200, | ||
api: { | ||
getPreviews: (x) => x, | ||
updatePreview: (x) => x | ||
}, | ||
{ | ||
name: 'listUser', | ||
ttl: 200, | ||
api: { | ||
getPreviews: (x) => x, | ||
updatePreview: (x) => x, | ||
}, | ||
invalidates: ['fda'], | ||
viewOf: 'user' | ||
} | ||
invalidates: ['fda'], | ||
viewOf: 'user' | ||
}, | ||
{ | ||
name: 'listUser', | ||
ttl: 200, | ||
api: { | ||
getPreviews: (x) => x, | ||
updatePreview: (x) => x | ||
}, | ||
invalidates: ['fda'], | ||
viewOf: 'user' | ||
} | ||
]; | ||
describe('Delete', () => { | ||
describe('decorateDelete', () => { | ||
it('Removes cache', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const xOrg = {id: 1, name: 'Kalle'}; | ||
const aFn = sinon.spy(() => { | ||
return Promise.resolve({}); | ||
}); | ||
put(es, e, addId({}, undefined, undefined, xOrg)); | ||
const res = decorateDelete({}, es, qc, e, aFn); | ||
res(1).then(() => { | ||
expect(get(es, e, 1)).to.equal(undefined); | ||
done(); | ||
}); | ||
}); | ||
describe('decorateDelete', () => { | ||
it('Removes cache', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const xOrg = {id: 1, name: 'Kalle'}; | ||
const aFnWithoutSpy = createApiFunction(() => Promise.resolve({})); | ||
const aFn = sinon.spy(aFnWithoutSpy); | ||
put(es, e, addId({}, undefined, undefined, xOrg)); | ||
const res = decorateDelete({}, es, qc, e, aFn); | ||
res(1).then(() => { | ||
expect(get(es, e, 1)).to.equal(undefined); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
}); |
@@ -1,2 +0,4 @@ | ||
import {curry, mapValues} from 'fp'; | ||
import {compose, values} from '../fp'; | ||
import {createEntityStore} from '../entity-store'; | ||
import {createQueryCache} from '../query-cache'; | ||
import {decorateCreate} from './create'; | ||
@@ -8,19 +10,18 @@ import {decorateRead} from './read'; | ||
const decorateApi = curry((config, entityStore, queryCache, entity, apiFn) => { | ||
const handler = { | ||
CREATE: decorateCreate, | ||
READ: decorateRead, | ||
UPDATE: decorateUpdate, | ||
DELETE: decorateDelete, | ||
NO_OPERATION: decorateNoOperation | ||
}[apiFn.operation || 'NO_OPERATION']; | ||
return handler(config, entityStore, queryCache, entity, apiFn); | ||
}); | ||
const HANDLERS = { | ||
CREATE: decorateCreate, | ||
READ: decorateRead, | ||
UPDATE: decorateUpdate, | ||
DELETE: decorateDelete, | ||
NO_OPERATION: decorateNoOperation | ||
}; | ||
export const decorate = curry((config, entityStore, queryCache, entity) => { | ||
const decoratedApi = mapValues(decorateApi(config, entityStore, queryCache, entity), entity.api); | ||
return { | ||
...entity, | ||
api: decoratedApi | ||
}; | ||
}); | ||
export const decorator = (onChange) => ({ config, entityConfigs }) => { | ||
const entityStore = compose((c) => createEntityStore(c, onChange), values)(entityConfigs); | ||
const queryCache = createQueryCache(entityStore, onChange); | ||
return ({ entity, fn }) => { | ||
const handler = HANDLERS[fn.operation]; | ||
return handler(config, entityStore, queryCache, entity, fn); | ||
}; | ||
}; | ||
@@ -0,27 +1,30 @@ | ||
/* eslint-disable no-unused-expressions */ | ||
import {decorate} from './index'; | ||
import {createEntityStore} from 'entity-store'; | ||
import {createQueryCache, put, contains} from 'query-cache'; | ||
import {addId} from 'id-helper'; | ||
import {createEntityStore} from '../entity-store'; | ||
import {createQueryCache, put, contains} from '../query-cache'; | ||
import {addId} from '../id-helper'; | ||
import {createApiFunction} from '../test-helper'; | ||
const config = [ | ||
{ | ||
name: 'user', | ||
ttl: 300, | ||
api: { | ||
getUsers: (x) => x, | ||
getUsers2: (x) => x, | ||
deleteUser: (x) => x, | ||
}, | ||
invalidates: ['user'], | ||
invalidatesOn: ['GET'] | ||
{ | ||
name: 'user', | ||
ttl: 300, | ||
api: { | ||
getUsers: (x) => x, | ||
getUsers2: (x) => x, | ||
deleteUser: (x) => x | ||
}, | ||
{ | ||
name: 'cars', | ||
ttl: 200, | ||
api: { | ||
triggerCarValueCalculation: (x) => Promise.resolve([x]) | ||
}, | ||
invalidates: ['user'], | ||
invalidatesOn: ['NO_OPERATION'] | ||
} | ||
invalidates: ['user'], | ||
invalidatesOn: ['GET'] | ||
}, | ||
{ | ||
name: 'cars', | ||
ttl: 200, | ||
api: { | ||
triggerCarValueCalculation: createApiFunction((x) => Promise.resolve([x])) | ||
}, | ||
invalidates: ['user'], | ||
invalidatesOn: ['NO_OPERATION'] | ||
} | ||
]; | ||
@@ -31,25 +34,19 @@ | ||
describe('Decorate', () => { | ||
it('returns decorated function if no operation specified', () => { | ||
const f = (x) => x; | ||
const entity = {api: {getAll: f}}; | ||
const res = decorate({}, null, null, entity); | ||
expect(res.api.getAll).not.to.equal(f); | ||
}); | ||
it('decorated function invalidates if NO_OPERATION is configured', (done) => { | ||
const aFn = () => Promise.resolve('hej'); | ||
const xOrg = [{id: 1, name: 'Kalle'}]; | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const eUser = config[0]; | ||
const eCar = config[1]; | ||
const carsApi = decorate({}, es, qc, eCar, aFn); | ||
put(qc, eUser, aFn, [1], addId({}, undefined, undefined, xOrg)); | ||
xit('decorated function invalidates if NO_OPERATION is configured', (done) => { | ||
const aFn = createApiFunction(() => Promise.resolve('hej')); | ||
const xOrg = [{id: 1, name: 'Kalle'}]; | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const eUser = config[0]; | ||
const eCar = config[1]; | ||
const carsApi = decorate({}, es, qc, eCar, aFn); | ||
put(qc, eUser, aFn, [1], addId({}, undefined, undefined, xOrg)); | ||
expect(contains(qc, eUser, aFn, [1])).to.be.true; | ||
const shouldHaveRemovedUser = () => { | ||
expect(contains(qc, eUser, aFn, [1])).to.be.false; | ||
done(); | ||
}; | ||
carsApi.api.triggerCarValueCalculation(xOrg).then(shouldHaveRemovedUser); | ||
}); | ||
expect(contains(qc, eUser, aFn, [1])).to.be.true; | ||
const shouldHaveRemovedUser = () => { | ||
expect(contains(qc, eUser, aFn, [1])).to.be.false; | ||
done(); | ||
}; | ||
carsApi.api.triggerCarValueCalculation(xOrg).then(shouldHaveRemovedUser); | ||
}); | ||
}); |
@@ -1,16 +0,9 @@ | ||
import {invalidate} from 'query-cache'; | ||
import {passThrough} from 'fp'; | ||
import {invalidate} from '../query-cache'; | ||
import {passThrough} from '../fp'; | ||
export function decorateNoOperation(c, es, qc, e, aFn) { | ||
const newApiFn = aFn.bind(null); | ||
for (let x in aFn) { | ||
if (aFn.hasOwnProperty(x)) { | ||
newApiFn[x] = aFn[x]; | ||
} | ||
} | ||
newApiFn.operation = 'NO_OPERATION'; | ||
return (...args) => { | ||
return aFn(...args) | ||
.then(passThrough(() => invalidate(qc, e, newApiFn))); | ||
}; | ||
return (...args) => { | ||
return aFn(...args) | ||
.then(passThrough(() => invalidate(qc, e, aFn))); | ||
}; | ||
} |
@@ -0,88 +1,55 @@ | ||
/* eslint-disable no-unused-expressions */ | ||
import sinon from 'sinon'; | ||
import {decorateNoOperation} from './no-operation'; | ||
import {createEntityStore} from 'entity-store'; | ||
import {createQueryCache, contains, put} from 'query-cache'; | ||
import sinon from 'sinon'; | ||
import {createEntityStore} from '../entity-store'; | ||
import {createQueryCache, contains, put} from '../query-cache'; | ||
import {createSampleConfig, createApiFunction} from '../test-helper'; | ||
const config = [ | ||
{ | ||
name: 'user', | ||
ttl: 300, | ||
api: { | ||
getUsers: (x) => x, | ||
getUsers2: (x) => x, | ||
deleteUser: (x) => x, | ||
}, | ||
invalidates: ['user'], | ||
invalidatesOn: ['GET'] | ||
}, | ||
{ | ||
name: 'userPreview', | ||
ttl: 200, | ||
api: { | ||
getPreviews: (x) => x, | ||
updatePreview: (x) => x, | ||
}, | ||
invalidates: ['fda'], | ||
viewOf: 'user' | ||
}, | ||
{ | ||
name: 'listUser', | ||
ttl: 200, | ||
api: { | ||
getPreviews: (x) => x, | ||
updatePreview: (x) => x, | ||
}, | ||
invalidates: ['fda'], | ||
viewOf: 'user' | ||
} | ||
]; | ||
const config = createSampleConfig(); | ||
describe('DecorateNoOperation', () => { | ||
it('Invalidates based on what is specified in the original function', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const xOrg = {__ladda__id: 1, name: 'Kalle'}; | ||
const aFn = sinon.spy(() => { | ||
return Promise.resolve({}); | ||
}); | ||
const getUsers = () => Promise.resolve(xOrg); | ||
aFn.invalidates = ['getUsers']; | ||
put(qc, e, getUsers, ['args'], xOrg); | ||
const res = decorateNoOperation({}, es, qc, e, aFn); | ||
res(xOrg).then(() => { | ||
const killedCache = !contains(qc, e, getUsers, ['args']); | ||
expect(killedCache).to.be.true; | ||
done(); | ||
}); | ||
it('Invalidates based on what is specified in the original function', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
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']; | ||
put(qc, e, getUsers, ['args'], xOrg); | ||
const res = decorateNoOperation({}, es, qc, e, aFn); | ||
res(xOrg).then(() => { | ||
const killedCache = !contains(qc, e, getUsers, ['args']); | ||
expect(killedCache).to.be.true; | ||
done(); | ||
}); | ||
it('Does not change original function', () => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const aFn = sinon.spy(() => { | ||
return Promise.resolve({}); | ||
}); | ||
decorateNoOperation({}, es, qc, e, aFn); | ||
expect(aFn.operation).to.be.undefined; | ||
}); | ||
it('Does not change original function', () => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const aFn = sinon.spy(() => { | ||
return Promise.resolve({}); | ||
}); | ||
it('Ignored inherited invalidation config', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const xOrg = {__ladda__id: 1, name: 'Kalle'}; | ||
const aFn = sinon.spy(() => { | ||
return Promise.resolve({}); | ||
}); | ||
const getUsers = () => Promise.resolve(xOrg); | ||
aFn.invalidates = ['getUsers']; | ||
aFn.hasOwnProperty = () => false; | ||
put(qc, e, getUsers, ['args'], xOrg); | ||
const res = decorateNoOperation({}, es, qc, e, aFn); | ||
res(xOrg).then(() => { | ||
const killedCache = !contains(qc, e, getUsers, ['args']); | ||
expect(killedCache).to.be.false; | ||
done(); | ||
}); | ||
decorateNoOperation({}, es, qc, e, aFn); | ||
expect(aFn.operation).to.be.undefined; | ||
}); | ||
it('Ignored inherited invalidation config', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const xOrg = {__ladda__id: 1, name: 'Kalle'}; | ||
const aFnWithoutSpy = createApiFunction(() => Promise.resolve({}), {invalidates: ['user']}); | ||
const aFn = sinon.spy(aFnWithoutSpy); | ||
const getUsers = createApiFunction(() => Promise.resolve(xOrg)); | ||
aFn.hasOwnProperty = () => false; | ||
put(qc, e, getUsers, ['args'], xOrg); | ||
const res = decorateNoOperation({}, es, qc, e, aFn); | ||
res(xOrg).then(() => { | ||
const killedCache = !contains(qc, e, getUsers, ['args']); | ||
expect(killedCache).to.be.false; | ||
done(); | ||
}); | ||
}); | ||
}); |
import {get as getFromEs, | ||
put as putInEs, | ||
contains as inEs} from 'entity-store'; | ||
mPut as mPutInEs, | ||
contains as inEs} from '../entity-store'; | ||
import {get as getFromQc, | ||
@@ -8,48 +9,88 @@ invalidate, | ||
contains as inQc, | ||
getValue} from 'query-cache'; | ||
import {passThrough, compose} from 'fp'; | ||
import {addId, removeId} from 'id-helper'; | ||
getValue} from '../query-cache'; | ||
import {passThrough, compose, curry, reduce, toIdMap, map, concat, zip} from '../fp'; | ||
import {addId, removeId} from '../id-helper'; | ||
const getTtl = e => (e.ttl || 300) * 1000; | ||
const getTtl = e => e.ttl * 1000; | ||
// Entity -> Int -> Bool | ||
const hasExpired = (e, timestamp) => { | ||
return (Date.now() - timestamp) > getTtl(e); | ||
return (Date.now() - timestamp) > getTtl(e); | ||
}; | ||
const readFromCache = curry((es, e, aFn, id) => { | ||
if (inEs(es, e, id) && !aFn.alwaysGetFreshData) { | ||
const v = getFromEs(es, e, id); | ||
if (!hasExpired(e, v.timestamp)) { | ||
return removeId(v.value); | ||
} | ||
} | ||
return undefined; | ||
}); | ||
const decorateReadSingle = (c, es, qc, e, aFn) => { | ||
return (id) => { | ||
if (inEs(es, e, id) && !aFn.alwaysGetFreshData) { | ||
const v = getFromEs(es, e, id); | ||
if (!hasExpired(e, v.timestamp)) { | ||
return Promise.resolve(removeId(v.value)); | ||
} | ||
} | ||
return (id) => { | ||
const fromCache = readFromCache(es, e, aFn, id); | ||
if (fromCache) { | ||
return Promise.resolve(fromCache); | ||
} | ||
return aFn(id).then(passThrough(compose(putInEs(es, e), addId(c, aFn, id)))) | ||
.then(passThrough(() => invalidate(qc, e, aFn))); | ||
}; | ||
return aFn(id) | ||
.then(passThrough(compose(putInEs(es, e), addId(c, aFn, id)))) | ||
.then(passThrough(() => invalidate(qc, e, aFn))); | ||
}; | ||
}; | ||
const decorateReadSome = (c, es, qc, e, aFn) => { | ||
return (ids) => { | ||
const readFromCache_ = readFromCache(es, e, aFn); | ||
const [cached, remaining] = reduce(([c_, r], id) => { | ||
const fromCache = readFromCache_(id); | ||
if (fromCache) { | ||
c_.push(fromCache); | ||
} else { | ||
r.push(id); | ||
} | ||
return [c_, r]; | ||
}, [[], []], ids); | ||
if (!remaining.length) { | ||
return Promise.resolve(cached); | ||
} | ||
const addIds = map(([id, item]) => addId(c, aFn, id, item)); | ||
return aFn(remaining) | ||
.then(passThrough(compose(mPutInEs(es, e), addIds, zip(remaining)))) | ||
.then(passThrough(() => invalidate(qc, e, aFn))) | ||
.then((other) => { | ||
const asMap = compose(toIdMap, concat)(cached, other); | ||
return map((id) => asMap[id], ids); | ||
}); | ||
}; | ||
}; | ||
const decorateReadQuery = (c, es, qc, e, aFn) => { | ||
return (...args) => { | ||
if (inQc(qc, e, aFn, args) && !aFn.alwaysGetFreshData) { | ||
const v = getFromQc(qc, e, aFn, args); | ||
if (!hasExpired(e, v.timestamp)) { | ||
return Promise.resolve(removeId(getValue(v.value))); | ||
} | ||
} | ||
return (...args) => { | ||
if (inQc(qc, e, aFn, args) && !aFn.alwaysGetFreshData) { | ||
const v = getFromQc(qc, e, aFn, args); | ||
if (!hasExpired(e, v.timestamp)) { | ||
return Promise.resolve(removeId(getValue(v.value))); | ||
} | ||
} | ||
return aFn(...args) | ||
.then(passThrough(compose(putInQc(qc, e, aFn, args), addId(c, aFn, args)))) | ||
.then(passThrough(() => invalidate(qc, e, aFn))); | ||
}; | ||
return aFn(...args) | ||
.then(passThrough(compose(putInQc(qc, e, aFn, args), addId(c, aFn, args)))) | ||
.then(passThrough(() => invalidate(qc, e, aFn))); | ||
}; | ||
}; | ||
export function decorateRead(c, es, qc, e, aFn) { | ||
if (aFn.byId) { | ||
return decorateReadSingle(c, es, qc, e, aFn); | ||
} else { | ||
return decorateReadQuery(c, es, qc, e, aFn); | ||
} | ||
if (aFn.byId) { | ||
return decorateReadSingle(c, es, qc, e, aFn); | ||
} | ||
if (aFn.byIds) { | ||
return decorateReadSome(c, es, qc, e, aFn); | ||
} | ||
return decorateReadQuery(c, es, qc, e, aFn); | ||
} |
@@ -0,223 +1,270 @@ | ||
/* eslint-disable no-unused-expressions */ | ||
import sinon from 'sinon'; | ||
import {decorateRead} from './read'; | ||
import {createEntityStore} from 'entity-store'; | ||
import {createQueryCache} from 'query-cache'; | ||
import sinon from 'sinon'; | ||
import {createEntityStore} from '../entity-store'; | ||
import {createQueryCache} from '../query-cache'; | ||
import {createSampleConfig, createApiFunction} from '../test-helper'; | ||
import {map} from '../fp'; | ||
const config = [ | ||
{ | ||
name: 'user', | ||
ttl: 300, | ||
api: { | ||
getUsers: (x) => x, | ||
getUsers2: (x) => x, | ||
deleteUser: (x) => x, | ||
}, | ||
invalidates: ['user'], | ||
invalidatesOn: ['GET'] | ||
}, | ||
{ | ||
name: 'userPreview', | ||
ttl: 200, | ||
api: { | ||
getPreviews: (x) => x, | ||
updatePreview: (x) => x, | ||
}, | ||
invalidates: ['fda'], | ||
viewOf: 'user' | ||
}, | ||
{ | ||
name: 'listUser', | ||
ttl: 200, | ||
api: { | ||
getPreviews: (x) => x, | ||
updatePreview: (x) => x, | ||
}, | ||
invalidates: ['fda'], | ||
viewOf: 'user' | ||
} | ||
]; | ||
const config = createSampleConfig(); | ||
describe('Read', () => { | ||
describe('decorateRead', () => { | ||
it('stores and returns an array with elements that lack id', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const xOrg = [{name: 'Kalle'}, {name: 'Anka'}]; | ||
const aFn = sinon.spy(() => { | ||
return Promise.resolve(xOrg); | ||
}); | ||
aFn.idFrom = 'ARGS'; | ||
const res = decorateRead({}, es, qc, e, aFn); | ||
res(1).then(x => { | ||
expect(x).to.deep.equal(xOrg); | ||
done(); | ||
}); | ||
describe('decorateRead', () => { | ||
it('stores and returns an array with elements that lack id', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
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({}, es, qc, e, aFn); | ||
res(1).then(x => { | ||
expect(x).to.deep.equal(xOrg); | ||
done(); | ||
}); | ||
}); | ||
it('does set id to serialized args if idFrom ARGS', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const xOrg = {name: 'Kalle'}; | ||
const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg), {idFrom: 'ARGS'}); | ||
const aFn = sinon.spy(aFnWithoutSpy); | ||
const res = decorateRead({}, es, qc, e, aFn); | ||
res({hello: 'hej', other: 'svej'}).then(x => { | ||
expect(x).to.deep.equal({name: 'Kalle'}); | ||
done(); | ||
}); | ||
}); | ||
it('calls api fn if not in cache with byId set', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
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({}, es, qc, 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 es = createEntityStore(myConfig); | ||
const qc = createQueryCache(es); | ||
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({}, es, qc, 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 es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
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({}, es, qc, 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' }, | ||
b: { id: 'b' }, | ||
c: { id: 'c' } | ||
}; | ||
const fn = (ids) => Promise.resolve(map((id) => users[id], ids)); | ||
const decoratedFn = createApiFunction(fn, { byIds: true }); | ||
it('calls api fn if nothing in cache', () => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const fnWithSpy = sinon.spy(decoratedFn); | ||
const apiFn = decorateRead({}, es, qc, e, fnWithSpy); | ||
return apiFn(['a', 'b']).then((res) => { | ||
expect(res).to.deep.equal([users.a, users.b]); | ||
}); | ||
it('does set id to serialized args if idFrom ARGS', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const xOrg = {name: 'Kalle'}; | ||
const aFn = sinon.spy(() => { | ||
return Promise.resolve(xOrg); | ||
}); | ||
aFn.idFrom = 'ARGS'; | ||
const res = decorateRead({}, es, qc, e, aFn); | ||
res({hello: 'hej', other: 'svej'}).then(x => { | ||
expect(x).to.deep.equal({name: 'Kalle'}); | ||
done(); | ||
}); | ||
}); | ||
it('calls api fn if nothing in cache', () => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const fnWithSpy = sinon.spy(decoratedFn); | ||
const apiFn = decorateRead({}, es, qc, e, fnWithSpy); | ||
return apiFn(['a', 'b']).then((res) => { | ||
expect(res).to.deep.equal([users.a, users.b]); | ||
}); | ||
it('calls api fn if not in cache with byId set', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const xOrg = {id: 1, name: 'Kalle'}; | ||
const aFn = sinon.spy(() => { | ||
return Promise.resolve(xOrg); | ||
}); | ||
aFn.byId = true; | ||
const res = decorateRead({}, es, qc, e, aFn); | ||
res(1).then((x) => { | ||
expect(aFn.callCount).to.equal(1); | ||
done(); | ||
}); | ||
}); | ||
it('puts item in the cache and can read them again', () => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const fnWithSpy = sinon.spy(decoratedFn); | ||
const apiFn = decorateRead({}, es, qc, e, fnWithSpy); | ||
const args = ['a', 'b']; | ||
return apiFn(args).then(() => { | ||
return apiFn(args).then((res) => { | ||
expect(fnWithSpy).to.have.been.calledOnce; | ||
expect(res).to.deep.equal([users.a, users.b]); | ||
}); | ||
}); | ||
it('does not call api fn if in cache with byId set', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const xOrg = {id: 1, name: 'Kalle'}; | ||
const aFn = sinon.spy(() => { | ||
return Promise.resolve(xOrg); | ||
}); | ||
aFn.byId = true; | ||
const res = decorateRead({}, es, qc, e, aFn); | ||
res(1).then(res.bind(null, 1)).then(() => { | ||
expect(aFn.callCount).to.equal(1); | ||
done(); | ||
}); | ||
}); | ||
it('only makes additional request for uncached items', () => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const fnWithSpy = sinon.spy(decoratedFn); | ||
const apiFn = decorateRead({}, es, qc, e, fnWithSpy); | ||
return apiFn(['a', 'b']).then(() => { | ||
return apiFn(['b', 'c']).then(() => { | ||
expect(fnWithSpy).to.have.been.calledTwice; | ||
expect(fnWithSpy).to.have.been.calledWith(['a', 'b']); | ||
expect(fnWithSpy).to.have.been.calledWith(['c']); | ||
}); | ||
}); | ||
it('calls api fn if not in cache', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const xOrg = {id: 1, name: 'Kalle'}; | ||
const aFn = sinon.spy(() => { | ||
return Promise.resolve(xOrg); | ||
}); | ||
const res = decorateRead({}, es, qc, e, aFn); | ||
res(1).then(() => { | ||
expect(aFn.callCount).to.equal(1); | ||
done(); | ||
}); | ||
}); | ||
it('does not call api fn if in cache', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const xOrg = {id: 1, name: 'Kalle'}; | ||
const aFn = sinon.spy(() => { | ||
return Promise.resolve(xOrg); | ||
}); | ||
const res = decorateRead({}, es, qc, e, aFn); | ||
}); | ||
const firstCall = res(1); | ||
firstCall.then(() => { | ||
res(1).then(() => { | ||
expect(aFn.callCount).to.equal(1); | ||
done(); | ||
}); | ||
}); | ||
it('returns all items in correct order when making partial requests', () => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const fnWithSpy = sinon.spy(decoratedFn); | ||
const apiFn = decorateRead({}, es, qc, 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]); | ||
}); | ||
}); | ||
it('does call api fn if in cache but expired', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = {...config[0], ttl: -1}; | ||
const xOrg = {id: 1, name: 'Kalle'}; | ||
const aFn = sinon.spy(() => { | ||
return Promise.resolve(xOrg); | ||
}); | ||
const res = decorateRead({}, es, qc, e, aFn); | ||
}); | ||
}); | ||
it('calls api fn if not in cache', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const xOrg = {id: 1, name: 'Kalle'}; | ||
const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg)); | ||
const aFn = sinon.spy(aFnWithoutSpy); | ||
const res = decorateRead({}, es, qc, e, aFn); | ||
res(1).then(() => { | ||
expect(aFn.callCount).to.equal(1); | ||
done(); | ||
}); | ||
}); | ||
it('does not call api fn if in cache', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const xOrg = {id: 1, name: 'Kalle'}; | ||
const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg)); | ||
const aFn = sinon.spy(aFnWithoutSpy); | ||
const res = decorateRead({}, es, qc, e, aFn); | ||
const firstCall = res(1); | ||
const firstCall = res(1); | ||
firstCall.then(() => { | ||
res(1).then(() => { | ||
expect(aFn.callCount).to.equal(2); | ||
done(); | ||
}); | ||
}); | ||
firstCall.then(() => { | ||
res(1).then(() => { | ||
expect(aFn.callCount).to.equal(1); | ||
done(); | ||
}); | ||
it('calls api fn if not in cache (plural)', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const xOrg = [{id: 1, name: 'Kalle'}]; | ||
const aFn = sinon.spy(() => { | ||
return Promise.resolve(xOrg); | ||
}); | ||
const res = decorateRead({}, es, qc, e, aFn); | ||
res(1).then((x) => { | ||
expect(x).to.equal(xOrg); | ||
done(); | ||
}); | ||
}); | ||
it('does not call api fn if in cache (plural)', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const xOrg = [{id: 1, name: 'Kalle'}]; | ||
const aFn = sinon.spy(() => { | ||
return Promise.resolve(xOrg); | ||
}); | ||
const res = decorateRead({}, es, qc, e, aFn); | ||
}); | ||
}); | ||
it('does call api fn if in cache but expired', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = {...config[0], ttl: -1}; | ||
const xOrg = {id: 1, name: 'Kalle'}; | ||
const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg)); | ||
const aFn = sinon.spy(aFnWithoutSpy); | ||
const res = decorateRead({}, es, qc, e, aFn); | ||
const firstCall = res(1); | ||
const firstCall = res(1); | ||
firstCall.then(() => { | ||
res(1).then(() => { | ||
expect(aFn.callCount).to.equal(1); | ||
done(); | ||
}); | ||
}); | ||
firstCall.then(() => { | ||
res(1).then(() => { | ||
expect(aFn.callCount).to.equal(2); | ||
done(); | ||
}); | ||
it('does call api fn if in cache but expired (plural)', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = {...config[0], ttl: -1}; | ||
const xOrg = [{id: 1, name: 'Kalle'}]; | ||
const aFn = sinon.spy(() => { | ||
return Promise.resolve(xOrg); | ||
}); | ||
const res = decorateRead({}, es, qc, e, aFn); | ||
}); | ||
}); | ||
it('calls api fn if not in cache (plural)', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const xOrg = [{id: 1, name: 'Kalle'}]; | ||
const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg)); | ||
const aFn = sinon.spy(aFnWithoutSpy); | ||
const res = decorateRead({}, es, qc, e, aFn); | ||
res(1).then((x) => { | ||
expect(x).to.equal(xOrg); | ||
done(); | ||
}); | ||
}); | ||
it('does not call api fn if in cache (plural)', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const xOrg = [{id: 1, name: 'Kalle'}]; | ||
const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg)); | ||
const aFn = sinon.spy(aFnWithoutSpy); | ||
const res = decorateRead({}, es, qc, e, aFn); | ||
const firstCall = res(1); | ||
const firstCall = res(1); | ||
firstCall.then(() => { | ||
res(1).then(() => { | ||
expect(aFn.callCount).to.equal(2); | ||
done(); | ||
}); | ||
}); | ||
firstCall.then(() => { | ||
res(1).then(() => { | ||
expect(aFn.callCount).to.equal(1); | ||
done(); | ||
}); | ||
it('throws if id is missing', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = {...config[0], ttl: 300}; | ||
const xOrg = {name: 'Kalle'}; | ||
const aFn = sinon.spy(() => { | ||
return Promise.resolve(xOrg); | ||
}); | ||
const res = decorateRead({}, es, qc, e, aFn); | ||
}); | ||
}); | ||
it('does call api fn if in cache but expired (plural)', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = {...config[0], ttl: -1}; | ||
const xOrg = [{id: 1, name: 'Kalle'}]; | ||
const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg)); | ||
const aFn = sinon.spy(aFnWithoutSpy); | ||
const res = decorateRead({}, es, qc, e, aFn); | ||
res().catch(e => { | ||
expect(e).to.be.a('Error'); | ||
done(); | ||
}); | ||
const firstCall = res(1); | ||
firstCall.then(() => { | ||
res(1).then(() => { | ||
expect(aFn.callCount).to.equal(2); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
it('throws if id is missing', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = {...config[0], ttl: 300}; | ||
const xOrg = {name: 'Kalle'}; | ||
const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg)); | ||
const aFn = sinon.spy(aFnWithoutSpy); | ||
const res = decorateRead({}, es, qc, e, aFn); | ||
res().catch(err => { | ||
expect(err).to.be.a('Error'); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
}); |
@@ -1,12 +0,12 @@ | ||
import {put} from 'entity-store'; | ||
import {invalidate} from 'query-cache'; | ||
import {passThrough} from 'fp'; | ||
import {addId} from 'id-helper'; | ||
import {put} from '../entity-store'; | ||
import {invalidate} from '../query-cache'; | ||
import {passThrough} from '../fp'; | ||
import {addId} from '../id-helper'; | ||
export function decorateUpdate(c, es, qc, e, aFn) { | ||
return (eValue, ...args) => { | ||
put(es, e, addId(c, undefined, undefined, eValue)); | ||
return aFn(eValue, ...args) | ||
.then(passThrough(() => invalidate(qc, e, aFn))); | ||
}; | ||
return (eValue, ...args) => { | ||
return aFn(eValue, ...args) | ||
.then(passThrough(() => invalidate(qc, e, aFn))) | ||
.then(passThrough(() => put(es, e, addId(c, undefined, undefined, eValue)))); | ||
}; | ||
} |
@@ -0,58 +1,58 @@ | ||
import sinon from 'sinon'; | ||
import {decorateUpdate} from './update'; | ||
import {createEntityStore, get} from 'entity-store'; | ||
import {createQueryCache} from 'query-cache'; | ||
import sinon from 'sinon'; | ||
import {createEntityStore, get} from '../entity-store'; | ||
import {createQueryCache} from '../query-cache'; | ||
import {createApiFunction} from '../test-helper'; | ||
const config = [ | ||
{ | ||
name: 'user', | ||
ttl: 300, | ||
api: { | ||
getUsers: (x) => x, | ||
getUsers2: (x) => x, | ||
deleteUser: (x) => x, | ||
}, | ||
invalidates: ['user'], | ||
invalidatesOn: ['GET'] | ||
{ | ||
name: 'user', | ||
ttl: 300, | ||
api: { | ||
getUsers: (x) => x, | ||
getUsers2: (x) => x, | ||
deleteUser: (x) => x | ||
}, | ||
{ | ||
name: 'userPreview', | ||
ttl: 200, | ||
api: { | ||
getPreviews: (x) => x, | ||
updatePreview: (x) => x, | ||
}, | ||
invalidates: ['fda'], | ||
viewOf: 'user' | ||
invalidates: ['user'], | ||
invalidatesOn: ['GET'] | ||
}, | ||
{ | ||
name: 'userPreview', | ||
ttl: 200, | ||
api: { | ||
getPreviews: (x) => x, | ||
updatePreview: (x) => x | ||
}, | ||
{ | ||
name: 'listUser', | ||
ttl: 200, | ||
api: { | ||
getPreviews: (x) => x, | ||
updatePreview: (x) => x, | ||
}, | ||
invalidates: ['fda'], | ||
viewOf: 'user' | ||
} | ||
invalidates: ['fda'], | ||
viewOf: 'user' | ||
}, | ||
{ | ||
name: 'listUser', | ||
ttl: 200, | ||
api: { | ||
getPreviews: (x) => x, | ||
updatePreview: (x) => x | ||
}, | ||
invalidates: ['fda'], | ||
viewOf: 'user' | ||
} | ||
]; | ||
describe('Update', () => { | ||
describe('decorateUpdate', () => { | ||
it('Updates cache based on argument', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const xOrg = {id: 1, name: 'Kalle'}; | ||
const aFn = sinon.spy(() => { | ||
return Promise.resolve({}); | ||
}); | ||
describe('decorateUpdate', () => { | ||
it('Updates cache based on argument', (done) => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const xOrg = {id: 1, name: 'Kalle'}; | ||
const aFnWithoutSpy = createApiFunction(() => Promise.resolve(xOrg)); | ||
const aFn = sinon.spy(aFnWithoutSpy); | ||
const res = decorateUpdate({}, es, qc, e, aFn); | ||
res(xOrg).then(() => { | ||
expect(get(es, e, 1).value).to.deep.equal({...xOrg, __ladda__id: 1}); | ||
done(); | ||
}); | ||
}); | ||
const res = decorateUpdate({}, es, qc, e, aFn); | ||
res(xOrg, 'other args').then(() => { | ||
expect(get(es, e, 1).value).to.deep.equal({...xOrg, __ladda__id: 1}); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
}); |
@@ -15,3 +15,4 @@ /* A data structure that is aware of views and entities. | ||
import {merge} from './merger'; | ||
import {curry, reduce, map_, clone} from 'fp'; | ||
import {curry, reduce, map_, clone, noop} from './fp'; | ||
import {removeId} from './id-helper'; | ||
@@ -25,3 +26,3 @@ // Value -> StoreValue | ||
// EntityStore -> String -> Value -> () | ||
const set = ([eMap, s], k, v) => s[k] = toStoreValue(clone(v)); | ||
const set = ([eMap, s], k, v) => { s[k] = toStoreValue(clone(v)); }; | ||
@@ -36,5 +37,5 @@ // EntityStore -> String -> () | ||
const rmViews = ([eMap, s], e) => { | ||
const entityType = getEntityType(e); | ||
const toRemove = [...eMap[entityType]]; | ||
map_(rm([eMap, s]), toRemove); | ||
const entityType = getEntityType(e); | ||
const toRemove = [...eMap[entityType]]; | ||
map_(rm([eMap, s]), toRemove); | ||
}; | ||
@@ -44,3 +45,3 @@ | ||
const createEntityKey = (e, v) => { | ||
return getEntityType(e) + v.__ladda__id; | ||
return getEntityType(e) + v.__ladda__id; | ||
}; | ||
@@ -50,3 +51,3 @@ | ||
const createViewKey = (e, v) => { | ||
return e.name + v.__ladda__id; | ||
return e.name + v.__ladda__id; | ||
}; | ||
@@ -57,15 +58,18 @@ | ||
// EntityStore -> Entity -> String -> () | ||
export const remove = (es, e, id) => { | ||
rm(es, createEntityKey(e, {__ladda__id: id})); | ||
rmViews(es, e); | ||
}; | ||
// 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)) { | ||
return viewHandler(s, e, v); | ||
} else { | ||
return entityHandler(s, e, v); | ||
} | ||
if (isView(e)) { | ||
return viewHandler(s, e, v); | ||
} | ||
return entityHandler(s, e, v); | ||
}); | ||
@@ -78,8 +82,8 @@ | ||
const setEntityValue = (s, e, v) => { | ||
if (!v.__ladda__id) { | ||
throw new Error(`Value is missing id, tried to add to entity ${e.name}`); | ||
} | ||
const k = createEntityKey(e, v); | ||
set(s, k, v); | ||
return v; | ||
if (!v.__ladda__id) { | ||
throw new Error(`Value is missing id, tried to add to entity ${e.name}`); | ||
} | ||
const k = createEntityKey(e, v); | ||
set(s, k, v); | ||
return v; | ||
}; | ||
@@ -89,25 +93,31 @@ | ||
const setViewValue = (s, e, v) => { | ||
if (!v.__ladda__id) { | ||
throw new Error(`Value is missing id, tried to add to view ${e.name}`); | ||
} | ||
if (!v.__ladda__id) { | ||
throw new Error(`Value is missing id, tried to add to view ${e.name}`); | ||
} | ||
if (entityValueExist(s, e, v)) { | ||
const eValue = read(s, createEntityKey(e, v)).value; | ||
setEntityValue(s, e, merge(v, eValue)); | ||
rmViews(s, e); // all views will prefer entity cache since it is newer | ||
} else { | ||
const k = createViewKey(e, v); | ||
set(s, k, v); | ||
} | ||
if (entityValueExist(s, e, v)) { | ||
const eValue = read(s, createEntityKey(e, v)).value; | ||
setEntityValue(s, e, merge(v, eValue)); | ||
rmViews(s, e); // all views will prefer entity cache since it is newer | ||
} else { | ||
const k = createViewKey(e, v); | ||
set(s, k, v); | ||
} | ||
return v; | ||
return 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 -> () | ||
export const put = handle(setViewValue, setEntityValue); | ||
export const put = curry((es, e, x) => mPut(es, e, [x])); | ||
// EntityStore -> Entity -> String -> Value | ||
const getEntityValue = (s, e, id) => { | ||
const k = createEntityKey(e, {__ladda__id: id}); | ||
return read(s, k); | ||
const k = createEntityKey(e, {__ladda__id: id}); | ||
return read(s, k); | ||
}; | ||
@@ -117,11 +127,10 @@ | ||
const getViewValue = (s, e, id) => { | ||
const entityValue = read(s, createEntityKey(e, {__ladda__id: id})); | ||
const viewValue = read(s, createViewKey(e, {__ladda__id: id})); | ||
const onlyViewValueExist = viewValue && !entityValue; | ||
const entityValue = read(s, createEntityKey(e, {__ladda__id: id})); | ||
const viewValue = read(s, createViewKey(e, {__ladda__id: id})); | ||
const onlyViewValueExist = viewValue && !entityValue; | ||
if (onlyViewValueExist) { | ||
return viewValue; | ||
} else { | ||
return entityValue; | ||
} | ||
if (onlyViewValueExist) { | ||
return viewValue; | ||
} | ||
return entityValue; | ||
}; | ||
@@ -132,2 +141,12 @@ | ||
// EntityStore -> Entity -> String -> () | ||
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]); | ||
} | ||
}; | ||
// EntityStore -> Entity -> String -> Bool | ||
@@ -137,22 +156,22 @@ export const contains = (es, e, id) => !!handle(getViewValue, getEntityValue)(es, e, id); | ||
// EntityStore -> Entity -> EntityStore | ||
const registerView = ([eMap, store], e) => { | ||
if (!eMap[e.viewOf]) { | ||
eMap[e.viewOf] = []; | ||
} | ||
eMap[e.viewOf].push(e.name); | ||
return [eMap, store]; | ||
const registerView = ([eMap, ...other], e) => { | ||
if (!eMap[e.viewOf]) { | ||
eMap[e.viewOf] = []; | ||
} | ||
eMap[e.viewOf].push(e.name); | ||
return [eMap, ...other]; | ||
}; | ||
// EntityStore -> Entity -> EntityStore | ||
const registerEntity = ([eMap, store], e) => { | ||
if (!eMap[e.name]) { | ||
eMap[e.name] = []; | ||
} | ||
return [eMap, store]; | ||
const registerEntity = ([eMap, ...other], e) => { | ||
if (!eMap[e.name]) { | ||
eMap[e.name] = []; | ||
} | ||
return [eMap, ...other]; | ||
}; | ||
// EntityStore -> Entity -> EntityStore | ||
const updateIndex = (m, e) => isView(e) ? registerView(m, e) : registerEntity(m, e); | ||
const updateIndex = (m, e) => { return isView(e) ? registerView(m, e) : registerEntity(m, e); }; | ||
// [Entity] -> EntityStore | ||
export const createEntityStore = c => reduce(updateIndex, [{}, {}], c); | ||
export const createEntityStore = (c, hook = noop) => reduce(updateIndex, [{}, {}, hook], c); |
@@ -1,218 +0,290 @@ | ||
import {createEntityStore, put, get, contains, remove} from './entity-store'; | ||
import {addId} from 'id-helper'; | ||
/* eslint-disable no-unused-expressions */ | ||
import sinon from 'sinon'; | ||
import {createEntityStore, put, mPut, get, contains, remove} from './entity-store'; | ||
import {addId} from './id-helper'; | ||
const config = [ | ||
{ | ||
name: 'user', | ||
ttl: 300, | ||
api: { | ||
getUsers: (x) => x, | ||
deleteUser: (x) => x, | ||
}, | ||
invalidates: ['alles'] | ||
{ | ||
name: 'user', | ||
ttl: 300, | ||
api: { | ||
getUsers: (x) => x, | ||
deleteUser: (x) => x | ||
}, | ||
{ | ||
name: 'userPreview', | ||
ttl: 200, | ||
api: { | ||
getPreviews: (x) => x, | ||
updatePreview: (x) => x, | ||
}, | ||
invalidates: ['fda'], | ||
viewOf: 'user' | ||
invalidates: ['alles'] | ||
}, | ||
{ | ||
name: 'userPreview', | ||
ttl: 200, | ||
api: { | ||
getPreviews: (x) => x, | ||
updatePreview: (x) => x | ||
}, | ||
{ | ||
name: 'listUser', | ||
ttl: 200, | ||
api: { | ||
getPreviews: (x) => x, | ||
updatePreview: (x) => x, | ||
}, | ||
invalidates: ['fda'], | ||
viewOf: 'user' | ||
} | ||
invalidates: ['fda'], | ||
viewOf: 'user' | ||
}, | ||
{ | ||
name: 'listUser', | ||
ttl: 200, | ||
api: { | ||
getPreviews: (x) => x, | ||
updatePreview: (x) => x | ||
}, | ||
invalidates: ['fda'], | ||
viewOf: 'user' | ||
} | ||
]; | ||
describe('EntityStore', () => { | ||
describe('createEntityStore', () => { | ||
it('returns store', () => { | ||
const s = createEntityStore(config); | ||
expect(s).to.be.ok; | ||
}); | ||
it('returns store', () => { | ||
const myConfig = [ | ||
{ | ||
name: 'userPreview', | ||
viewOf: 'user' | ||
}, | ||
{ | ||
name: 'user' | ||
} | ||
]; | ||
const s = createEntityStore(myConfig); | ||
expect(s).to.be.ok; | ||
}); | ||
describe('createEntityStore', () => { | ||
it('returns store', () => { | ||
const s = createEntityStore(config); | ||
expect(s).to.be.ok; | ||
}); | ||
it('returns store', () => { | ||
const myConfig = [ | ||
{ | ||
name: 'userPreview', | ||
viewOf: 'user' | ||
}, | ||
{ | ||
name: 'user' | ||
} | ||
]; | ||
const s = createEntityStore(myConfig); | ||
expect(s).to.be.ok; | ||
}); | ||
}); | ||
describe('put', () => { | ||
it('an added value is later returned when calling get', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello'}; | ||
const e = { name: 'user'}; | ||
put(s, e, addId({}, undefined, undefined, v)); | ||
const r = get(s, e, v.id); | ||
expect(r.value).to.deep.equal({...v, __ladda__id: 'hello'}); | ||
}); | ||
it('altering an added value does not alter the stored value when doing a get later', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello', name: 'kalle'}; | ||
const e = { name: 'user'}; | ||
put(s, e, addId({}, undefined, undefined, v)); | ||
v.name = 'ingvar'; | ||
const r = get(s, e, v.id); | ||
expect(r.value.name).to.equal('kalle'); | ||
}); | ||
it('an added value to a view is later returned when calling get for view', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello'}; | ||
const e = { name: 'user'}; | ||
put(s, e, addId({}, undefined, undefined, v)); | ||
const r = get(s, e, v.id); | ||
expect(r.value).to.deep.equal({...v, __ladda__id: 'hello'}); | ||
}); | ||
it('merges view into entity value', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello'}; | ||
const e = {name: 'user'}; | ||
const eView = {name: 'userPreview', viewOf: 'user'}; | ||
put(s, e, addId({}, undefined, undefined, {...v, name: 'kalle'})); | ||
put(s, eView, addId({}, undefined, undefined, {...v, name: 'ingvar'})); | ||
const r = get(s, eView, v.id); | ||
expect(r.value).to.be.deep.equal({__ladda__id: 'hello', id: 'hello', name: 'ingvar'}); | ||
}); | ||
it('writing view value without id throws error', () => { | ||
const s = createEntityStore(config); | ||
const v = {aid: 'hello'}; | ||
const eView = {name: 'userPreview', viewOf: 'user'}; | ||
const write = () => put(s, eView, addId({}, undefined, undefined, {...v, name: 'kalle'})); | ||
expect(write).to.throw(Error); | ||
}); | ||
it('writing entitiy value without id throws error', () => { | ||
const s = createEntityStore(config); | ||
const v = {aid: 'hello'}; | ||
const e = {name: 'user'}; | ||
const write = () => put(s, e, addId({}, undefined, undefined, {...v, name: 'kalle'})); | ||
expect(write).to.throw(Error); | ||
}); | ||
}); | ||
describe('mPut', () => { | ||
it('adds values which are later returned when calling get', () => { | ||
const s = createEntityStore(config); | ||
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]); | ||
const r1 = get(s, e, v1.id); | ||
const r2 = get(s, e, v2.id); | ||
expect(r1.value).to.deep.equal({...v1, __ladda__id: 'hello'}); | ||
expect(r2.value).to.deep.equal({...v2, __ladda__id: 'there'}); | ||
}); | ||
}); | ||
describe('get', () => { | ||
it('gets value with timestamp', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello'}; | ||
const e = { name: 'user'}; | ||
put(s, e, addId({}, undefined, undefined, v)); | ||
const r = get(s, e, v.id); | ||
expect(r.timestamp).to.not.be.undefined; | ||
}); | ||
it('altering retrieved value does not alter the stored value', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello', name: 'kalle'}; | ||
const e = { name: 'user'}; | ||
put(s, e, addId({}, undefined, undefined, v)); | ||
const r = get(s, e, v.id); | ||
r.value.name = 'ingvar'; | ||
const r2 = get(s, e, v.id); | ||
expect(r2.value.name).to.equal(v.name); | ||
}); | ||
it('gets undefined if value does not exist', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello'}; | ||
const e = { name: 'user'}; | ||
const r = get(s, e, v.id); | ||
expect(r).to.be.undefined; | ||
}); | ||
it('gets undefined for view if not existing', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello'}; | ||
const e = {name: 'userPreview', viewOf: 'user'}; | ||
const r = get(s, e, v.id); | ||
expect(r).to.be.undefined; | ||
}); | ||
it('gets entity of view if only it exist', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello'}; | ||
const e = {name: 'user'}; | ||
put(s, e, addId({}, undefined, undefined, v)); | ||
const eView = {name: 'userPreview', viewOf: 'user'}; | ||
const r = get(s, eView, v.id); | ||
expect(r.value).to.be.deep.equal({...v, __ladda__id: 'hello'}); | ||
}); | ||
it('gets view if only it exist', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello'}; | ||
const eView = {name: 'userPreview', viewOf: 'user'}; | ||
put(s, eView, addId({}, undefined, undefined, v)); | ||
const r = get(s, eView, v.id); | ||
expect(r.value).to.be.deep.equal({...v, __ladda__id: 'hello'}); | ||
}); | ||
it('gets entity value if same timestamp as view value', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello'}; | ||
const e = {name: 'user'}; | ||
const eView = {name: 'userPreview', viewOf: 'user'}; | ||
put(s, eView, addId({}, undefined, undefined, v)); | ||
put(s, e, addId({}, undefined, undefined, {...v, name: 'kalle'})); | ||
const r = get(s, eView, v.id); | ||
expect(r.value).to.be.deep.equal({...v, name: 'kalle', __ladda__id: 'hello'}); | ||
}); | ||
it('gets entity value if newer than view value', (done) => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello'}; | ||
const e = {name: 'user'}; | ||
const eView = {name: 'userPreview', viewOf: 'user'}; | ||
put(s, eView, addId({}, undefined, undefined, v)); | ||
setTimeout(() => { | ||
put(s, e, addId({}, undefined, undefined, {...v, name: 'kalle'})); | ||
const r = get(s, eView, v.id); | ||
expect(r.value).to.be.deep.equal({...v, name: 'kalle', __ladda__id: 'hello'}); | ||
done(); | ||
}, 1); | ||
}); | ||
}); | ||
describe('contains', () => { | ||
it('true if value exist', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello'}; | ||
const e = { name: 'user'}; | ||
put(s, e, addId({}, undefined, undefined, v)); | ||
const r = contains(s, e, v.id); | ||
expect(r).to.be.true; | ||
}); | ||
it('false if no value exist', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello'}; | ||
const e = { name: 'user'}; | ||
const r = contains(s, e, v.id); | ||
expect(r).to.be.false; | ||
}); | ||
}); | ||
describe('remove', () => { | ||
it('removes an existing value', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello'}; | ||
const e = { name: 'user'}; | ||
put(s, e, addId({}, undefined, undefined, v)); | ||
remove(s, e, v.id); | ||
const r = contains(s, e, v.id); | ||
expect(r).to.be.false; | ||
}); | ||
it('does not crash when removing not existing value', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello'}; | ||
const e = { name: 'user'}; | ||
const fn = () => remove(s, e, v.id); | ||
expect(fn).to.not.throw(); | ||
}); | ||
}); | ||
describe('with a hook', () => { | ||
describe('put', () => { | ||
it('an added value is later returned when calling get', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello'}; | ||
const e = { name: 'user'}; | ||
put(s, e, addId({}, undefined, undefined, v)); | ||
const r = get(s, e, v.id); | ||
expect(r.value).to.deep.equal({...v, __ladda__id: 'hello'}); | ||
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] | ||
}); | ||
it('altering an added value does not alter the stored value when doing a get later', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello', name: 'kalle'}; | ||
const e = { name: 'user'}; | ||
put(s, e, addId({}, undefined, undefined, v)); | ||
v.name = 'ingvar'; | ||
const r = get(s, e, v.id); | ||
expect(r.value.name).to.equal('kalle'); | ||
}); | ||
it('an added value to a view is later returned when calling get for view', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello'}; | ||
const e = { name: 'user'}; | ||
put(s, e, addId({}, undefined, undefined, v)); | ||
const r = get(s, e, v.id); | ||
expect(r.value).to.deep.equal({...v, __ladda__id: 'hello'}); | ||
}); | ||
it('merges view into entity value', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello'}; | ||
const e = {name: 'user'}; | ||
const eView = {name: 'userPreview', viewOf: 'user'}; | ||
put(s, e, addId({}, undefined, undefined, {...v, name: 'kalle'})); | ||
put(s, eView, addId({}, undefined, undefined, {...v, name: 'ingvar'})); | ||
const r = get(s, eView, v.id); | ||
expect(r.value).to.be.deep.equal({__ladda__id: 'hello', id: 'hello', name: 'ingvar'}); | ||
}); | ||
it('writing view value without id throws error', () => { | ||
const s = createEntityStore(config); | ||
const v = {aid: 'hello'}; | ||
const eView = {name: 'userPreview', viewOf: 'user'}; | ||
const write = () => put(s, eView, addId({}, undefined, undefined, {...v, name: 'kalle'})); | ||
expect(write).to.throw(Error); | ||
}); | ||
it('writing entitiy value without id throws error', () => { | ||
const s = createEntityStore(config); | ||
const v = {aid: 'hello'}; | ||
const e = {name: 'user'}; | ||
const write = () => put(s, e, addId({}, undefined, undefined, {...v, name: 'kalle'})); | ||
expect(write).to.throw(Error); | ||
}); | ||
}); | ||
}); | ||
describe('get', () => { | ||
it('gets value with timestamp', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello'}; | ||
const e = { name: 'user'}; | ||
put(s, e, addId({}, undefined, undefined, v)); | ||
const r = get(s, e, v.id); | ||
expect(r.timestamp).to.not.be.undefined; | ||
}); | ||
it('altering retrieved value does not alter the stored value', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello', name: 'kalle'}; | ||
const e = { name: 'user'}; | ||
put(s, e, addId({}, undefined, undefined, v)); | ||
const r = get(s, e, v.id); | ||
r.value.name = 'ingvar'; | ||
const r2 = get(s, e, v.id); | ||
expect(r2.value.name).to.equal(v.name); | ||
}); | ||
it('gets undefined if value does not exist', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello'}; | ||
const e = { name: 'user'}; | ||
const r = get(s, e, v.id); | ||
expect(r).to.be.undefined; | ||
}); | ||
it('gets undefined for view if not existing', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello'}; | ||
const e = {name: 'userPreview', viewOf: 'user'}; | ||
const r = get(s, e, v.id); | ||
expect(r).to.be.undefined; | ||
}); | ||
it('gets entity of view if only it exist', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello'}; | ||
const e = {name: 'user'}; | ||
put(s, e, addId({}, undefined, undefined, v)); | ||
const eView = {name: 'userPreview', viewOf: 'user'}; | ||
const r = get(s, eView, v.id); | ||
expect(r.value).to.be.deep.equal({...v, __ladda__id: 'hello'}); | ||
}); | ||
it('gets view if only it exist', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello'}; | ||
const e = {name: 'user'}; | ||
const eView = {name: 'userPreview', viewOf: 'user'}; | ||
put(s, eView, addId({}, undefined, undefined, v)); | ||
const r = get(s, eView, v.id); | ||
expect(r.value).to.be.deep.equal({...v, __ladda__id: 'hello'}); | ||
}); | ||
it('gets entity value if same timestamp as view value', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello'}; | ||
const e = {name: 'user'}; | ||
const eView = {name: 'userPreview', viewOf: 'user'}; | ||
put(s, eView, addId({}, undefined, undefined, v)); | ||
put(s, e, addId({}, undefined, undefined, {...v, name: 'kalle'})); | ||
const r = get(s, eView, v.id); | ||
expect(r.value).to.be.deep.equal({...v, name: 'kalle', __ladda__id: 'hello'}); | ||
}); | ||
it('gets entity value if newer than view value', (done) => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello'}; | ||
const e = {name: 'user'}; | ||
const eView = {name: 'userPreview', viewOf: 'user'}; | ||
put(s, eView, addId({}, undefined, undefined, v)); | ||
setTimeout(() => { | ||
put(s, e, addId({}, undefined, undefined, {...v, name: 'kalle'})); | ||
const r = get(s, eView, v.id); | ||
expect(r.value).to.be.deep.equal({...v, name: 'kalle', __ladda__id: 'hello'}); | ||
done(); | ||
}, 1); | ||
}); | ||
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('contains', () => { | ||
it('true if value exist', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello'}; | ||
const e = { name: 'user'}; | ||
put(s, e, addId({}, undefined, undefined, v)); | ||
const r = contains(s, e, v.id); | ||
expect(r).to.be.true; | ||
}); | ||
it('false if no value exist', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello'}; | ||
const e = { name: 'user'}; | ||
const r = contains(s, e, v.id); | ||
expect(r).to.be.false; | ||
}); | ||
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]); | ||
}); | ||
}); | ||
describe('remove', () => { | ||
it('removes an existing value', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello'}; | ||
const e = { name: 'user'}; | ||
put(s, e, addId({}, undefined, undefined, v)); | ||
remove(s, e, v.id); | ||
const r = contains(s, e, v.id); | ||
expect(r).to.be.false; | ||
}); | ||
it('does not crash when removing not existing value', () => { | ||
const s = createEntityStore(config); | ||
const v = {id: 'hello'}; | ||
const e = { name: 'user'}; | ||
const fn = () => remove(s, e, v.id); | ||
const r = contains(s, e, v.id); | ||
expect(fn).to.not.throw(); | ||
}); | ||
}); | ||
}); | ||
}); |
@@ -1,48 +0,44 @@ | ||
import {serialize} from 'serializer'; | ||
import {curry, map, prop} from 'fp'; | ||
import {serialize} from './serializer'; | ||
import {curry, map, prop} from './fp'; | ||
const getIdGetter = (c, aFn) => { | ||
if (aFn && aFn.idFrom && typeof aFn.idFrom === 'function') { | ||
return aFn.idFrom; | ||
} else { | ||
return prop(c.idField || 'id'); | ||
} | ||
if (aFn && aFn.idFrom && typeof aFn.idFrom === 'function') { | ||
return aFn.idFrom; | ||
} | ||
return prop(c.idField || 'id'); | ||
}; | ||
export const addId = curry((c, aFn, args, o) => { | ||
if (aFn && aFn.idFrom === 'ARGS') { | ||
return { | ||
...o, | ||
__ladda__id: serialize(args) | ||
}; | ||
} else { | ||
const getId = getIdGetter(c, aFn); | ||
if (Array.isArray(o)) { | ||
return map(x => ({ | ||
...x, | ||
__ladda__id: getId(x) | ||
}), o); | ||
} else { | ||
return { | ||
...o, | ||
__ladda__id: getId(o) | ||
}; | ||
} | ||
} | ||
if (aFn && aFn.idFrom === 'ARGS') { | ||
return { | ||
...o, | ||
__ladda__id: serialize(args) | ||
}; | ||
} | ||
const getId = getIdGetter(c, aFn); | ||
if (Array.isArray(o)) { | ||
return map(x => ({ | ||
...x, | ||
__ladda__id: getId(x) | ||
}), o); | ||
} | ||
return { | ||
...o, | ||
__ladda__id: getId(o) | ||
}; | ||
}); | ||
export const removeId = (o) => { | ||
if (!o) { | ||
return o; | ||
} | ||
if (!o) { | ||
return o; | ||
} | ||
if (Array.isArray(o)) { | ||
return map(x => { | ||
delete x.__ladda__id; | ||
return x; | ||
}, o); | ||
} else { | ||
delete o.__ladda__id; | ||
return o; | ||
} | ||
if (Array.isArray(o)) { | ||
return map(x => { | ||
delete x.__ladda__id; | ||
return x; | ||
}, o); | ||
} | ||
delete o.__ladda__id; | ||
return o; | ||
}; |
@@ -1,29 +0,29 @@ | ||
import {addId, removeId} from 'id-helper'; | ||
import {addId, removeId} from './id-helper'; | ||
describe('IdHelper', () => { | ||
it('removeId is the inverse of addId', () => { | ||
const o = {id: 1}; | ||
expect(removeId(addId({}, undefined, undefined, o))).to.deep.equal(o); | ||
}); | ||
it('addId creates id from args if idFrom is ARGS', () => { | ||
const o = {name: 'kalle'}; | ||
const aFn = {idFrom: 'ARGS'}; | ||
expect(addId({}, aFn, [1,2,3], o)).to.deep.equal( | ||
{...o, __ladda__id: '1-2-3'} | ||
); | ||
}); | ||
it('removing id from undefined returns undefined', () => { | ||
const o = undefined; | ||
expect(removeId(o)).to.equal(o); | ||
}); | ||
it('removing id from array remove it from individual elements', () => { | ||
const o = [{__ladda__id: 1, id: 1}, {__ladda__id: 2, id: 2}, {__ladda__id: 3, id: 3}]; | ||
expect(removeId(o)).to.deep.equal([{id: 1}, {id: 2}, {id: 3}]); | ||
}); | ||
it('addId can use a custom function', () => { | ||
const o = {myId: 15}; | ||
const aFn = {idFrom: x => x.myId}; | ||
const res = addId({}, aFn, [1,2,3], o); | ||
expect(res).to.deep.equal({...o, __ladda__id: 15}); | ||
}); | ||
it('removeId is the inverse of addId', () => { | ||
const o = {id: 1}; | ||
expect(removeId(addId({}, undefined, undefined, o))).to.deep.equal(o); | ||
}); | ||
it('addId creates id from args if idFrom is ARGS', () => { | ||
const o = {name: 'kalle'}; | ||
const aFn = {idFrom: 'ARGS'}; | ||
expect(addId({}, aFn, [1, 2, 3], o)).to.deep.equal( | ||
{...o, __ladda__id: '1-2-3'} | ||
); | ||
}); | ||
it('removing id from undefined returns undefined', () => { | ||
const o = undefined; | ||
expect(removeId(o)).to.equal(o); | ||
}); | ||
it('removing id from array remove it from individual elements', () => { | ||
const o = [{__ladda__id: 1, id: 1}, {__ladda__id: 2, id: 2}, {__ladda__id: 3, id: 3}]; | ||
expect(removeId(o)).to.deep.equal([{id: 1}, {id: 2}, {id: 3}]); | ||
}); | ||
it('addId can use a custom function', () => { | ||
const o = {myId: 15}; | ||
const aFn = {idFrom: x => x.myId}; | ||
const res = addId({}, aFn, [1, 2, 3], o); | ||
expect(res).to.deep.equal({...o, __ladda__id: 15}); | ||
}); | ||
}); |
export function merge(source, destination) { | ||
const result = { ...destination }; | ||
const result = { ...destination }; | ||
const keysForNonObjects = getNonObjectKeys(source); | ||
keysForNonObjects.forEach(key => { | ||
if (destination[key] !== undefined) { | ||
result[key] = source[key]; | ||
} | ||
}); | ||
const keysForNonObjects = getNonObjectKeys(source); | ||
keysForNonObjects.forEach(key => { | ||
if (destination[key] !== undefined) { | ||
result[key] = source[key]; | ||
} | ||
}); | ||
const keysForObjects = getObjectKeys(source); | ||
keysForObjects.forEach(key => { | ||
if (destination[key] !== undefined) { | ||
result[key] = merge(source[key], destination[key]); | ||
} | ||
}); | ||
const keysForObjects = getObjectKeys(source); | ||
keysForObjects.forEach(key => { | ||
if (destination[key] !== undefined) { | ||
result[key] = merge(source[key], destination[key]); | ||
} | ||
}); | ||
return result; | ||
return result; | ||
} | ||
function getNonObjectKeys(object) { | ||
return Object.keys(object).filter(key => { | ||
return object[key] === null | ||
|| typeof object[key] !== 'object' | ||
|| Array.isArray(object[key]); | ||
}); | ||
return Object.keys(object).filter(key => { | ||
return object[key] === null | ||
|| typeof object[key] !== 'object' | ||
|| Array.isArray(object[key]); | ||
}); | ||
} | ||
function getObjectKeys(object) { | ||
return Object.keys(object).filter(key => { | ||
return object[key] !== null | ||
&& !Array.isArray(object[key]) | ||
&& typeof object[key] === 'object'; | ||
}); | ||
return Object.keys(object).filter(key => { | ||
return object[key] !== null | ||
&& !Array.isArray(object[key]) | ||
&& typeof object[key] === 'object'; | ||
}); | ||
} |
import {merge} from './merger'; | ||
describe('Merger', () => { | ||
it('overwrites stuff in dest', () => { | ||
const src = {a: 'hej'}; | ||
const dest = {a: 'hello'}; | ||
const res = merge(src, dest); | ||
expect(res.a).to.equal('hej'); | ||
}); | ||
it('do not write anything that does not exist in destination object', () => { | ||
const src = {a: 'hej'}; | ||
const dest = {}; | ||
const res = merge(src, dest); | ||
expect(res).to.deep.equal(dest); | ||
}); | ||
it('merge objects in the objects', () => { | ||
const src = {a: {b: {c: 'hello'}}}; | ||
const dest = {a: {b: {c: 'hej'}}}; | ||
const res = merge(src, dest); | ||
expect(res.a.b.c).to.equal('hello'); | ||
}); | ||
it('do not merge objects in the objects of destination lack object', () => { | ||
const src = {a: {b: {c: 'hello'}}}; | ||
const dest = {a: {b: {e: 'hej'}}}; | ||
const res = merge(src, dest); | ||
expect(Object.keys(res.a.b)).to.not.deep.equal(['c']); | ||
}); | ||
it('overwrites stuff in dest', () => { | ||
const src = {a: 'hej'}; | ||
const dest = {a: 'hello'}; | ||
const res = merge(src, dest); | ||
expect(res.a).to.equal('hej'); | ||
}); | ||
it('do not write anything that does not exist in destination object', () => { | ||
const src = {a: 'hej'}; | ||
const dest = {}; | ||
const res = merge(src, dest); | ||
expect(res).to.deep.equal(dest); | ||
}); | ||
it('merge objects in the objects', () => { | ||
const src = {a: {b: {c: 'hello'}}}; | ||
const dest = {a: {b: {c: 'hej'}}}; | ||
const res = merge(src, dest); | ||
expect(res.a.b.c).to.equal('hello'); | ||
}); | ||
it('do not merge objects in the objects of destination lack object', () => { | ||
const src = {a: {b: {c: 'hello'}}}; | ||
const dest = {a: {b: {e: 'hej'}}}; | ||
const res = merge(src, dest); | ||
expect(Object.keys(res.a.b)).to.not.deep.equal(['c']); | ||
}); | ||
it('do not write anything that does not exist in destination object (object)', () => { | ||
const src = {a: {foo: 'bar'}}; | ||
const dest = {}; | ||
const res = merge(src, dest); | ||
expect(res).to.deep.equal(dest); | ||
}); | ||
}); |
@@ -6,6 +6,6 @@ /* Handles queries, in essence all GET operations. | ||
import {put as putInEs, get as getFromEs} from './entity-store'; | ||
import {mPut as mPutInEs, get as getFromEs} from './entity-store'; | ||
import {on2, prop, join, reduce, identity, | ||
curry, map, map_, startsWith, compose, filter} from 'fp'; | ||
import {serialize} from 'serializer'; | ||
curry, map, map_, startsWith, compose, filter} from './fp'; | ||
import {serialize} from './serializer'; | ||
@@ -26,11 +26,11 @@ // Entity -> [String] -> String | ||
const getFromCache = (qc, e, k) => { | ||
const rawValue = toValue(qc.cache[k]); | ||
const getValuesFromEs = compose(filter(identity), map(getFromEs(qc.entityStore, e))); | ||
const value = Array.isArray(rawValue) | ||
? getValuesFromEs(rawValue) | ||
: getFromEs(qc.entityStore, e, rawValue); | ||
return { | ||
...qc.cache[k], | ||
value | ||
}; | ||
const rawValue = toValue(qc.cache[k]); | ||
const getValuesFromEs = compose(filter(identity), map(getFromEs(qc.entityStore, e))); | ||
const value = Array.isArray(rawValue) | ||
? getValuesFromEs(rawValue) | ||
: getFromEs(qc.entityStore, e, rawValue); | ||
return { | ||
...qc.cache[k], | ||
value | ||
}; | ||
}; | ||
@@ -40,10 +40,10 @@ | ||
export const put = curry((qc, e, aFn, args, xs) => { | ||
const k = createKey(e, [aFn.name, ...filter(identity, args)]); | ||
if (Array.isArray(xs)) { | ||
qc.cache[k] = toCacheValue(map(prop('__ladda__id'), xs)); | ||
} else { | ||
qc.cache[k] = toCacheValue(prop('__ladda__id', xs)); | ||
} | ||
map_(putInEs(qc.entityStore, e), Array.isArray(xs) ? xs : [xs]); | ||
return xs; | ||
const k = createKey(e, [aFn.name, ...filter(identity, args)]); | ||
if (Array.isArray(xs)) { | ||
qc.cache[k] = toCacheValue(map(prop('__ladda__id'), xs)); | ||
} else { | ||
qc.cache[k] = toCacheValue(prop('__ladda__id', xs)); | ||
} | ||
mPutInEs(qc.entityStore, e, Array.isArray(xs) ? xs : [xs]); | ||
return xs; | ||
}); | ||
@@ -53,4 +53,3 @@ | ||
export const getValue = (v) => { | ||
return Array.isArray(v) | ||
? map(toValue, v) : toValue(v); | ||
return Array.isArray(v) ? map(toValue, v) : toValue(v); | ||
}; | ||
@@ -60,4 +59,4 @@ | ||
export const contains = (qc, e, aFn, args) => { | ||
const k = createKey(e, [aFn.name, ...filter(identity, args)]); | ||
return inCache(qc, k); | ||
const k = createKey(e, [aFn.name, ...filter(identity, args)]); | ||
return inCache(qc, k); | ||
}; | ||
@@ -67,19 +66,16 @@ | ||
export const get = (qc, e, aFn, args) => { | ||
const k = createKey(e, [aFn.name, ...filter(identity, args)]); | ||
if (!inCache(qc, k)) { | ||
throw new Error( | ||
`Tried to access ${e.name} with key ${k} which doesn't exist. | ||
Do a contains check first!` | ||
); | ||
} | ||
return getFromCache(qc, e, k); | ||
const k = createKey(e, [aFn.name, ...filter(identity, args)]); | ||
if (!inCache(qc, k)) { | ||
throw new Error( | ||
`Tried to access ${e.name} with key ${k} which doesn't exist. | ||
Do a contains check first!` | ||
); | ||
} | ||
return getFromCache(qc, e, k); | ||
}; | ||
// Entity -> [String] | ||
const getInvalidatesOn = e => e.invalidatesOn || ['CREATE', 'UPDATE', 'DELETE']; | ||
// Entity -> Operation -> Bool | ||
const shouldInvalidateEntity = (e, op) => { | ||
const invalidatesOn = getInvalidatesOn(e); | ||
return invalidatesOn && invalidatesOn.indexOf(op) > -1; | ||
const invalidatesOn = e.invalidatesOn; | ||
return invalidatesOn && invalidatesOn.indexOf(op) > -1; | ||
}; | ||
@@ -89,19 +85,19 @@ | ||
const invalidateEntity = curry((qc, entityName) => { | ||
const keys = Object.keys(qc.cache); | ||
const removeIfEntity = k => { | ||
if (startsWith(entityName, k)) { | ||
delete qc.cache[k]; | ||
} | ||
}; | ||
map_(removeIfEntity, keys); | ||
const keys = Object.keys(qc.cache); | ||
const removeIfEntity = k => { | ||
if (startsWith(entityName, k)) { | ||
delete qc.cache[k]; | ||
} | ||
}; | ||
map_(removeIfEntity, keys); | ||
}); | ||
// Object -> [String] | ||
const getInvalidates = x => x.invalidates || []; | ||
const getInvalidates = x => x.invalidates; | ||
// QueryCache -> Entity -> ApiFunction -> () | ||
const invalidateBasedOnEntity = (qc, e, aFn) => { | ||
if (shouldInvalidateEntity(e, aFn.operation)) { | ||
map_(invalidateEntity(qc), getInvalidates(e)); | ||
} | ||
if (shouldInvalidateEntity(e, aFn.operation)) { | ||
map_(invalidateEntity(qc), getInvalidates(e)); | ||
} | ||
}; | ||
@@ -111,5 +107,5 @@ | ||
const invalidateBasedOnApiFn = (qc, e, aFn) => { | ||
const prependEntity = x => `${e.name}-${x}`; | ||
const invalidateEntityByApiFn = compose(invalidateEntity(qc), prependEntity); | ||
map_(invalidateEntityByApiFn, getInvalidates(aFn)); | ||
const prependEntity = x => `${e.name}-${x}`; | ||
const invalidateEntityByApiFn = compose(invalidateEntity(qc), prependEntity); | ||
map_(invalidateEntityByApiFn, getInvalidates(aFn)); | ||
}; | ||
@@ -119,4 +115,4 @@ | ||
export const invalidate = (qc, e, aFn) => { | ||
invalidateBasedOnEntity(qc, e, aFn); | ||
invalidateBasedOnApiFn(qc, e, aFn); | ||
invalidateBasedOnEntity(qc, e, aFn); | ||
invalidateBasedOnApiFn(qc, e, aFn); | ||
}; | ||
@@ -126,3 +122,3 @@ | ||
export const createQueryCache = (es) => { | ||
return {entityStore: es, cache: {}}; | ||
return {entityStore: es, cache: {}}; | ||
}; |
@@ -0,154 +1,116 @@ | ||
/* eslint-disable no-unused-expressions */ | ||
import {createEntityStore} from './entity-store'; | ||
import {createQueryCache, getValue, put, contains, get, invalidate} from './query-cache'; | ||
import {addId} from 'id-helper'; | ||
import {addId} from './id-helper'; | ||
import {createSampleConfig, createApiFunction} from './test-helper'; | ||
const config = [ | ||
{ | ||
name: 'user', | ||
ttl: 300, | ||
api: { | ||
getUsers: (x) => x, | ||
getUsers2: (x) => x, | ||
deleteUser: (x) => x, | ||
}, | ||
invalidates: ['user'], | ||
invalidatesOn: ['READ'] | ||
}, | ||
{ | ||
name: 'userPreview', | ||
ttl: 200, | ||
api: { | ||
getPreviews: (x) => x, | ||
updatePreview: (x) => x, | ||
}, | ||
invalidates: ['fds'], | ||
viewOf: 'user' | ||
}, | ||
{ | ||
name: 'cars', | ||
ttl: 200, | ||
api: { | ||
getCars: (x) => x, | ||
updateCar: (x) => x, | ||
}, | ||
invalidates: ['user'], | ||
viewOf: 'user' | ||
}, | ||
{ | ||
name: 'bikes', | ||
ttl: 200, | ||
api: { | ||
getCars: (x) => x, | ||
updateCar: (x) => x, | ||
} | ||
} | ||
]; | ||
const config = createSampleConfig(); | ||
describe('QueryCache', () => { | ||
describe('createQueryCache', () => { | ||
it('Returns an object', () => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
expect(qc).to.be.a('object'); | ||
}); | ||
describe('createQueryCache', () => { | ||
it('Returns an object', () => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
expect(qc).to.be.a('object'); | ||
}); | ||
describe('getValue', () => { | ||
it('extracts values from an array of cache values and returns these', () => { | ||
const expected = [[1, 2, 3], [4, 5, 6]]; | ||
const data = [{value: [1, 2, 3]}, {value: [4, 5, 6]}]; | ||
expect(getValue(data)).to.deep.equal(expected); | ||
}); | ||
it('extracts values from a cache value and returns it', () => { | ||
const expected = [1, 2, 3]; | ||
const data = {value: [1, 2, 3]}; | ||
expect(getValue(data)).to.deep.equal(expected); | ||
}); | ||
}); | ||
describe('getValue', () => { | ||
it('extracts values from an array of cache values and returns these', () => { | ||
const expected = [[1, 2, 3], [4, 5, 6]]; | ||
const data = [{value: [1, 2, 3]}, {value: [4, 5, 6]}]; | ||
expect(getValue(data)).to.deep.equal(expected); | ||
}); | ||
describe('contains & put', () => { | ||
it('if an element exist, return true', () => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const aFn = (x) => x; | ||
const args = [1, 2, 3]; | ||
const xs = [{id: 1}, {id: 2}, {id: 3}]; | ||
put(qc, e, aFn, args, addId({}, undefined, undefined, xs)); | ||
expect(contains(qc, e, aFn, args)).to.be.true; | ||
}); | ||
it('if an element exist, and args contains a complex object, return true', () => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const aFn = (x) => x; | ||
const args = [1, {hello: {world: 'Kalle'}}, 3]; | ||
const xs = [{id: 1}, {id: 2}, {id: 3}]; | ||
put(qc, e, aFn, args, addId({}, undefined, undefined, xs)); | ||
expect(contains(qc, e, aFn, args)).to.be.true; | ||
}); | ||
it('if an element exist, and args contains a simple object, return true', () => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const aFn = (x) => x; | ||
const args = [1, {hello: 'world'}, 3]; | ||
const xs = [{id: 1}, {id: 2}, {id: 3}]; | ||
put(qc, e, aFn, args, addId({}, undefined, undefined, xs)); | ||
expect(contains(qc, e, aFn, args)).to.be.true; | ||
}); | ||
it('if an element does not exist, return false', () => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const aFn = (x) => x; | ||
const args = [1, 2, 3]; | ||
expect(contains(qc, e, aFn, args)).to.be.false; | ||
}); | ||
it('extracts values from a cache value and returns it', () => { | ||
const expected = [1, 2, 3]; | ||
const data = {value: [1, 2, 3]}; | ||
expect(getValue(data)).to.deep.equal(expected); | ||
}); | ||
describe('get', () => { | ||
it('if an element exist, return it', () => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const aFn = (x) => x; | ||
const args = [1, 2, 3]; | ||
const xs = [{id: 1}, {id: 2}, {id: 3}]; | ||
const xsRet = [{id: 1, __ladda__id: 1}, {id: 2, __ladda__id: 2}, {id: 3, __ladda__id: 3}]; | ||
put(qc, e, aFn, args, addId({}, undefined, undefined, xs)); | ||
expect(getValue(get(qc, e, aFn, args).value)).to.deep.equal(xsRet); | ||
}); | ||
it('if an does not exist, throw an error', () => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const aFn = (x) => x; | ||
const args = [1, 2, 3]; | ||
const fnUnderTest = () => getValue(get(qc, e, aFn, args).value); | ||
expect(fnUnderTest).to.throw(Error); | ||
}); | ||
}); | ||
describe('contains & put', () => { | ||
it('if an element exist, return true', () => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const aFn = (x) => x; | ||
const args = [1, 2, 3]; | ||
const xs = [{id: 1}, {id: 2}, {id: 3}]; | ||
put(qc, e, aFn, args, addId({}, undefined, undefined, xs)); | ||
expect(contains(qc, e, aFn, args)).to.be.true; | ||
}); | ||
describe('invalidate', () => { | ||
it('invalidates other cache as specified', () => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const eUser = config[0]; | ||
const eCars = config[2]; | ||
const aFn = (x) => x; | ||
aFn.operation = 'CREATE'; | ||
const args = [1, 2, 3]; | ||
const xs = [{id: 1}, {id: 2}, {id: 3}]; | ||
put(qc, eUser, aFn, args, addId({}, undefined, undefined, xs)); | ||
invalidate(qc, eCars, aFn); | ||
const hasUser = contains(qc, eUser, aFn, args); | ||
expect(hasUser).to.be.false; | ||
}); | ||
it('does not crash when no invalidates specified', () => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const eBikes = config[3]; | ||
const aFn = (x) => x; | ||
aFn.operation = 'CREATE'; | ||
const fn = () => invalidate(qc, eBikes, aFn); | ||
expect(fn).to.not.throw(); | ||
}); | ||
it('if an element exist, and args contains a complex object, return true', () => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const aFn = (x) => x; | ||
const args = [1, {hello: {world: 'Kalle'}}, 3]; | ||
const xs = [{id: 1}, {id: 2}, {id: 3}]; | ||
put(qc, e, aFn, args, addId({}, undefined, undefined, xs)); | ||
expect(contains(qc, e, aFn, args)).to.be.true; | ||
}); | ||
it('if an element exist, and args contains a simple object, return true', () => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const aFn = (x) => x; | ||
const args = [1, {hello: 'world'}, 3]; | ||
const xs = [{id: 1}, {id: 2}, {id: 3}]; | ||
put(qc, e, aFn, args, addId({}, undefined, undefined, xs)); | ||
expect(contains(qc, e, aFn, args)).to.be.true; | ||
}); | ||
it('if an element does not exist, return false', () => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const aFn = (x) => x; | ||
const args = [1, 2, 3]; | ||
expect(contains(qc, e, aFn, args)).to.be.false; | ||
}); | ||
}); | ||
describe('get', () => { | ||
it('if an element exist, return it', () => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const aFn = (x) => x; | ||
const args = [1, 2, 3]; | ||
const xs = [{id: 1}, {id: 2}, {id: 3}]; | ||
const xsRet = [{id: 1, __ladda__id: 1}, {id: 2, __ladda__id: 2}, {id: 3, __ladda__id: 3}]; | ||
put(qc, e, aFn, args, addId({}, undefined, undefined, xs)); | ||
expect(getValue(get(qc, e, aFn, args).value)).to.deep.equal(xsRet); | ||
}); | ||
it('if an does not exist, throw an error', () => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const e = config[0]; | ||
const aFn = (x) => x; | ||
const args = [1, 2, 3]; | ||
const fnUnderTest = () => getValue(get(qc, e, aFn, args).value); | ||
expect(fnUnderTest).to.throw(Error); | ||
}); | ||
}); | ||
describe('invalidate', () => { | ||
it('invalidates other cache as specified', () => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const eUser = config[0]; | ||
const eCars = config[2]; | ||
const aFn = createApiFunction(x => x, {operation: 'CREATE'}); | ||
const args = [1, 2, 3]; | ||
const xs = [{id: 1}, {id: 2}, {id: 3}]; | ||
put(qc, eUser, aFn, args, addId({}, undefined, undefined, xs)); | ||
invalidate(qc, eCars, aFn); | ||
const hasUser = contains(qc, eUser, aFn, args); | ||
expect(hasUser).to.be.false; | ||
}); | ||
it('does not crash when no invalidates specified', () => { | ||
const es = createEntityStore(config); | ||
const qc = createQueryCache(es); | ||
const eBikes = config[3]; | ||
const aFn = createApiFunction(x => x, {operation: 'CREATE'}); | ||
aFn.operation = 'CREATE'; | ||
const fn = () => invalidate(qc, eBikes, aFn); | ||
expect(fn).to.not.throw(); | ||
}); | ||
}); | ||
}); |
import { build } from './builder'; | ||
import { observable } from './plugins/observable'; | ||
import { denormalizer } from './plugins/denormalizer'; | ||
module.exports = { | ||
build | ||
build, | ||
plugins: { | ||
observable, | ||
denormalizer | ||
} | ||
}; |
const serializeObject = (o) => { | ||
return Object.keys(o).map(x => { | ||
if (o[x] && typeof o[x] === 'object') { | ||
return serializeObject(o[x]); | ||
} else { | ||
return o[x]; | ||
} | ||
}).join('-'); | ||
return Object.keys(o).map(x => { | ||
if (o[x] && typeof o[x] === 'object') { | ||
return serializeObject(o[x]); | ||
} | ||
return o[x]; | ||
}).join('-'); | ||
}; | ||
export const serialize = (x) => { | ||
if (x && typeof x === 'object') { | ||
return serializeObject(x); | ||
} else { | ||
return x; | ||
} | ||
if (x instanceof Object) { | ||
return serializeObject(x); | ||
} | ||
return x; | ||
}; |
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
No contributors or author data
MaintenancePackage does not specify a list of contributors or an author in package.json.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
5561
54
248263
18
97
2
4