├── babel.config.js ├── src ├── types │ ├── simple │ │ ├── simple.actions.js │ │ ├── index.js │ │ ├── simple.selectors.js │ │ ├── simple.middleware.js │ │ ├── __snapshots__ │ │ │ ├── simple.selectors.spec.js.snap │ │ │ └── simple.middleware.spec.js.snap │ │ ├── simple.middleware.spec.js │ │ └── simple.selectors.spec.js │ ├── index.js │ └── keyValue │ │ ├── keyValue.actions.js │ │ ├── index.js │ │ ├── keyValue.selectors.js │ │ ├── keyValue.middleware.js │ │ ├── keyValue.selectors.spec.js │ │ ├── keyValue.middleware.spec.js │ │ └── __snapshots__ │ │ ├── keyValue.selectors.spec.js.snap │ │ └── keyValue.middleware.spec.js.snap ├── index.spec.js ├── index.js ├── selectors.js ├── utils.js ├── actions.spec.js ├── reducer.js ├── __snapshots__ │ ├── actions.spec.js.snap │ ├── helpers.spec.js.snap │ ├── reducer.spec.js.snap │ └── index.spec.js.snap ├── helpers.js ├── actions.js ├── reducer.spec.js ├── helpers.spec.js └── factories.js ├── .npmignore ├── index.spec.js ├── typing ├── copy.js └── index.d.ts ├── LICENSE ├── .gitignore ├── .circleci └── config.yml ├── helpers ├── index.js └── index.es.js ├── dist ├── index.d.ts ├── index.es.js └── index.js ├── MIGRATING.md ├── package.json ├── perf.spec.js ├── misc └── common-tests.js ├── TYPES.md ├── README.md └── __snapshots__ └── index.spec.js.snap /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-env', 4 | ], 5 | } 6 | -------------------------------------------------------------------------------- /src/types/simple/simple.actions.js: -------------------------------------------------------------------------------- 1 | export { set, SET, reset, RESET, update, UPDATE } from '../../actions' 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | .circleci 3 | **.log 4 | **.spec.js 5 | yarn.lock 6 | coverage 7 | .eslintcache 8 | babel.config.js 9 | **.snap 10 | misc/ 11 | -------------------------------------------------------------------------------- /src/index.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import * as lib from './index' 3 | import commonTests from '../misc/common-tests' 4 | 5 | commonTests(lib, 'src/index') 6 | -------------------------------------------------------------------------------- /src/types/index.js: -------------------------------------------------------------------------------- 1 | export { default as keyValue } from './keyValue/index' 2 | export { default as simple } from './simple/index' 3 | export { default as simpleObject } from './simple/index' 4 | -------------------------------------------------------------------------------- /src/types/keyValue/keyValue.actions.js: -------------------------------------------------------------------------------- 1 | export { 2 | set, SET, 3 | add, ADD, 4 | reset, RESET, 5 | remove, REMOVE, 6 | update, UPDATE, 7 | addOrUpdate, ADD_OR_UPDATE, 8 | } from '../../actions' 9 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { factory as factoryImpl, simple, keyValue, simpleObject } from './factories' 2 | 3 | export const factory = factoryImpl() 4 | export { 5 | simple, 6 | keyValue, 7 | simpleObject, 8 | } 9 | -------------------------------------------------------------------------------- /index.spec.js: -------------------------------------------------------------------------------- 1 | import commonTests from './misc/common-tests' 2 | import * as libES from './dist/index.es' 3 | import * as libCJS from './dist/index' 4 | 5 | commonTests(libES, 'es version') 6 | commonTests(libCJS, 'cjs version') 7 | -------------------------------------------------------------------------------- /typing/copy.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | fs.copyFile('typing/index.d.ts', 'dist/index.d.ts', (err) => { 4 | // eslint-disable-next-line no-console 5 | if (err) console.error('unable to copy typing file to the dist directory', err) 6 | }) 7 | -------------------------------------------------------------------------------- /src/types/simple/index.js: -------------------------------------------------------------------------------- 1 | import middleware from './simple.middleware' 2 | import * as actions from './simple.actions' 3 | import selectors from './simple.selectors' 4 | 5 | export default { 6 | middlewares: [middleware], 7 | actions, 8 | selectors, 9 | } 10 | -------------------------------------------------------------------------------- /src/types/keyValue/index.js: -------------------------------------------------------------------------------- 1 | import middleware from './keyValue.middleware' 2 | import * as actions from './keyValue.actions' 3 | import selectors from './keyValue.selectors' 4 | 5 | export default { 6 | middlewares: [middleware], 7 | actions, 8 | selectors, 9 | } 10 | -------------------------------------------------------------------------------- /src/selectors.js: -------------------------------------------------------------------------------- 1 | import { getFromPath } from './utils' 2 | 3 | /* eslint-disable import/prefer-default-export */ 4 | export const getState = options => (rootState) => { 5 | const { path, name } = options 6 | let subState = rootState 7 | 8 | if (path !== undefined && path.length > 0) subState = getFromPath(rootState, path) 9 | 10 | return subState[name] 11 | } 12 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export const getFromPath = (data, path) => path.split('.').reduce( 2 | (curr, sub) => curr && curr[sub], 3 | data, 4 | ) 5 | 6 | export const memoize = (callback) => { 7 | const memory = { 8 | data: undefined, 9 | result: undefined, 10 | } 11 | 12 | return (data) => { 13 | if (memory.data !== data) { 14 | memory.data = data 15 | memory.result = callback(data) 16 | } 17 | 18 | return memory.result 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/types/simple/simple.selectors.js: -------------------------------------------------------------------------------- 1 | import { getState } from '../../selectors' 2 | import { initState } from './simple.middleware' 3 | 4 | export default (options) => { 5 | const getStateWithOptions = getState(options) 6 | const get = () => rootState => getStateWithOptions(rootState) 7 | const isInitialized = (rootState) => { 8 | if (options.defaultData !== undefined) return get()(rootState) !== options.defaultData 9 | return get()(rootState) !== initState 10 | } 11 | 12 | return { 13 | get, 14 | isInitialized, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/actions.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import { set, remove, add, reset, addOrUpdate } from './actions' 3 | 4 | const prefix = 'testPrefix' 5 | const name = 'testName' 6 | const Element = code => ({ code, an: `element_${code}` }) 7 | 8 | describe('actions', () => { 9 | it('should trigger a set action', () => expect(set(prefix)(name)([Element('set1'), Element('set2')])).toMatchSnapshot()) 10 | it('should trigger a remove action', () => expect(remove(prefix)(name)('remove')).toMatchSnapshot()) 11 | it('should trigger an add action', () => expect(add(prefix)(name)(Element('add'))).toMatchSnapshot()) 12 | it('should trigger a reset action', () => expect(reset(prefix)(name)()).toMatchSnapshot()) 13 | it('should trigger an addOrUpdate action', () => expect(addOrUpdate(prefix)(name)(Element('add'))).toMatchSnapshot()) 14 | }) 15 | -------------------------------------------------------------------------------- /src/types/simple/simple.middleware.js: -------------------------------------------------------------------------------- 1 | import { SET, RESET, UPDATE } from './simple.actions' 2 | 3 | export const initState = {} 4 | const defaultState = defaultData => (defaultData !== undefined ? defaultData : initState) 5 | 6 | const reducer = (/* key */) => prefix => name => defaultData => ( 7 | (state = defaultState(defaultData), { type, payload } = {}) => { 8 | switch (type) { 9 | case SET(prefix)(name): 10 | return payload 11 | case UPDATE(prefix)(name): 12 | return { ...state, ...payload } 13 | case RESET(prefix)(name): 14 | return defaultState(defaultData) 15 | default: 16 | return state 17 | } 18 | } 19 | ) 20 | 21 | export default key => prefix => name => defaultData => (ctx = {}) => ({ 22 | ...ctx, 23 | state: reducer(key)(prefix)(name)(defaultData)(ctx.state, ctx.action), 24 | }) 25 | -------------------------------------------------------------------------------- /src/reducer.js: -------------------------------------------------------------------------------- 1 | export default middlewares => key => prefix => name => defaultData => ( 2 | (state, { type = 'UNKNOWN', payload } = {}) => { 3 | let prevCtx = { state, action: { type, payload } } 4 | 5 | // middlewares to call (in right order) 6 | const middlewaresToCall = [ 7 | // injected by user 8 | ...(middlewares.pre || []), 9 | // injected by types selection (reducer type) 10 | ...(middlewares.engine || []), 11 | // injected by user 12 | ...(middlewares.post || []), 13 | ] 14 | 15 | middlewaresToCall 16 | // pass parameters 17 | .map(middleware => middleware(key)(prefix)(name)(defaultData)) 18 | // call middlewares 19 | .forEach((middleware) => { 20 | prevCtx = middleware(prevCtx) 21 | }) 22 | 23 | // returns last results to Redux 24 | return prevCtx.state 25 | } 26 | ) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 uni rakun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | -------------------------------------------------------------------------------- /src/__snapshots__/actions.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`actions should trigger a remove action 1`] = ` 4 | Object { 5 | "payload": "remove", 6 | "type": "@@krf/REMOVE>TESTPREFIX>TESTNAME", 7 | } 8 | `; 9 | 10 | exports[`actions should trigger a reset action 1`] = ` 11 | Object { 12 | "type": "@@krf/RESET>TESTPREFIX>TESTNAME", 13 | } 14 | `; 15 | 16 | exports[`actions should trigger a set action 1`] = ` 17 | Object { 18 | "payload": Array [ 19 | Object { 20 | "an": "element_set1", 21 | "code": "set1", 22 | }, 23 | Object { 24 | "an": "element_set2", 25 | "code": "set2", 26 | }, 27 | ], 28 | "type": "@@krf/SET>TESTPREFIX>TESTNAME", 29 | } 30 | `; 31 | 32 | exports[`actions should trigger an add action 1`] = ` 33 | Object { 34 | "payload": Object { 35 | "an": "element_add", 36 | "code": "add", 37 | }, 38 | "type": "@@krf/ADD>TESTPREFIX>TESTNAME", 39 | } 40 | `; 41 | 42 | exports[`actions should trigger an addOrUpdate action 1`] = ` 43 | Object { 44 | "payload": Object { 45 | "an": "element_add", 46 | "code": "add", 47 | }, 48 | "type": "@@krf/ADD_OR_UPDATE>TESTPREFIX>TESTNAME", 49 | } 50 | `; 51 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | export const reducer = mapper => ( 2 | // middleware signature 3 | (/* key */) => (/* prefix */) => (/* name */) => (/* defaultData */) => ctx => ({ ...ctx, state: mapper(ctx.action, ctx.state) }) 4 | ) 5 | 6 | 7 | export const mapAction = mapper => ( 8 | // middleware signature 9 | (/* key */) => (/* prefix */) => (/* name */) => (/* defaultData */) => ctx => ({ 10 | ...ctx, 11 | action: mapper(ctx.action), 12 | }) 13 | ) 14 | 15 | export const mapState = actionMatches => mapper => ( 16 | // middleware signature 17 | (/* key */) => (/* prefix */) => (/* name */) => (/* defaultData */) => (ctx) => { 18 | const { action } = ctx 19 | if (!actionMatches || actionMatches.test(action.type)) { 20 | return { action, state: mapper(ctx.state) } 21 | } 22 | return ctx 23 | } 24 | ) 25 | 26 | export const mapPayload = actionMatches => mapper => ( 27 | // middleware signature 28 | (/* key */) => (/* prefix */) => (/* name */) => (/* defaultData */) => (ctx) => { 29 | const { payload, type } = ctx.action 30 | if (!actionMatches || actionMatches.test(type)) { 31 | return mapAction(action => ({ ...action, payload: mapper(payload) }))()()()()(ctx) 32 | } 33 | return ctx 34 | } 35 | ) 36 | -------------------------------------------------------------------------------- /src/actions.js: -------------------------------------------------------------------------------- 1 | const scope = action => `@@krf/${action.toUpperCase()}` 2 | 3 | const getPrefix = prefix => name => `${prefix ? '>' : ''}${prefix}>${name}` 4 | 5 | export const SET = prefix => name => scope(`SET${getPrefix(prefix)(name)}`) 6 | export const set = prefix => name => payload => ({ type: SET(prefix)(name), payload }) 7 | 8 | export const RESET = prefix => name => scope(`RESET${getPrefix(prefix)(name)}`) 9 | export const reset = prefix => name => () => ({ type: RESET(prefix)(name) }) 10 | 11 | export const ADD = prefix => name => scope(`ADD${getPrefix(prefix)(name)}`) 12 | export const add = prefix => name => payload => ({ type: ADD(prefix)(name), payload }) 13 | 14 | export const UPDATE = prefix => name => scope(`UPDATE${getPrefix(prefix)(name)}`) 15 | export const update = prefix => name => payload => ({ type: UPDATE(prefix)(name), payload }) 16 | 17 | export const REMOVE = prefix => name => scope(`REMOVE${getPrefix(prefix)(name)}`) 18 | export const remove = prefix => name => key => ({ type: REMOVE(prefix)(name), payload: key }) 19 | 20 | export const ADD_OR_UPDATE = prefix => name => scope(`ADD_OR_UPDATE${getPrefix(prefix)(name)}`) 21 | export const addOrUpdate = prefix => name => payload => ({ type: ADD_OR_UPDATE(prefix)(name), payload }) 22 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | lockfile: 4 | docker: 5 | - image: circleci/node:latest 6 | steps: 7 | - checkout 8 | - restore_cache: 9 | keys: 10 | - cache-lockfile-{{ checksum "package.json" }} 11 | - cache-lockfile 12 | - run: 'sudo yarn global add greenkeeper-lockfile@1' 13 | - run: 'greenkeeper-lockfile-update' 14 | - run: 'greenkeeper-lockfile-upload' 15 | - save_cache: 16 | key: cache-lockfile-{{ checksum "package.json" }} 17 | paths: 18 | - ~/.cache/yarn 19 | 20 | debug: 21 | docker: 22 | - image: circleci/node:latest 23 | steps: 24 | - run: node --version 25 | - run: yarn --version 26 | 27 | build: 28 | docker: 29 | - image: circleci/node:latest 30 | steps: 31 | - checkout 32 | - restore_cache: 33 | keys: 34 | - dependencies-{{ checksum "package.json" }} 35 | - dependencies- 36 | - run: yarn 37 | - save_cache: 38 | paths: 39 | - node_modules 40 | key: dependencies-{{ checksum "package.json" }} 41 | - run: yarn ci 42 | 43 | workflows: 44 | version: 2 45 | workflow_name: 46 | jobs: 47 | - debug 48 | - lockfile 49 | - build: 50 | requires: 51 | - lockfile 52 | -------------------------------------------------------------------------------- /helpers/index.js: -------------------------------------------------------------------------------- 1 | function t(t,n,r){return n in t?Object.defineProperty(t,n,{value:r,enumerable:!0,configurable:!0,writable:!0}):t[n]=r,t}function n(t,n){var r=Object.keys(t);if(Object.getOwnPropertySymbols){var e=Object.getOwnPropertySymbols(t);n&&(e=e.filter(function(n){return Object.getOwnPropertyDescriptor(t,n).enumerable})),r.push.apply(r,e)}return r}function r(r){for(var e=1;e get defaultData as a string should return the current data 1`] = `"DATA"`; 4 | 5 | exports[`selectors/simple => get defaultData as a string should return the default data 1`] = `""`; 6 | 7 | exports[`selectors/simple => get defaultData as an object should return the current data 1`] = ` 8 | Object { 9 | "i": "DATA", 10 | } 11 | `; 12 | 13 | exports[`selectors/simple => get defaultData as an object should return the default data 1`] = `Object {}`; 14 | 15 | exports[`selectors/simple => get without defaultData should return the initState 1`] = `Object {}`; 16 | 17 | exports[`selectors/simple => isInitialized defaultData as a string should be initialized 1`] = `true`; 18 | 19 | exports[`selectors/simple => isInitialized defaultData as a string should not be initialized 1`] = `false`; 20 | 21 | exports[`selectors/simple => isInitialized defaultData as an object should be initialized 1`] = `true`; 22 | 23 | exports[`selectors/simple => isInitialized defaultData as an object should not be initialized 1`] = `false`; 24 | 25 | exports[`selectors/simple => isInitialized without defaultData should be initialized -object- 1`] = `true`; 26 | 27 | exports[`selectors/simple => isInitialized without defaultData should be initialized -string- 1`] = `true`; 28 | 29 | exports[`selectors/simple => isInitialized without defaultData should not be initialized 1`] = `false`; 30 | -------------------------------------------------------------------------------- /src/types/keyValue/keyValue.selectors.js: -------------------------------------------------------------------------------- 1 | import { getFromPath, memoize } from '../../utils' 2 | import { getState } from '../../selectors' 3 | 4 | export default (options) => { 5 | // get reducer state from rootState 6 | const getStateWithOptions = getState(options) 7 | 8 | // basic selectors 9 | const getData = rootState => getStateWithOptions(rootState).data 10 | const getMap = memoize(data => new Map(data)) 11 | const mapToKeys = memoize(data => Array.from(getMap(data).keys())) 12 | const mapToValues = memoize(data => Array.from(getMap(data).values())) 13 | const getKeys = rootState => mapToKeys(getData(rootState)) 14 | const getObject = memoize((data) => { 15 | const object = {} 16 | data.forEach(([key, value]) => { 17 | object[key] = value 18 | }) 19 | return object 20 | }) 21 | const get = keys => (rootState) => { 22 | const data = getData(rootState) 23 | // all data 24 | if (keys === undefined || keys === null) return getObject(data) 25 | // by key(s) 26 | const map = getMap(data) 27 | if (Array.isArray(keys)) return keys.map(key => map.get(key)) 28 | return map.get(keys) 29 | } 30 | const getBy = (path, filters) => (rootState) => { 31 | const values = mapToValues(getMap(getData(rootState))) 32 | 33 | if (Array.isArray(filters)) return values.filter(d => filters.includes(getFromPath(d, path))) 34 | return values.filter(d => filters === getFromPath(d, path)) 35 | } 36 | 37 | return { 38 | get, 39 | getKeys, 40 | getBy, 41 | getState: getStateWithOptions, 42 | getAsArray: rootState => mapToValues(getData(rootState)), 43 | getLength: rootState => getData(rootState).length, 44 | hasKey: key => rootState => getMap(getData(rootState)).has(key), 45 | isInitialized: rootState => getStateWithOptions(rootState).initialized, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/reducer.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import { add } from './actions' 3 | import reducer from './reducer' 4 | 5 | const middleware = middlewareName => key => prefix => name => defaultData => ctx => ({ 6 | state: { 7 | ...ctx.state, 8 | key, 9 | prefix, 10 | name, 11 | defaultData, 12 | [middlewareName]: true, 13 | prev: ctx, 14 | }, 15 | action: { ...ctx.action, [name]: true }, 16 | }) 17 | 18 | const initState = { data: [{ some: 'data' }] } 19 | const prefix = 'testPrefix' 20 | const name = 'testName' 21 | 22 | describe('reducer', () => { 23 | it('should initialize an action if undefined', () => { 24 | const testPrefix = reducer({ 25 | engine: [ 26 | middleware('engine'), 27 | ], 28 | })('code')(prefix)(name)() 29 | expect(testPrefix(initState, undefined)).toMatchSnapshot() 30 | }) 31 | 32 | it('should call -engine- middleware', () => { 33 | const testPrefix = reducer({ 34 | engine: [ 35 | middleware('engine'), 36 | ], 37 | })('code')(prefix)(name)() 38 | expect(testPrefix(initState, add(prefix)(name)({ code: '1', some: 'info' }))).toMatchSnapshot() 39 | }) 40 | 41 | it('should call -pre- middlewares', () => { 42 | const testPrefix = reducer({ 43 | pre: [ 44 | middleware('pre1'), 45 | middleware('pre2'), 46 | ], 47 | })('code')(prefix)(name)() 48 | 49 | expect(testPrefix(initState, add(prefix)(name)({ code: '1', some: 'info' }))).toMatchSnapshot() 50 | }) 51 | 52 | it('should call -post- middlewares', () => { 53 | const testPrefix = reducer({ 54 | post: [ 55 | middleware('post1'), 56 | middleware('post2'), 57 | ], 58 | })('code')(prefix)(name)() 59 | 60 | expect(testPrefix(initState, add(prefix)(name)({ code: '1', some: 'info' }))).toMatchSnapshot() 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /src/types/simple/simple.middleware.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import { set, reset, update } from '../../actions' 3 | import simple from './simple.middleware' 4 | 5 | const prefix = 'testPrefix' 6 | const name = 'testName' 7 | const Element = code => ({ code, some: 'other', infos: code }) 8 | const state = Element('elm') 9 | 10 | describe('middlewares/simple', () => { 11 | // Use the factory to create a new reducer named 'testPrefix' 12 | // It uses 'code' as key in givent elements 13 | const testPrefix = simple('code')(prefix)(name) 14 | 15 | it('should initialize', () => expect(testPrefix(/* defaultData */)()).toMatchSnapshot()) 16 | 17 | it('should initialize with defaultData', () => expect(testPrefix('default')()).toMatchSnapshot()) 18 | 19 | it('should set element [elm2]', () => expect(testPrefix(/* defaultData */)({ 20 | state, 21 | action: set(prefix)(name)(Element('elm2')), 22 | })).toMatchSnapshot()) 23 | 24 | it('should reset state', () => expect(testPrefix(/* defaultData */)({ 25 | state, 26 | action: reset(prefix)(name)(), 27 | })).toMatchSnapshot()) 28 | 29 | it('should update element', () => expect(testPrefix(/* defaultData */)({ 30 | state, 31 | action: update(prefix)(name)({ some: 'other 2', modifi: 'cation' }), 32 | })).toMatchSnapshot()) 33 | 34 | it('should take defaultData param to populate data field -init-', () => { 35 | // init - with empty object 36 | expect(testPrefix({})()).toMatchSnapshot() 37 | // init - with object 38 | expect(testPrefix({ im: 'default' })()).toMatchSnapshot() 39 | // init - with empty string 40 | expect(testPrefix('')()).toMatchSnapshot() 41 | }) 42 | it('should take defaultData param to populate data field -reset-', () => { 43 | // reset 44 | expect(testPrefix({ im: 'default' })({ state, action: reset(prefix)(name)() })).toMatchSnapshot() 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "k-redux-factory" { 2 | export type TypeDefinition = object; 3 | export type TypeOptions = object; 4 | export type KeyValueDefinition = { 5 | keyValue: (options?: TypeOptions) => TypeDefinition; 6 | }; 7 | export type SimpleDefinition = { 8 | object: (options?: TypeOptions) => TypeDefinition; 9 | bool: (options?: TypeOptions) => TypeDefinition; 10 | string: (options?: TypeOptions) => TypeDefinition; 11 | array: (options?: TypeOptions) => TypeDefinition; 12 | number: (options?: TypeOptions) => TypeDefinition; 13 | }; 14 | export type TypeDefinitionFunction = SimpleDefinition & KeyValueDefinition; 15 | export type ReducerType = { 16 | [key as string]: (...any: any[]) => any; 17 | }; 18 | export type SimpleType = { 19 | set: (value: T) => void; 20 | update: (value: T) => void; 21 | reset: () => void; 22 | get: () => T; 23 | isInitialized: () => boolean; 24 | }; 25 | export type ArrayType = typeof Array; 26 | export namespace Types { 27 | interface KeyValue extends ReducerType { 28 | set: (elements: T[]) => void; 29 | add: (elements: T | T[]) => void; 30 | update: (elements: T | T[]) => void; 31 | addOrUpdate: (elements: T | T[]) => void; 32 | remove: (elements: T | T[] | U | U[]) => void; 33 | reset: () => void; 34 | get: (id?: U | U[]) => T | T[]; 35 | getBy: (propertyPath: string, value: any) => T; 36 | getKeys: () => U[]; 37 | getAsArray: () => T[]; 38 | getLength: () => number; 39 | isInitialized: () => boolean; 40 | getState: () => any; 41 | hasKey: (key: U) => boolean; 42 | } 43 | type Object = SimpleType; 44 | type Bool = SimpleType; 45 | type String = SimpleType; 46 | type Array = SimpleType; 47 | type Number = SimpleType; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /typing/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "k-redux-factory" { 2 | export type TypeDefinition = object; 3 | export type TypeOptions = object; 4 | export type KeyValueDefinition = { 5 | keyValue: (options?: TypeOptions) => TypeDefinition; 6 | }; 7 | export type SimpleDefinition = { 8 | object: (options?: TypeOptions) => TypeDefinition; 9 | bool: (options?: TypeOptions) => TypeDefinition; 10 | string: (options?: TypeOptions) => TypeDefinition; 11 | array: (options?: TypeOptions) => TypeDefinition; 12 | number: (options?: TypeOptions) => TypeDefinition; 13 | }; 14 | export type TypeDefinitionFunction = SimpleDefinition & KeyValueDefinition; 15 | export type ReducerType = { 16 | [key as string]: (...any: any[]) => any; 17 | }; 18 | export type SimpleType = { 19 | set: (value: T) => void; 20 | update: (value: T) => void; 21 | reset: () => void; 22 | get: () => T; 23 | isInitialized: () => boolean; 24 | }; 25 | export type ArrayType = typeof Array; 26 | export namespace Types { 27 | interface KeyValue extends ReducerType { 28 | set: (elements: T[]) => void; 29 | add: (elements: T | T[]) => void; 30 | update: (elements: T | T[]) => void; 31 | addOrUpdate: (elements: T | T[]) => void; 32 | remove: (elements: T | T[] | U | U[]) => void; 33 | reset: () => void; 34 | get: (id?: U | U[]) => T | T[]; 35 | getBy: (propertyPath: string, value: any) => T; 36 | getKeys: () => U[]; 37 | getAsArray: () => T[]; 38 | getLength: () => number; 39 | isInitialized: () => boolean; 40 | getState: () => any; 41 | hasKey: (key: U) => boolean; 42 | } 43 | type Object = SimpleType; 44 | type Bool = SimpleType; 45 | type String = SimpleType; 46 | type Array = SimpleType; 47 | type Number = SimpleType; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /MIGRATING.md: -------------------------------------------------------------------------------- 1 | # Migrating Guide 2 | ## From 5.X.X to 6.0.0 3 | ### Deprecated 4 | - `simpleObject` factory is deprecated. [You can now use `simple` factories](./TYPES.md): 5 | * `simple.object` 6 | * `simple.string` 7 | * `simple.bool` 8 | * `simple.array` 9 | 10 | ### Breaking changes 11 | - `replace` action is removed, use `add` action 12 | - `orderBy` action is removed 13 | * You can write [your own with a middleware](./README.md#example-we-create-a-middleware-but-we-modify-action-and-state-) 14 | * You can use [k-ramel](https://github.com/unirakun/k-ramel) to handle your logical code and use `k-redux-factory` for the key/value store only 15 | * You can use [redux-saga](https://github.com/redux-saga/redux-saga) to handle your logical code and use `k-redux-factory` for the key/value store only 16 | - `factory` export is not the default one 17 | * **Before**: `import factory, { keyValue } from 'k-redux-factory'` 18 | * **Now**: `import { factory, keyValue } from 'k-redux-factory'` 19 | - `state` created by `k-redux-factory` is modified to be less bloated when serialized: 20 | * `getState` selector returns a new shape, see [dedicated chapter](#state) 21 | * all other selectors work as before 👌 22 | 23 | ### State 24 | The state shape used by `k-redux-factory` is changed to be less bloated when serialized. 25 | Under the hood we use Javascript *Map* object. 26 | 27 | This is a breaking change when you: 28 | - manually retrieve the substate from a `k-redux-factory` reducer 29 | - or use the `getState` selector 30 | 31 | All other selectors work as before 👌 32 | 33 | **k-redux-factory v5.x.x** 34 | ```js 35 | { 36 | data: { : , : }, 37 | array: [, ], 38 | keys: [, ], 39 | initialized: true, 40 | } 41 | ``` 42 | 43 | **k-redux-factory v6.0.0** 44 | ```js 45 | { 46 | data: [ 47 | [, ], 48 | [, ], 49 | ], 50 | initialized: true, 51 | } 52 | ``` 53 | -------------------------------------------------------------------------------- /src/helpers.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import { reducer, mapAction, mapState, mapPayload } from './helpers' 3 | 4 | const ctx = { 5 | action: { 6 | payload: 'payload', 7 | type: 'TYPE', 8 | }, 9 | state: {}, 10 | } 11 | 12 | const customCtx = { 13 | action: { 14 | payload: 'payload', 15 | type: 'CUSTOM', 16 | }, 17 | state: 'STATE', 18 | } 19 | 20 | const mapperAction = () => 'ACTION MAPPED !' 21 | const mapperPayload = () => 'PAYLOAD MAPPED !' 22 | const mapperState = state => `${state} MAPPED !` 23 | const customReducer = (action, state) => (/CUSTOM/.test(action.type) ? 'CUSTOM_STATE' : state) 24 | 25 | const execHelper = context => helper => helper()()()()(context) 26 | 27 | describe('helpers/reducer', () => { 28 | it('should add reducer but not change state', () => expect(execHelper(ctx)(reducer(customReducer))).toMatchSnapshot()) 29 | it('should add reducer to change state', () => expect(execHelper(customCtx)(reducer(customReducer))).toMatchSnapshot()) 30 | }) 31 | 32 | describe('helpers/mapState', () => { 33 | it('should map state without matching type', () => expect(execHelper(customCtx)(mapState()(mapperState))).toMatchSnapshot()) 34 | it('should map state (type matcher is KO)', () => expect(execHelper(customCtx)(mapState(/NOTHING/)(mapperState))).toMatchSnapshot()) 35 | it('should map state (type matcher OK)', () => expect(execHelper(customCtx)(mapState(/CUSTOM/)(mapperState))).toMatchSnapshot()) 36 | }) 37 | 38 | describe('helpers/mapPayload', () => { 39 | it('should map payload without matching type', () => expect(execHelper(ctx)(mapPayload()(mapperPayload))).toMatchSnapshot()) 40 | it('should not map payload (type matcher is KO)', () => expect(execHelper(ctx)(mapPayload(/NOTHING/)(mapperPayload))).toMatchSnapshot()) 41 | it('should map payload (type matcher OK)', () => expect(execHelper(ctx)(mapPayload(/TYPE/)(mapperPayload))).toMatchSnapshot()) 42 | }) 43 | 44 | describe('helpers/mapAction', () => { 45 | it('should map action', () => expect(execHelper(ctx)(mapAction(mapperAction))).toMatchSnapshot()) 46 | }) 47 | -------------------------------------------------------------------------------- /src/__snapshots__/helpers.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`helpers/mapAction should map action 1`] = ` 4 | Object { 5 | "action": "ACTION MAPPED !", 6 | "state": Object {}, 7 | } 8 | `; 9 | 10 | exports[`helpers/mapPayload should map payload (type matcher OK) 1`] = ` 11 | Object { 12 | "action": Object { 13 | "payload": "PAYLOAD MAPPED !", 14 | "type": "TYPE", 15 | }, 16 | "state": Object {}, 17 | } 18 | `; 19 | 20 | exports[`helpers/mapPayload should map payload without matching type 1`] = ` 21 | Object { 22 | "action": Object { 23 | "payload": "PAYLOAD MAPPED !", 24 | "type": "TYPE", 25 | }, 26 | "state": Object {}, 27 | } 28 | `; 29 | 30 | exports[`helpers/mapPayload should not map payload (type matcher is KO) 1`] = ` 31 | Object { 32 | "action": Object { 33 | "payload": "payload", 34 | "type": "TYPE", 35 | }, 36 | "state": Object {}, 37 | } 38 | `; 39 | 40 | exports[`helpers/mapState should map state (type matcher OK) 1`] = ` 41 | Object { 42 | "action": Object { 43 | "payload": "payload", 44 | "type": "CUSTOM", 45 | }, 46 | "state": "STATE MAPPED !", 47 | } 48 | `; 49 | 50 | exports[`helpers/mapState should map state (type matcher is KO) 1`] = ` 51 | Object { 52 | "action": Object { 53 | "payload": "payload", 54 | "type": "CUSTOM", 55 | }, 56 | "state": "STATE", 57 | } 58 | `; 59 | 60 | exports[`helpers/mapState should map state without matching type 1`] = ` 61 | Object { 62 | "action": Object { 63 | "payload": "payload", 64 | "type": "CUSTOM", 65 | }, 66 | "state": "STATE MAPPED !", 67 | } 68 | `; 69 | 70 | exports[`helpers/reducer should add reducer but not change state 1`] = ` 71 | Object { 72 | "action": Object { 73 | "payload": "payload", 74 | "type": "TYPE", 75 | }, 76 | "state": Object {}, 77 | } 78 | `; 79 | 80 | exports[`helpers/reducer should add reducer to change state 1`] = ` 81 | Object { 82 | "action": Object { 83 | "payload": "payload", 84 | "type": "CUSTOM", 85 | }, 86 | "state": "CUSTOM_STATE", 87 | } 88 | `; 89 | -------------------------------------------------------------------------------- /src/types/simple/__snapshots__/simple.middleware.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`middlewares/simple should initialize 1`] = ` 4 | Object { 5 | "state": Object {}, 6 | } 7 | `; 8 | 9 | exports[`middlewares/simple should initialize with defaultData 1`] = ` 10 | Object { 11 | "state": "default", 12 | } 13 | `; 14 | 15 | exports[`middlewares/simple should reset state 1`] = ` 16 | Object { 17 | "action": Object { 18 | "type": "@@krf/RESET>TESTPREFIX>TESTNAME", 19 | }, 20 | "state": Object {}, 21 | } 22 | `; 23 | 24 | exports[`middlewares/simple should set element [elm2] 1`] = ` 25 | Object { 26 | "action": Object { 27 | "payload": Object { 28 | "code": "elm2", 29 | "infos": "elm2", 30 | "some": "other", 31 | }, 32 | "type": "@@krf/SET>TESTPREFIX>TESTNAME", 33 | }, 34 | "state": Object { 35 | "code": "elm2", 36 | "infos": "elm2", 37 | "some": "other", 38 | }, 39 | } 40 | `; 41 | 42 | exports[`middlewares/simple should take defaultData param to populate data field -init- 1`] = ` 43 | Object { 44 | "state": Object {}, 45 | } 46 | `; 47 | 48 | exports[`middlewares/simple should take defaultData param to populate data field -init- 2`] = ` 49 | Object { 50 | "state": Object { 51 | "im": "default", 52 | }, 53 | } 54 | `; 55 | 56 | exports[`middlewares/simple should take defaultData param to populate data field -init- 3`] = ` 57 | Object { 58 | "state": "", 59 | } 60 | `; 61 | 62 | exports[`middlewares/simple should take defaultData param to populate data field -reset- 1`] = ` 63 | Object { 64 | "action": Object { 65 | "type": "@@krf/RESET>TESTPREFIX>TESTNAME", 66 | }, 67 | "state": Object { 68 | "im": "default", 69 | }, 70 | } 71 | `; 72 | 73 | exports[`middlewares/simple should update element 1`] = ` 74 | Object { 75 | "action": Object { 76 | "payload": Object { 77 | "modifi": "cation", 78 | "some": "other 2", 79 | }, 80 | "type": "@@krf/UPDATE>TESTPREFIX>TESTNAME", 81 | }, 82 | "state": Object { 83 | "code": "elm", 84 | "infos": "elm", 85 | "modifi": "cation", 86 | "some": "other 2", 87 | }, 88 | } 89 | `; 90 | -------------------------------------------------------------------------------- /src/types/keyValue/keyValue.middleware.js: -------------------------------------------------------------------------------- 1 | import { SET, ADD, UPDATE, REMOVE, RESET, ADD_OR_UPDATE } from './keyValue.actions' 2 | 3 | export const initState = { 4 | data: [], // this is a serialized Map [[key, value], [key2, value2]] 5 | initialized: false, 6 | } 7 | 8 | const getAsArray = entry => (Array.isArray(entry) ? entry : [entry]) 9 | 10 | const toEntries = (key, payload) => payload.map(entity => [entity[key], entity]) 11 | 12 | const mapDataToState = state => data => ({ 13 | ...state, 14 | data, 15 | initialized: true, 16 | }) 17 | 18 | const set = (key, state, payload) => mapDataToState(state)(toEntries(key, getAsArray(payload))) 19 | 20 | const add = (key, state, payload) => mapDataToState(state)(Array.from(new Map([...state.data, ...toEntries(key, getAsArray(payload))]))) 21 | 22 | const addOrUpdate = shouldAdd => (key, state, payload) => { 23 | const map = new Map(state.data) 24 | const instances = getAsArray(payload) 25 | 26 | instances.forEach((instance) => { 27 | const stored = map.get(instance[key]) 28 | if (shouldAdd || instance) { 29 | map.set(instance[key], { ...stored, ...instance }) 30 | } 31 | }) 32 | 33 | if (map.size === 0) return state 34 | 35 | return mapDataToState(state)(Array.from(map.entries())) 36 | } 37 | 38 | const remove = (key, state, payload) => { 39 | const map = new Map(state.data) 40 | const instances = getAsArray(payload) 41 | 42 | instances.forEach((instance) => { 43 | map.delete(typeof instance === 'object' ? instance[key] : instance) 44 | }) 45 | 46 | return mapDataToState(state)(Array.from(map.entries())) 47 | } 48 | 49 | const defaultState = (key, defaultData) => (defaultData !== undefined ? set(key, initState, defaultData) : initState) 50 | 51 | const reducer = key => prefix => name => defaultData => ( 52 | (state = defaultState(key, defaultData), { type, payload } = {}) => { 53 | switch (type) { 54 | case SET(prefix)(name): return set(key, state, payload) 55 | case ADD(prefix)(name): return add(key, state, payload) 56 | case ADD_OR_UPDATE(prefix)(name): return addOrUpdate(true)(key, state, payload) 57 | case REMOVE(prefix)(name): return remove(key, state, payload) 58 | case RESET(prefix)(name): return defaultState(key, defaultData) 59 | case UPDATE(prefix)(name): return addOrUpdate(false)(key, state, payload) 60 | default: return state 61 | } 62 | } 63 | ) 64 | 65 | export default key => prefix => name => defaultData => (ctx = {}) => ({ 66 | ...ctx, 67 | state: reducer(key)(prefix)(name)(defaultData)(ctx.state, ctx.action), 68 | }) 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "k-redux-factory", 3 | "version": "6.0.6", 4 | "main": "dist/index.js", 5 | "module": "dist/index.es.js", 6 | "license": "MIT", 7 | "repository": "https://github.com/unirakun/k-redux-factory", 8 | "bugs": "https://github.com/unirakun/k-redux-factory/issues", 9 | "scripts": { 10 | "lint:js": "eslint --cache . --ext js,jsx --ignore-pattern dist/ --ignore-pattern coverage/ --ignore-pattern node_modules/ --ignore-pattern helpers/", 11 | "lint": "npm-run-all --parallel lint:*", 12 | "cmd:build": "microbundle --external all --sourcemap false -f es,cjs -i ", 13 | "build:core": "yarn cmd:build src/index.js", 14 | "build:helper": "yarn cmd:build src/helpers.js -o helpers/index.js", 15 | "build:copy-types": "node typing/copy.js", 16 | "build": "npm-run-all --parallel build:*", 17 | "test": "jest", 18 | "coveralls": "jest --coverage --collectCoverageFrom=src/**/*.{js,jsx} && cat ./coverage/lcov.info | coveralls", 19 | "ci": "npm-run-all --parallel coveralls lint" 20 | }, 21 | "devDependencies": { 22 | "@babel/cli": "7.6.4", 23 | "@babel/core": "7.6.4", 24 | "@babel/plugin-external-helpers": "7.2.0", 25 | "@babel/preset-env": "7.6.3", 26 | "babel-core": "7.0.0-bridge.0", 27 | "babel-eslint": "10.0.3", 28 | "babel-jest": "24.9.0", 29 | "babel-plugin-external-helpers": "6.22.0", 30 | "coveralls": "3.0.7", 31 | "cross-env": "6.0.3", 32 | "eslint": "6.6.0", 33 | "eslint-config-airbnb": "18.0.1", 34 | "eslint-import-resolver-node": "0.3.2", 35 | "eslint-plugin-babel": "5.3.0", 36 | "eslint-plugin-import": "2.18.2", 37 | "eslint-plugin-jsx-a11y": "6.2.3", 38 | "eslint-plugin-react": "7.16.0", 39 | "jest": "24.9.0", 40 | "jest-environment-jsdom": "24.9.0", 41 | "microbundle": "0.11.0", 42 | "npm-run-all": "4.1.5" 43 | }, 44 | "types": "./dist/index.d.ts", 45 | "keywords": [ 46 | "redux", 47 | "reducers", 48 | "selectors", 49 | "factory", 50 | "factories", 51 | "helper" 52 | ], 53 | "jest": { 54 | "coveragePathIgnorePatterns": [ 55 | "/node_modules/", 56 | "/dist/" 57 | ], 58 | "transform": { 59 | "^.+\\.jsx?$": "babel-jest" 60 | } 61 | }, 62 | "eslintConfig": { 63 | "parser": "babel-eslint", 64 | "extends": [ 65 | "airbnb" 66 | ], 67 | "plugins": [ 68 | "babel" 69 | ], 70 | "globals": { 71 | "fetch": false 72 | }, 73 | "env": { 74 | "browser": true 75 | }, 76 | "rules": { 77 | "object-curly-newline": "off", 78 | "semi": [ 79 | "error", 80 | "never" 81 | ], 82 | "arrow-parens": [ 83 | "error", 84 | "as-needed", 85 | { 86 | "requireForBlockBody": true 87 | } 88 | ], 89 | "max-len": [ 90 | "error", 91 | { 92 | "code": 200 93 | } 94 | ] 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/types/simple/simple.selectors.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import selectorsFactory from './simple.selectors' 3 | import { initState } from './simple.middleware' 4 | 5 | describe('selectors/simple => ', () => { 6 | describe('isInitialized', () => { 7 | describe('without defaultData', () => { 8 | const options = { name: 'o' } 9 | it('should not be initialized', () => { 10 | expect(selectorsFactory(options).isInitialized({ o: initState })).toMatchSnapshot() 11 | }) 12 | it('should be initialized -object-', () => { 13 | expect(selectorsFactory(options).isInitialized({ o: { i: 'initialized' } })).toMatchSnapshot() 14 | }) 15 | it('should be initialized -string-', () => { 16 | expect(selectorsFactory(options).isInitialized({ o: 'initialized' })).toMatchSnapshot() 17 | }) 18 | }) 19 | 20 | describe('defaultData as a string', () => { 21 | const options = { name: 'o', defaultData: 'defaultData' } 22 | it('should not be initialized', () => { 23 | expect(selectorsFactory(options).isInitialized({ o: options.defaultData })).toMatchSnapshot() 24 | }) 25 | it('should be initialized', () => { 26 | expect(selectorsFactory(options).isInitialized({ o: 'initialized' })).toMatchSnapshot() 27 | }) 28 | }) 29 | 30 | describe('defaultData as an object', () => { 31 | const options = { name: 'o', defaultData: {} } 32 | it('should not be initialized', () => { 33 | expect(selectorsFactory(options).isInitialized({ o: options.defaultData })).toMatchSnapshot() 34 | }) 35 | it('should be initialized', () => { 36 | expect(selectorsFactory(options).isInitialized({ o: { i: 'initialized' } })).toMatchSnapshot() 37 | }) 38 | }) 39 | }) 40 | 41 | describe('get', () => { 42 | describe('without defaultData', () => { 43 | const options = { name: 'o' } 44 | it('should return the initState', () => { 45 | expect(selectorsFactory(options).get()({ o: initState })).toMatchSnapshot() 46 | }) 47 | }) 48 | 49 | describe('defaultData as a string', () => { 50 | const options = { name: 'o', defaultData: '' } 51 | it('should return the default data', () => { 52 | expect(selectorsFactory(options).get()({ o: options.defaultData })).toMatchSnapshot() 53 | }) 54 | it('should return the current data', () => { 55 | expect(selectorsFactory(options).get()({ o: 'DATA' })).toMatchSnapshot() 56 | }) 57 | }) 58 | 59 | describe('defaultData as an object', () => { 60 | const options = { name: 'o', defaultData: {} } 61 | it('should return the default data', () => { 62 | expect(selectorsFactory({ name: 'o' }).get()({ o: options.defaultData })).toMatchSnapshot() 63 | }) 64 | it('should return the current data', () => { 65 | expect(selectorsFactory(options).get()({ o: { i: 'DATA' } })).toMatchSnapshot() 66 | }) 67 | }) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /src/factories.js: -------------------------------------------------------------------------------- 1 | import * as types from './types/index' 2 | import reducer from './reducer' 3 | 4 | const defaultOptions = { 5 | key: 'id', 6 | type: 'keyValue', 7 | prefix: '', 8 | } 9 | 10 | const simpleDefaultData = { 11 | bool: false, 12 | string: '', 13 | array: [], 14 | object: {}, 15 | number: 0, 16 | } 17 | 18 | const getWrappedStore = (middlewares = {}) => (options = {}) => { 19 | const innerOptions = { ...defaultOptions, ...options } 20 | const { 21 | key, 22 | type, 23 | prefix, 24 | name, 25 | defaultData, 26 | } = innerOptions 27 | 28 | // eslint-disable-next-line max-len 29 | if (type === 'simpleObject') console.warn('/k-redux-factory/ You are using a deprecated "simpleObject" type. We recommend using one of these types: simple.object, simple.array, simple.bool or simple.string.') 30 | 31 | const [innerType, subType] = type.split('.') 32 | const typeConfig = types[innerType] 33 | 34 | // get default data 35 | // - default is the given one 36 | // - if there is no given one we try to retrieve the one associated with the type (simple.) 37 | if (defaultData === undefined && subType) { 38 | innerOptions.defaultData = simpleDefaultData[subType] 39 | } 40 | 41 | const result = Object.assign( 42 | reducer({ ...middlewares, engine: typeConfig.middlewares })(key)(prefix)(name)(innerOptions.defaultData), 43 | 44 | // type (debug purpose) 45 | { krfType: type }, 46 | 47 | // actions 48 | ...Object.keys(typeConfig.actions).map(k => ({ [k]: typeConfig.actions[k](prefix)(name) })), 49 | 50 | // selectors 51 | typeConfig.selectors(innerOptions), 52 | ) 53 | 54 | return result 55 | } 56 | 57 | // error :( - not a middleware nor an option parameter 58 | const error = () => { throw Error('parameter is not a middleware configuration, nor a factory option object.') } 59 | 60 | // params checkers 61 | const isMiddleware = params => params.engine || params.pre || params.post 62 | const isOptions = params => (!!params.name) || (typeof params === 'string') 63 | 64 | export const factory = (forcedOptions = {}) => (params) => { 65 | // no param : error 66 | if (params === null || params === undefined) return error() 67 | 68 | // middleware case 69 | if (isMiddleware(params)) { 70 | return options => getWrappedStore(params)({ ...options, ...forcedOptions }) 71 | } 72 | // no middleware case 73 | if (isOptions(params)) { 74 | if (typeof params === 'string') return getWrappedStore()({ name: params, ...forcedOptions }) 75 | return getWrappedStore()({ ...params, ...forcedOptions }) 76 | } 77 | 78 | // not a valid param 79 | return error() 80 | } 81 | 82 | export const keyValue = factory({ type: 'keyValue' }) 83 | 84 | export const simple = factory({ type: 'simple' }) 85 | Object.assign( 86 | simple, 87 | { 88 | object: factory({ type: 'simple.object' }), 89 | bool: factory({ type: 'simple.bool' }), 90 | string: factory({ type: 'simple.string' }), 91 | array: factory({ type: 'simple.array' }), 92 | number: factory({ type: 'simple.number' }), 93 | }, 94 | ) 95 | 96 | // older method deprecated 97 | export const simpleObject = simple 98 | -------------------------------------------------------------------------------- /src/types/keyValue/keyValue.selectors.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import selectorsFactory from './keyValue.selectors' 3 | 4 | const Element = code => ({ code, some: 'other', infos: code }) 5 | const SubElement = code => subCode => ({ ...Element(code), sub: { subCode } }) 6 | 7 | const path = 'api.raw' 8 | const name = 'testName' 9 | const subState = { 10 | testName: { 11 | data: [ 12 | ['elm2', Element('elm2')], 13 | ['elm1', Element('elm1')], 14 | ['elm3', Element('elm3')], 15 | ['elm4', SubElement('elm4')('subelm4')], 16 | ['elm5', SubElement('elm5')('subelm5')], 17 | [false, Element(false)], 18 | [0, Element(0)], 19 | ], 20 | initialized: true, 21 | }, 22 | } 23 | const state = { 24 | api: { 25 | raw: { 26 | ...subState, 27 | }, 28 | }, 29 | } 30 | 31 | const { 32 | getState, 33 | getKeys, 34 | getLength, 35 | isInitialized, 36 | get, 37 | getAsArray, 38 | getBy, 39 | hasKey, 40 | } = selectorsFactory({ name, path }) 41 | 42 | describe('selectors', () => { 43 | it('should retrieve state', () => expect(getState(state)).toMatchSnapshot()) 44 | it('should retrieve state, without path set', () => expect(selectorsFactory({ name }).getState(subState)).toMatchSnapshot()) 45 | it('should retrieve keys', () => expect(getKeys(state)).toMatchSnapshot()) 46 | it('should retrieve length', () => expect(getLength(state)).toMatchSnapshot()) 47 | it('should retrieve initialized value', () => expect(isInitialized(state)).toMatchSnapshot()) 48 | it('should retrieve all data, without key', () => expect(get()(state)).toMatchSnapshot()) 49 | it('should retrieve all data, with null parameters', () => expect(get(null)(state)).toMatchSnapshot()) 50 | it('should retrieve data by ids, when ids is false', () => expect(get(false)(state)).toMatchSnapshot()) 51 | it('should retrieve data by ids, when ids is 0', () => expect(get(0)(state)).toMatchSnapshot()) 52 | it('should retrieve data by ids, with an array of keys', () => expect(get(['elm1', 'elm3'])(state)).toMatchSnapshot()) 53 | it('should retrieve data by id, with a single key', () => expect(get('elm3')(state)).toMatchSnapshot()) 54 | it('should retrieve all data, without key to array', () => expect(getAsArray(state)).toMatchSnapshot()) 55 | it('should retrieve all data, validate (path, values), with simple path and simple value', () => expect(getBy('code', 'elm2')(state)).toMatchSnapshot()) 56 | it('should retrieve all data, validate (path, values), with sub path and simple value', () => expect(getBy('sub.subCode', 'subelm4')(state)).toMatchSnapshot()) 57 | it('should retrieve all data, validate (path, values), with simple path and multi value', () => expect(getBy('code', ['elm1', 'elm4', 'subelm5'])(state)).toMatchSnapshot()) 58 | it('should retrieve all data, validate (path, values), with sub path and multi value', () => expect(getBy('sub.subCode', ['subelm4', 'subelm5', 'elm1'])(state)).toMatchSnapshot()) 59 | it('should retrieve all data, validate (path, values), with wrong path', () => expect(getBy('sub.wrong', 'subelm4')(state)).toMatchSnapshot()) 60 | it('should retrieve all data, validate (path, values), when not find value', () => expect(getBy('sub.wrong', 'subelm4')(state)).toMatchSnapshot()) 61 | it('should find the given key', () => expect(hasKey('elm3')(state)).toBe(true)) 62 | it('should not find the given key', () => expect(hasKey('not-found')(state)).toBe(false)) 63 | 64 | describe('bugs', () => { 65 | it('should works with empty path (not undefined) for first level reducer', () => { 66 | const bugState = { 67 | firstLevel: 'firstLevel', 68 | } 69 | 70 | expect(selectorsFactory({ path: '', name: 'firstLevel' }).getState(bugState)).toMatchSnapshot() 71 | }) 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /src/__snapshots__/reducer.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`reducer should call -engine- middleware 1`] = ` 4 | Object { 5 | "data": Array [ 6 | Object { 7 | "some": "data", 8 | }, 9 | ], 10 | "defaultData": undefined, 11 | "engine": true, 12 | "key": "code", 13 | "name": "testName", 14 | "prefix": "testPrefix", 15 | "prev": Object { 16 | "action": Object { 17 | "payload": Object { 18 | "code": "1", 19 | "some": "info", 20 | }, 21 | "type": "@@krf/ADD>TESTPREFIX>TESTNAME", 22 | }, 23 | "state": Object { 24 | "data": Array [ 25 | Object { 26 | "some": "data", 27 | }, 28 | ], 29 | }, 30 | }, 31 | } 32 | `; 33 | 34 | exports[`reducer should call -post- middlewares 1`] = ` 35 | Object { 36 | "data": Array [ 37 | Object { 38 | "some": "data", 39 | }, 40 | ], 41 | "defaultData": undefined, 42 | "key": "code", 43 | "name": "testName", 44 | "post1": true, 45 | "post2": true, 46 | "prefix": "testPrefix", 47 | "prev": Object { 48 | "action": Object { 49 | "payload": Object { 50 | "code": "1", 51 | "some": "info", 52 | }, 53 | "testName": true, 54 | "type": "@@krf/ADD>TESTPREFIX>TESTNAME", 55 | }, 56 | "state": Object { 57 | "data": Array [ 58 | Object { 59 | "some": "data", 60 | }, 61 | ], 62 | "defaultData": undefined, 63 | "key": "code", 64 | "name": "testName", 65 | "post1": true, 66 | "prefix": "testPrefix", 67 | "prev": Object { 68 | "action": Object { 69 | "payload": Object { 70 | "code": "1", 71 | "some": "info", 72 | }, 73 | "type": "@@krf/ADD>TESTPREFIX>TESTNAME", 74 | }, 75 | "state": Object { 76 | "data": Array [ 77 | Object { 78 | "some": "data", 79 | }, 80 | ], 81 | }, 82 | }, 83 | }, 84 | }, 85 | } 86 | `; 87 | 88 | exports[`reducer should call -pre- middlewares 1`] = ` 89 | Object { 90 | "data": Array [ 91 | Object { 92 | "some": "data", 93 | }, 94 | ], 95 | "defaultData": undefined, 96 | "key": "code", 97 | "name": "testName", 98 | "pre1": true, 99 | "pre2": true, 100 | "prefix": "testPrefix", 101 | "prev": Object { 102 | "action": Object { 103 | "payload": Object { 104 | "code": "1", 105 | "some": "info", 106 | }, 107 | "testName": true, 108 | "type": "@@krf/ADD>TESTPREFIX>TESTNAME", 109 | }, 110 | "state": Object { 111 | "data": Array [ 112 | Object { 113 | "some": "data", 114 | }, 115 | ], 116 | "defaultData": undefined, 117 | "key": "code", 118 | "name": "testName", 119 | "pre1": true, 120 | "prefix": "testPrefix", 121 | "prev": Object { 122 | "action": Object { 123 | "payload": Object { 124 | "code": "1", 125 | "some": "info", 126 | }, 127 | "type": "@@krf/ADD>TESTPREFIX>TESTNAME", 128 | }, 129 | "state": Object { 130 | "data": Array [ 131 | Object { 132 | "some": "data", 133 | }, 134 | ], 135 | }, 136 | }, 137 | }, 138 | }, 139 | } 140 | `; 141 | 142 | exports[`reducer should initialize an action if undefined 1`] = ` 143 | Object { 144 | "data": Array [ 145 | Object { 146 | "some": "data", 147 | }, 148 | ], 149 | "defaultData": undefined, 150 | "engine": true, 151 | "key": "code", 152 | "name": "testName", 153 | "prefix": "testPrefix", 154 | "prev": Object { 155 | "action": Object { 156 | "payload": undefined, 157 | "type": "UNKNOWN", 158 | }, 159 | "state": Object { 160 | "data": Array [ 161 | Object { 162 | "some": "data", 163 | }, 164 | ], 165 | }, 166 | }, 167 | } 168 | `; 169 | -------------------------------------------------------------------------------- /perf.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | // init avec default data 3 | // 100 SET d'un tableau de 1000 4 | // 100 ADD 5 | // 100 UPDATE 6 | // 100 ADD_OR_UPDATE 7 | // 100 REMOVE 8 | //