2018-08-07 14:46:54 -06:00
|
|
|
import { fromJS, Map, Set } from 'immutable';
|
2018-06-11 19:03:43 -07:00
|
|
|
|
|
|
|
const jsToMap = obj => {
|
|
|
|
if (obj === undefined) {
|
|
|
|
return Map();
|
|
|
|
}
|
|
|
|
const immutableObj = fromJS(obj);
|
|
|
|
if (!Map.isMap(immutableObj)) {
|
2018-08-07 14:46:54 -06:00
|
|
|
throw new Error('Object must be equivalent to a Map.');
|
2018-06-11 19:03:43 -07:00
|
|
|
}
|
|
|
|
return immutableObj;
|
|
|
|
};
|
|
|
|
|
2018-08-07 14:46:54 -06:00
|
|
|
const knownMetaKeys = Set(['index', 'count', 'pageSize', 'pageCount', 'usingOldPaginationAPI']);
|
2018-06-11 19:03:43 -07:00
|
|
|
const filterUnknownMetaKeys = meta => meta.filter((v, k) => knownMetaKeys.has(k));
|
|
|
|
|
|
|
|
/*
|
|
|
|
createCursorMap takes one of three signatures:
|
|
|
|
- () -> cursor with empty actions, data, and meta
|
|
|
|
- (cursorMap: <object/Map with optional actions, data, and meta keys>) -> cursor
|
|
|
|
- (actions: <array/List>, data: <object/Map>, meta: <optional object/Map>) -> cursor
|
|
|
|
*/
|
|
|
|
const createCursorMap = (...args) => {
|
2018-08-07 14:46:54 -06:00
|
|
|
const { actions, data, meta } =
|
|
|
|
args.length === 1
|
|
|
|
? jsToMap(args[0]).toObject()
|
|
|
|
: { actions: args[0], data: args[1], meta: args[2] };
|
2018-06-11 19:03:43 -07:00
|
|
|
return Map({
|
|
|
|
// actions are a Set, rather than a List, to ensure an efficient .has
|
|
|
|
actions: Set(actions),
|
|
|
|
|
|
|
|
// data and meta are Maps
|
|
|
|
data: jsToMap(data),
|
|
|
|
meta: jsToMap(meta).update(filterUnknownMetaKeys),
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2018-08-07 14:46:54 -06:00
|
|
|
const hasAction = (cursorMap, action) => cursorMap.hasIn(['actions', action]);
|
2018-06-11 19:03:43 -07:00
|
|
|
|
|
|
|
const getActionHandlers = (cursorMap, handler) =>
|
2018-08-07 14:46:54 -06:00
|
|
|
cursorMap
|
|
|
|
.get('actions', Set())
|
|
|
|
.toMap()
|
|
|
|
.map(action => handler(action));
|
2018-06-11 19:03:43 -07:00
|
|
|
|
|
|
|
// The cursor logic is entirely functional, so this class simply
|
|
|
|
// provides a chainable interface
|
|
|
|
export default class Cursor {
|
|
|
|
static create(...args) {
|
|
|
|
return new Cursor(...args);
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(...args) {
|
|
|
|
if (args[0] instanceof Cursor) {
|
|
|
|
return args[0];
|
|
|
|
}
|
|
|
|
|
|
|
|
this.store = createCursorMap(...args);
|
2018-08-07 14:46:54 -06:00
|
|
|
this.actions = this.store.get('actions');
|
|
|
|
this.data = this.store.get('data');
|
|
|
|
this.meta = this.store.get('meta');
|
2018-06-11 19:03:43 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
updateStore(...args) {
|
|
|
|
return new Cursor(this.store.update(...args));
|
|
|
|
}
|
|
|
|
updateInStore(...args) {
|
|
|
|
return new Cursor(this.store.updateIn(...args));
|
|
|
|
}
|
|
|
|
|
|
|
|
hasAction(action) {
|
|
|
|
return hasAction(this.store, action);
|
|
|
|
}
|
|
|
|
addAction(action) {
|
2018-08-07 14:46:54 -06:00
|
|
|
return this.updateStore('actions', actions => actions.add(action));
|
2018-06-11 19:03:43 -07:00
|
|
|
}
|
|
|
|
removeAction(action) {
|
2018-08-07 14:46:54 -06:00
|
|
|
return this.updateStore('actions', actions => actions.delete(action));
|
2018-06-11 19:03:43 -07:00
|
|
|
}
|
|
|
|
setActions(actions) {
|
2018-08-07 14:46:54 -06:00
|
|
|
return this.updateStore(store => store.set('actions', Set(actions)));
|
2018-06-11 19:03:43 -07:00
|
|
|
}
|
|
|
|
mergeActions(actions) {
|
2018-08-07 14:46:54 -06:00
|
|
|
return this.updateStore('actions', oldActions => oldActions.union(actions));
|
2018-06-11 19:03:43 -07:00
|
|
|
}
|
|
|
|
getActionHandlers(handler) {
|
|
|
|
return getActionHandlers(this.store, handler);
|
|
|
|
}
|
|
|
|
|
|
|
|
setData(data) {
|
2018-08-07 14:46:54 -06:00
|
|
|
return new Cursor(this.store.set('data', jsToMap(data)));
|
2018-06-11 19:03:43 -07:00
|
|
|
}
|
|
|
|
mergeData(data) {
|
2018-08-07 14:46:54 -06:00
|
|
|
return new Cursor(this.store.mergeIn(['data'], jsToMap(data)));
|
2018-06-11 19:03:43 -07:00
|
|
|
}
|
|
|
|
wrapData(data) {
|
2018-08-07 14:46:54 -06:00
|
|
|
return this.updateStore('data', oldData => jsToMap(data).set('wrapped_cursor_data', oldData));
|
2018-06-11 19:03:43 -07:00
|
|
|
}
|
|
|
|
unwrapData() {
|
2018-08-07 14:46:54 -06:00
|
|
|
return [
|
|
|
|
this.store.get('data').delete('wrapped_cursor_data'),
|
|
|
|
this.updateStore('data', data => data.get('wrapped_cursor_data')),
|
|
|
|
];
|
2018-06-11 19:03:43 -07:00
|
|
|
}
|
|
|
|
clearData() {
|
2018-08-07 14:46:54 -06:00
|
|
|
return this.updateStore('data', () => Map());
|
2018-06-11 19:03:43 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
setMeta(meta) {
|
2018-08-07 14:46:54 -06:00
|
|
|
return this.updateStore(store => store.set('meta', jsToMap(meta)));
|
2018-06-11 19:03:43 -07:00
|
|
|
}
|
|
|
|
mergeMeta(meta) {
|
2018-08-07 14:46:54 -06:00
|
|
|
return this.updateStore(store => store.update('meta', oldMeta => oldMeta.merge(jsToMap(meta))));
|
2018-06-11 19:03:43 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// This is a temporary hack to allow cursors to be added to the
|
|
|
|
// interface between backend.js and backends without modifying old
|
|
|
|
// backends at all. This should be removed in favor of wrapping old
|
|
|
|
// backends with a compatibility layer, as part of the backend API
|
|
|
|
// refactor.
|
2018-08-07 14:46:54 -06:00
|
|
|
export const CURSOR_COMPATIBILITY_SYMBOL = Symbol('cursor key for compatibility with old backends');
|