Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

ladda-cache

Package Overview
Dependencies
Maintainers
2
Versions
30
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

ladda-cache - npm Package Compare versions

Comparing version 0.1.4 to 0.2.0

.eslintrc

6

docs/Demos.md

@@ -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 @@

6

examples/minimal-react-ladda/package.json

@@ -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": ""
}

2

examples/minimal-vue-ladda/README.md

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc