Showing preview only (229K chars total). Download the full file or copy to clipboard to get everything.
Repository: egorgrushin/painless-redux
Branch: master
Commit: b5ca0bc04700
Files: 87
Total size: 207.6 KB
Directory structure:
gitextract__my3mksh/
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── package.json
├── src/
│ ├── affect-loading-state/
│ │ ├── affect-loading-state.ts
│ │ └── types.ts
│ ├── dispatcher/
│ │ ├── dispatcher.ts
│ │ └── types.ts
│ ├── entity/
│ │ ├── action-creators.ts
│ │ ├── action-creators.types.ts
│ │ ├── actions.ts
│ │ ├── constants.ts
│ │ ├── entity.int-spec.ts
│ │ ├── entity.spec.ts
│ │ ├── entity.ts
│ │ ├── methods/
│ │ │ ├── dispatch/
│ │ │ │ ├── dispatch.ts
│ │ │ │ └── types.ts
│ │ │ ├── mixed/
│ │ │ │ ├── mixed.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── utils.ts
│ │ │ └── select/
│ │ │ ├── select.ts
│ │ │ └── types.ts
│ │ ├── reducer.ts
│ │ ├── reducers/
│ │ │ ├── dictionary.spec.ts
│ │ │ ├── dictionary.ts
│ │ │ ├── ids.spec.ts
│ │ │ ├── ids.ts
│ │ │ ├── instance.spec.ts
│ │ │ ├── instance.ts
│ │ │ ├── loading-state.ts
│ │ │ ├── loading-states.ts
│ │ │ ├── pages.spec.ts
│ │ │ └── pages.ts
│ │ ├── selectors.ts
│ │ ├── types.ts
│ │ └── utils.ts
│ ├── index.ts
│ ├── painless-redux/
│ │ ├── action-creators.ts
│ │ ├── constants.ts
│ │ ├── painless-redux.ts
│ │ ├── reducers.ts
│ │ ├── register.ts
│ │ ├── selectors.ts
│ │ ├── types.ts
│ │ └── utils.ts
│ ├── select-manager/
│ │ ├── select-manager.ts
│ │ └── types.ts
│ ├── shared/
│ │ ├── change/
│ │ │ ├── actions.ts
│ │ │ ├── reducer.spec.ts
│ │ │ ├── reducer.ts
│ │ │ ├── selectors.ts
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ ├── loading-state/
│ │ │ ├── actions.ts
│ │ │ ├── reducers.spec.ts
│ │ │ ├── reducers.ts
│ │ │ ├── selectors.ts
│ │ │ └── types.ts
│ │ ├── system/
│ │ │ ├── actions.ts
│ │ │ ├── reducers.ts
│ │ │ ├── system.ts
│ │ │ └── types.ts
│ │ ├── types.ts
│ │ └── utils.ts
│ ├── system-types.ts
│ ├── testing/
│ │ ├── helpers.ts
│ │ └── store.ts
│ ├── utils.ts
│ └── workspace/
│ ├── action-creators.ts
│ ├── actions.ts
│ ├── constants.ts
│ ├── methods/
│ │ ├── dispatch/
│ │ │ ├── dispatch.ts
│ │ │ └── types.ts
│ │ ├── mixed/
│ │ │ ├── mixed.ts
│ │ │ └── types.ts
│ │ └── select/
│ │ ├── select.ts
│ │ └── types.ts
│ ├── reducer.ts
│ ├── selectors.ts
│ ├── types.ts
│ ├── utils.ts
│ ├── workspace.spec.ts
│ └── workspace.ts
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintignore
================================================
**/*.spec.ts
================================================
FILE: .eslintrc.json
================================================
{
// Настройки проекта
"env": {
// Проект для браузера
"browser": true,
// Включаем возможности ES6
"es6": true,
// Добавляем возможности ES2017
"es2017": true
},
// Наборы правил
"extends": [
// Базовый набор правил eslint
"eslint:recommended",
// Отключаем правила из базового набора
"plugin:@typescript-eslint/eslint-recommended",
// Базовые правила для TypeScript
"plugin:@typescript-eslint/recommended",
// Правила TS, требующие инфо о типах
"plugin:@typescript-eslint/recommended-requiring-type-checking"
],
// Движок парсинга
"parser": "@typescript-eslint/parser",
"parserOptions": {
// Движку нужен проект TS для правил с типами
"project": "tsconfig.json",
"tsconfigRootDir": "."
},
// Плагин с наборами правил для TypeScript
"plugins": [
"@typescript-eslint"
],
"rules": {
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/unbound-method": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-unused-vars": "off"
}
}
================================================
FILE: .gitignore
================================================
dist
node_modules
.idea
/compiled
*.metadata.json
================================================
FILE: .npmignore
================================================
node_modules
.idea
/compiled
/src/testing
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2019 Egor Grushin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# painless-redux
Reducers-actions-selectors free reactive state management in redux-way
# Overview
This package allows you to use CRUD (Create, Read, Update and Delete) manipulations with entities and workspaces.
General features:
- It provides several simple methods such as get, create, remove, change etc. for using on Entity instance.
- It provides loading state management (i.e. isLoading and error).
- Underhood it uses any redux-like library you want to (e.g. [@ngrx/store](https://github.com/ngrx/platform)), so it means you can use [Redux DevTools](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=ru) but with this library you don't have to create boilerplate code (e.g. reducers, actions, selectors, action creators etc.).
- All methods working with outer data sources (e.g. requests passed to `get$` method) are [RxJS](https://github.com/ReactiveX/rxjs) powered.
- It supports optimistic change, remove, add
- It provides Workspace (documentation will be ready soon) which allows you store filter, sorting, ui states etc.
# Requirements
1. To be familiar with [redux](https://github.com/reduxjs/redux)
2. To be familiar with [RxJS](https://github.com/ReactiveX/rxjs)
# Documentation
[Here](https://github.com/egorgrushin/painless-redux/wiki)
# Plain use
1. install using npm:
`npm i painless-redux`
2. create an store:
```typescript
import { createPainlessRedux, RxStore } from 'painless-redux';
const store: RxStore = <use any implementation you want>;
export const PAINLESS_REDUX_STORE = createPainlessRedux(store);
```
3. create an entity:
```typescript
import { createEntity } from 'painless-redux';
import { PAINLESS_REDUX_STORE } from './store';
export interface Painter {
id: number | string;
fullName: string;
born: number;
}
const PaintersEntity = createEntity<Painter>({ name: 'painters' });
PAINLESS_REDUX_STORE.registerSlot(PaintersEntity);
export PaintersEntity;
```
4. add new entity
```typescript
PaintersEntity.add({ id: 1, fullName: 'Vincent van Gogh', born: 1853 });
```
5. get entity or all entities
```typescript
PaintersEntity.getById$(1).subscribe((painter: Painter) => {});
PaintersEntity.get$().subscribe((painters: Painter[]) => {});
```
# Use with Angular
[This adapter](https://github.com/egorgrushin/ngx-painless-redux) will help you to connect `painless-redux` to your Angular project, who uses [@ngrx/store](https://github.com/ngrx/platform).
# Use with React
[This adapter](https://www.npmjs.com/package/react-painless-redux) will help you to connect `painless-redux` to your React project, who uses [@reduxjs/toolkit](https://www.npmjs.com/package/@reduxjs/toolkit).
# Common use
This part can be difficult to understand, but this is main feature of this library.
Commonly you need to load some entities from outer source (e.g. your backend api) with given filter. To achieve this you need to prepare your data source using RxJS's observable and use `Entity.get$` method like this:
```typescript
import { Observable, of } from 'rxjs';
import { PaintersEntity } from './painters';
const init = () => {
const config = {};
getPainters$(config).subscribe((painters: Painter[]) => {
// emits:
// 1. undefined immediately
// 2. painters array when getPaintersFromApi$'s observable emits.
});
}
const getPainters$ = (config: unknown): Observable<Painter[]> {
const dataSource$ = getPaintersFromApi$(config);
return PaintersEntity.get$(config, dataSource$);
}
const getPaintersFromApi$ = (config: unknown): Observable<Painter[]> => {
// use can use any data source you need, this is for demo purposes.
const painters: Painter[] = [
{ id: 1, fullName: 'Leonardo da Vinci', born: 1452 },
{ id: 2, fullName: 'Vincent van Gogh', born: 1853 },
{ id: 3, fullName: 'Pablo Picasso', born: 1881 },
];
return of(painters);
}
```
`Entity.get$` algorithm is described [here](https://github.com/egorgrushin/painless-redux/wiki/Entity#get_observable)
# Pagination
`Entity.get$` method supports pagination. For this you have to pass `paginator` BehaviorSubject as the last argument:
```typescript
import { Observable, of, BehaviorSubject } from 'rxjs';
import { Pagination } from 'painless-redux';
import { PaintersEntity } from './painters';
const init = () => {
const paginator = new BehaviorSubject(false);
const config = {};
getPainters$(config, paginator).subscribe((painters: Painter[]) => {
// emits:
// 1. undefined immediately
// 2. painters array when getPaintersFromApi$'s observable emits.
// idle 3000ms
// 3. painters array from second emit merged with another getPaintersFromApi$'s observable emits.
});
setTimeout(() => {
paginator.next(true);
}, 3000)
}
const getPainters$ = (config: unknown, paginator: BehaviorSubject<boolean>): Observable<Painter[]> {
const dataSource = ({ from, to, size, index }: Pagination) => getPaintersFromApi$(config, from, to);
return PaintersEntity.get$(config, dataSource, null, paginator);
}
const getPaintersFromApi$ = (config: unknown, from: number, to: number): Observable<Painter[]> => {
// use can use any data source you need, this is for demo purposes.
const painters: Painter[] = [
{ id: 1, fullName: 'Leonardo da Vinci', born: 1452 },
{ id: 2, fullName: 'Vincent van Gogh', born: 1853 },
{ id: 3, fullName: 'Pablo Picasso', born: 1881 },
].slice(from, to);
return of(painters);
}
================================================
FILE: package.json
================================================
{
"name": "painless-redux",
"version": "4.1.17",
"description": "Reducers-actions-selectors free reactive state management in redux-way",
"main": "dist/index.js",
"scripts": {
"build": "rimraf dist && tsc",
"preversion": "npm run test && npm run build",
"test": "jest"
},
"jest": {
"roots": [
"<rootDir>/src"
],
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec|int-spec))\\.tsx?$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
],
"preset": "ts-jest",
"moduleNameMapper": {
"^lodash-es$": "<rootDir>/node_modules/lodash/index.js"
}
},
"repository": {
"type": "git",
"url": "git+https://github.com/egorgrushin/painless-redux.git"
},
"keywords": [
"redux",
"rxjs"
],
"types": "dist/index.d.ts",
"author": "Grushin Egor",
"license": "MIT",
"bugs": {
"url": "https://github.com/egorgrushin/painless-redux/issues"
},
"homepage": "https://github.com/egorgrushin/painless-redux#readme",
"dependencies": {
"lodash-es": "^4.17.21",
"object-hash": "^2.0.3",
"reselect": "4.0.0",
"uuid": "^8.3.2"
},
"devDependencies": {
"@types/crypto-js": "^3.1.43",
"@types/jest": "^24.9.1",
"@types/lodash-es": "^4.17.5",
"@types/node": "^12.7.5",
"@types/object-hash": "^1.3.1",
"@types/uuid": "^8.3.2",
"@typescript-eslint/eslint-plugin": "^2.20.0",
"@typescript-eslint/parser": "^2.20.0",
"eslint": "^6.8.0",
"jest": "^25.2.7",
"jest-marbles": "^2.5.1",
"lodash": "^4.17.21",
"rimraf": "^2.6.3",
"rxjs": "^6.5.3",
"ts-jest": "^25.2.1",
"typescript": "^3.8.2"
},
"peerDependencies": {
"rxjs": "^6.3.3"
}
}
================================================
FILE: src/affect-loading-state/affect-loading-state.ts
================================================
import { EMPTY, Observable, of, OperatorFunction, throwError } from 'rxjs';
import { catchError, finalize, switchMap, tap } from 'rxjs/operators';
import { AffectLoadingStateFactory, AffectStateSetter } from './types';
import { isFunction } from 'lodash-es';
export const affectLoadingStateOperatorFactory = <T, E>(
setter: AffectStateSetter<T, E>,
rethrow: boolean = true,
) => (
...pipes: Array<OperatorFunction<any, any>>
): OperatorFunction<any, any> => (
source: Observable<T>,
) => {
let stateCleared: boolean;
return (source as any).pipe(
tap((value: T) => {
setter?.({ isLoading: true, error: undefined }, false, value);
stateCleared = false;
}),
switchMap((value: T) => (of(value) as any).pipe(
...pipes,
catchError((error: E) => {
setter?.({ isLoading: false, error }, false, undefined);
stateCleared = true;
return rethrow ? throwError(error) : EMPTY;
}),
)),
tap((value: T) => {
setter?.({ isLoading: false }, false, value);
stateCleared = true;
}),
finalize(() => {
if (stateCleared) return;
setter?.({ isLoading: false }, true, undefined);
}),
);
};
export const affectLoadingStateFactory = <T, E>(
setter: AffectStateSetter<T, E>,
rethrow: boolean = true,
): AffectLoadingStateFactory => (...pipesOrObs: any) => {
const obs = pipesOrObs[0];
const isPipes = isFunction(obs);
const operatorFactory = affectLoadingStateOperatorFactory(setter, rethrow);
if (isPipes) return operatorFactory(...pipesOrObs as OperatorFunction<T, T>[]);
const operator = operatorFactory(switchMap(() => obs));
return (of(undefined) as any).pipe(operator);
};
================================================
FILE: src/affect-loading-state/types.ts
================================================
import { LoadingState } from '../system-types';
import { Observable, OperatorFunction } from 'rxjs';
export interface AffectStateSetter<T = any, E = any> {
(
loadingState: LoadingState<E>,
isInterrupted: boolean,
value: T | undefined,
): void;
}
export interface AffectLoadingStateFactory {
<T>(...pipes: Array<OperatorFunction<any, any>>): OperatorFunction<T, T>;
<T>(observable: Observable<T>): Observable<T>;
<T>(...pipesOrObs: any): OperatorFunction<T, T> | Observable<T>;
}
================================================
FILE: src/dispatcher/dispatcher.ts
================================================
import { ActionCreator, AnyAction, RxStore, SameShaped } from '../system-types';
import { Dispatcher } from './types';
const createCreateAction = <TActionTypes, TActions>(
actionCreators: SameShaped<TActionTypes, ActionCreator<TActionTypes, TActions>>,
) => (
actionName: keyof TActionTypes,
args: any[],
options?: unknown,
): TActions => {
const actionCreator = actionCreators[actionName];
return actionCreator(...args, options);
};
export const createDispatcher = <TActionTypes, TActions extends AnyAction>(
rxStore: RxStore,
actionCreators: SameShaped<TActionTypes, ActionCreator<TActionTypes, TActions>>,
): Dispatcher<TActionTypes, TActions> => {
const dispatch = (action: AnyAction): void => rxStore.dispatch(action);
const createAction = createCreateAction(actionCreators);
const createAndDispatch = (
actionName: keyof TActionTypes,
args: any[],
options?: unknown,
): TActions => {
const action = createAction(actionName, args, options);
dispatch(action);
return action;
};
return {
dispatch,
createAndDispatch,
};
};
================================================
FILE: src/dispatcher/types.ts
================================================
export interface Dispatcher<TActionTypes, TActions> {
dispatch(action: TActions): void;
createAndDispatch(
actionName: keyof TActionTypes,
args: any[],
options?: any,
): TActions;
}
================================================
FILE: src/entity/action-creators.ts
================================================
import { EntityActionTypes, EntitySchema } from './types';
import {
createAdd,
createAddList,
createChange,
createChangeList,
createClear,
createClearAll,
createRemove,
createRemoveList,
createResolveAdd,
createResolveChange,
createResolveChangeList,
createResolveRemove,
createResolveRemoveList,
createRestoreRemoved,
createRestoreRemovedList,
createSetLoadingState,
createSetLoadingStates,
} from './actions';
import { createBatch } from '../shared/system/actions';
import { EntityActionCreators } from './action-creators.types';
export const createEntityActionCreators = <T, TPageMetadata>(
actionTypes: EntityActionTypes,
schema: EntitySchema<T>,
): EntityActionCreators<T, TPageMetadata> => ({
ADD: createAdd<T>(actionTypes, schema),
RESOLVE_ADD: createResolveAdd<T>(actionTypes, schema),
ADD_LIST: createAddList<T, TPageMetadata>(actionTypes, schema),
CHANGE: createChange<T>(actionTypes),
RESOLVE_CHANGE: createResolveChange<T>(actionTypes),
REMOVE: createRemove(actionTypes),
RESOLVE_REMOVE: createResolveRemove<T>(actionTypes),
RESTORE_REMOVED: createRestoreRemoved<T>(actionTypes),
REMOVE_LIST: createRemoveList(actionTypes),
RESOLVE_REMOVE_LIST: createResolveRemoveList(actionTypes),
RESTORE_REMOVED_LIST: createRestoreRemovedList<T>(actionTypes),
SET_LOADING_STATE: createSetLoadingState(actionTypes, schema),
CLEAR: createClear(actionTypes, schema),
CLEAR_ALL: createClearAll(actionTypes),
BATCH: createBatch<T>(actionTypes),
CHANGE_LIST: createChangeList<T>(actionTypes),
SET_LOADING_STATES: createSetLoadingStates(actionTypes),
RESOLVE_CHANGE_LIST: createResolveChangeList<T>(actionTypes),
});
================================================
FILE: src/entity/action-creators.types.ts
================================================
import {
EntityAddOptions,
EntityInternalAddListOptions,
EntityInternalAddOptions,
EntityInternalSetLoadingStateOptions,
EntityRemoveListOptions,
EntityRemoveOptions,
EntityType,
IdPatch,
} from './types';
import {DeepPartial, Id, LoadingState} from '../system-types';
import {ChangeOptions} from '../shared/change/types';
// this types for public use
export interface EntityActionCreators<T, TPageMetadata> {
ADD_LIST: (
entities: EntityType<T>[],
config?: unknown,
isReplace?: boolean,
hasMore?: boolean,
metadata?: TPageMetadata,
options?: EntityInternalAddListOptions,
) => { payload: { entities: EntityType<T>[]; isReplace: boolean; hasMore: boolean; configHash: string; metadata: TPageMetadata | undefined }; options: EntityInternalAddListOptions; type: 'ADD_LIST' };
RESTORE_REMOVED: (id: Id) => { payload: { id: Id }; type: 'RESTORE_REMOVED' };
ADD: (
entity: EntityType<T>,
config?: unknown,
tempId?: string,
options?: EntityInternalAddOptions,
) => { payload: { configHash: string; tempId: string | undefined; entity: { id: Id } }; options: EntityInternalAddOptions; type: 'ADD' };
RESOLVE_ADD: (
result: EntityType<T>,
success: boolean,
tempId: string,
config?: unknown,
options?: EntityAddOptions,
) => { payload: { result: EntityType<T>; success: boolean; configHash: string; tempId: string }; options: EntityAddOptions; type: 'RESOLVE_ADD' };
CLEAR_ALL: () => { type: 'CLEAR_ALL' };
RESOLVE_REMOVE: (
id: Id,
success: boolean,
options?: EntityRemoveOptions,
) => { payload: { success: boolean; id: Id }; options: EntityRemoveOptions; type: 'RESOLVE_REMOVE' };
REMOVE: (
id: Id,
options?: EntityRemoveOptions,
) => { payload: { id: Id }; options: EntityRemoveOptions; type: 'REMOVE' };
REMOVE_LIST: (
ids: Id[],
options?: EntityRemoveOptions,
) => { payload: { ids: Id[] }; options: EntityRemoveListOptions; type: 'REMOVE_LIST' };
RESOLVE_REMOVE_LIST: (
ids: Id[],
success: boolean,
options?: EntityRemoveOptions,
) => { payload: { success: boolean; ids: Id[] }; options: EntityRemoveListOptions; type: 'RESOLVE_REMOVE_LIST' };
RESTORE_REMOVED_LIST: (
ids: Id[],
) => { payload: { ids: Id[] }; type: 'RESTORE_REMOVED_LIST' };
CHANGE: (
id: Id,
patch: DeepPartial<T>,
changeId?: string,
options?: ChangeOptions,
) => { payload: { patch: DeepPartial<unknown>; changeId: string | undefined; id: Id }; readonly options: ChangeOptions; type: 'CHANGE' };
CHANGE_LIST: (
patches: IdPatch<T>[],
changeId?: string,
options?: ChangeOptions,
) => { payload: { patches: IdPatch<T>[]; changeId: string | undefined }; readonly options: ChangeOptions; type: 'CHANGE_LIST' };
BATCH: (actions: T[]) => { payload: { actions: T[] }; type: 'BATCH' };
SET_LOADING_STATE: (
state: LoadingState,
config?: unknown,
id?: Id,
key?: string,
options?: EntityInternalSetLoadingStateOptions,
) => { payload: { configHash: string; state: LoadingState<string>; id: Id | undefined; key: string | undefined }; options: { maxPagesCount: number }; readonly type: 'SET_LOADING_STATE' };
SET_LOADING_STATES: (
state: LoadingState,
ids: Id[],
options?: EntityInternalSetLoadingStateOptions,
) => { payload: { ids: Id[]; state: LoadingState<string> }; options: { maxPagesCount: number }; readonly type: 'SET_LOADING_STATES' };
RESOLVE_CHANGE: (
id: Id,
changeId: string,
success: boolean,
remotePatch?: DeepPartial<T>,
options?: ChangeOptions,
) => { payload: { success: boolean; remotePatch: DeepPartial<unknown> | undefined; changeId: string; id: Id }; readonly options: ChangeOptions; type: 'RESOLVE_CHANGE' };
RESOLVE_CHANGE_LIST: (
patches: IdPatch<T>[],
changeId: string,
success: boolean,
options?: ChangeOptions,
) => { payload: { patches: IdPatch<T>[]; changeId: string; success: boolean }; readonly options: ChangeOptions; type: 'RESOLVE_CHANGE_LIST' };
CLEAR: (config: unknown) => { payload: { configHash: string }; type: 'CLEAR' };
}
================================================
FILE: src/entity/actions.ts
================================================
import {
EntityActionTypes,
EntityAddOptions,
EntityInternalAddListOptions,
EntityInternalAddOptions,
EntityInternalSetLoadingStateOptions,
EntityRemoveListOptions,
EntityRemoveOptions,
EntitySchema,
EntityType,
IdPatch,
} from './types';
import { DeepPartial, Id, LoadingState } from '../system-types';
import { typedDefaultsDeep } from '../utils';
import * as loadingStateActions from '../shared/loading-state/actions';
import * as changeActions from '../shared/change/actions';
import { MAX_PAGES_COUNT } from './constants';
import { ChangeOptions } from '../shared/change/types';
import { SystemActions } from '../shared/system/actions';
export const createAddByHash = <T>(types: EntityActionTypes) => (
entity: EntityType<T>,
configHash: string,
tempId?: string,
options?: EntityInternalAddOptions,
) => {
options = typedDefaultsDeep(options, { merge: true, maxPagesCount: MAX_PAGES_COUNT });
const payload = { entity, configHash, tempId };
return { type: types.ADD, payload, options } as const;
};
export const createAdd = <T>(types: EntityActionTypes, schema: EntitySchema<T>) => (
entity: EntityType<T>,
config?: unknown,
tempId?: string,
options?: EntityInternalAddOptions,
) => {
const configHash = schema.hashFn(config);
options = typedDefaultsDeep(options, { maxPagesCount: MAX_PAGES_COUNT });
tempId = options.optimistic ? tempId : undefined;
entity = options.optimistic ? { ...entity, id: tempId as string } : entity;
return createAddByHash(types)(entity, configHash, tempId, options);
};
export const createResolveAdd = <T>(types: EntityActionTypes, schema: EntitySchema<T>) => (
result: EntityType<T>,
success: boolean,
tempId: string,
config?: unknown,
options?: EntityAddOptions,
) => {
const configHash = schema.hashFn(config);
options = typedDefaultsDeep(options, { merge: true });
const payload = { result, configHash, tempId, success };
return { type: types.RESOLVE_ADD, payload, options } as const;
};
export const createAddList = <T, TPageMetadata>(types: EntityActionTypes, schema: EntitySchema<T>) => (
entities: EntityType<T>[],
config?: unknown,
isReplace: boolean = false,
hasMore: boolean = false,
metadata?: TPageMetadata,
options?: EntityInternalAddListOptions,
) => {
const configHash = schema.hashFn(config);
options = typedDefaultsDeep(options, { merge: true, maxPagesCount: MAX_PAGES_COUNT });
const payload = { entities, configHash, isReplace, hasMore, metadata };
return { type: types.ADD_LIST, payload, options } as const;
};
export const createRemove = (types: EntityActionTypes) => (
id: Id,
options?: EntityRemoveOptions,
) => {
options = typedDefaultsDeep(options);
const payload = { id };
return { type: types.REMOVE, payload, options } as const;
};
export const createRemoveList = (types: EntityActionTypes) => (
ids: Id[],
options?: EntityRemoveListOptions,
) => {
options = typedDefaultsDeep(options);
const payload = { ids };
return { type: types.REMOVE_LIST, payload, options } as const;
};
export const createResolveRemoveList = (types: EntityActionTypes) => (
ids: Id[],
success: boolean,
options?: EntityRemoveListOptions,
) => {
options = typedDefaultsDeep(options);
const payload = { ids, success };
return { type: types.RESOLVE_REMOVE_LIST, payload, options } as const;
};
export const createSetLoadingState = (types: EntityActionTypes, schema: EntitySchema<unknown>) => (
state: LoadingState,
config?: unknown,
id?: Id,
key?: string,
options?: EntityInternalSetLoadingStateOptions,
) => {
const actionCreator = loadingStateActions.createSetLoadingState(types);
const action = actionCreator(state, key, options);
const configHash = schema.hashFn(config);
return {
...action,
payload: { ...action.payload, configHash, id },
options: { ...action.options, maxPagesCount: options?.maxPagesCount ?? MAX_PAGES_COUNT },
} as const;
};
export const createSetLoadingStates = (types: EntityActionTypes) => (
state: LoadingState,
ids: Id[],
options?: EntityInternalSetLoadingStateOptions,
) => {
return {
type: types.SET_LOADING_STATES,
payload: { state, ids },
options: { maxPagesCount: options?.maxPagesCount ?? MAX_PAGES_COUNT },
} as const;
};
export const createChange = <T>(types: EntityActionTypes) => (
id: Id,
patch: DeepPartial<T>,
changeId?: string,
options?: ChangeOptions,
) => {
const actionCreator = changeActions.createChange(types);
const action = actionCreator(patch, changeId, options);
return {
...action,
type: types.CHANGE,
payload: { ...action.payload, id },
} as const;
};
export const createChangeList = <T>(types: EntityActionTypes) => (
patches: IdPatch<T>[],
changeId?: string,
options?: ChangeOptions,
) => {
options = typedDefaultsDeep(options, { merge: true });
return {
type: types.CHANGE_LIST,
payload: { patches, changeId },
options,
} as const;
};
export const createResolveChange = <T>(types: EntityActionTypes) => (
id: Id,
changeId: string,
success: boolean,
remotePatch?: DeepPartial<T>,
options?: ChangeOptions,
) => {
const actionCreator = changeActions.createResolveChange(types);
const action = actionCreator(changeId, success, remotePatch, options);
return {
...action,
type: types.RESOLVE_CHANGE,
payload: { ...action.payload, id },
} as const;
};
export const createResolveChangeList = <T>(types: EntityActionTypes) => (
patches: IdPatch<T>[],
changeId: string,
success: boolean,
options?: ChangeOptions,
) => {
options = typedDefaultsDeep(options, { merge: true });
return {
type: types.RESOLVE_CHANGE_LIST,
payload: { patches, changeId, success },
options,
} as const;
};
export const createResolveRemove = <T>(types: EntityActionTypes) => (
id: Id,
success: boolean,
options?: EntityRemoveOptions,
) => {
options = typedDefaultsDeep(options);
return { type: types.RESOLVE_REMOVE, payload: { id, success }, options } as const;
};
export const createRestoreRemoved = <T>(types: EntityActionTypes) => (
id: Id,
) => {
return { type: types.RESTORE_REMOVED, payload: { id } } as const;
};
export const createRestoreRemovedList = <T>(types: EntityActionTypes) => (
ids: Id[],
) => {
return { type: types.RESTORE_REMOVED_LIST, payload: { ids } } as const;
};
export const createClear = (types: EntityActionTypes, schema: EntitySchema<unknown>) => (config: unknown) => {
const configHash = schema.hashFn(config);
return { type: types.CLEAR, payload: { configHash } } as const;
};
export const createClearAll = (types: EntityActionTypes) => () => {
return { type: types.CLEAR_ALL } as const;
};
type SelfActionCreators = ReturnType<typeof createAdd>
| ReturnType<typeof createResolveAdd>
| ReturnType<typeof createAddList>
| ReturnType<typeof createSetLoadingState>
| ReturnType<typeof createChange>
| ReturnType<typeof createResolveChange>
| ReturnType<typeof createRemove>
| ReturnType<typeof createResolveRemove>
| ReturnType<typeof createRestoreRemoved>
| ReturnType<typeof createRemoveList>
| ReturnType<typeof createResolveRemoveList>
| ReturnType<typeof createRestoreRemovedList>
| ReturnType<typeof createClear>
| ReturnType<typeof createClearAll>
| ReturnType<typeof createChangeList>
| ReturnType<typeof createSetLoadingStates>
| ReturnType<typeof createResolveChangeList>
export type EntityActions = ReturnType<SelfActionCreators> | SystemActions;
================================================
FILE: src/entity/constants.ts
================================================
import { EntityActionTypes } from './types';
export const DEFAULT_PAGE_SIZE = 300;
export const MAX_PAGES_COUNT = Infinity;
export const ENTITY_TYPE_NAMES: Array<keyof EntityActionTypes> = [
'ADD',
'RESOLVE_ADD',
'ADD_LIST',
'SET_LOADING_STATE',
'CHANGE',
'RESOLVE_CHANGE',
'REMOVE',
'RESOLVE_REMOVE',
'RESTORE_REMOVED',
'REMOVE_LIST',
'RESOLVE_REMOVE_LIST',
'RESTORE_REMOVED_LIST',
'CLEAR',
'CLEAR_ALL',
'BATCH',
'RESOLVE_CHANGE_LIST',
'CHANGE_LIST',
'SET_LOADING_STATES',
];
================================================
FILE: src/entity/entity.int-spec.ts
================================================
import {Id} from '../system-types';
import {Entity, EntityAddOptions, EntityRemoveOptions, Page} from './types';
import {createEntity} from './entity';
import {PainlessRedux} from '../painless-redux/types';
import {createPainlessRedux} from '../painless-redux/painless-redux';
import {TestStore} from '../testing/store';
import {cold} from 'jest-marbles';
import {switchMap} from 'rxjs/operators';
import {mocked} from 'ts-jest/utils';
import * as uuid from 'uuid';
import {ColdObservable} from 'rxjs/internal/testing/ColdObservable';
jest.mock('uuid');
type TestEntity = {
id: Id;
image?: string;
age?: number;
name?: string;
}
type TPageMetadata = any;
describe('[Integration] Entity', () => {
let entity: Entity<TestEntity, TPageMetadata>;
let pr: PainlessRedux;
let store: TestStore;
const filter = null;
const user: TestEntity = {id: 1, name: 'John'};
beforeEach(() => {
store = new TestStore({}, (state) => state);
pr = createPainlessRedux(store, {useAsapSchedulerInLoadingGuards: false});
entity = createEntity<TestEntity, TPageMetadata>(pr, {name: 'test', maxPagesCount: 2});
});
describe('#get$', () => {
test('should emit created earlier instance ', () => {
// arrange
entity.add(user);
const expected$ = cold('a', {a: [user]});
// act
const actual$ = entity.get$(filter);
// assert
expect(actual$).toBeObservable(expected$);
});
test('should emit instance after response', () => {
// arrange
const remote$ = cold(' --a', {a: {data: [user]}});
const expected$ = cold('a-b ', {a: undefined, b: [user]});
// act
const actual$ = entity.get$(filter, remote$);
// assert
expect(actual$).toBeObservable(expected$);
});
test('should not load instance if exist with single option ', () => {
// arrange
entity.add(user);
const remote$ = cold(' --a', {a: [user]});
const expected$ = cold('a ', {a: [user]});
// act
const actual$ = entity.get$(filter, remote$, {single: true});
// assert
expect(actual$).toBeObservable(expected$);
});
test('should not subscribe to remote$ if exist with single option ', () => {
// arrange
entity.add(user);
const remote$ = cold(' --a', {a: [user]});
// act
entity.get$(filter, remote$, {single: true}).subscribe();
// assert
expect(remote$).toHaveNoSubscriptions();
});
});
describe('getById$', () => {
test('should emit created earlier instance ', () => {
// arrange
entity.add(user);
const expected$ = cold('a', {a: user});
// act
const actual$ = entity.getById$(user.id);
// assert
expect(actual$).toBeObservable(expected$);
});
test('should emit instance after response', () => {
// arrange
const remote$ = cold(' --a', {a: user});
const expected$ = cold('a-b ', {a: undefined, b: user});
// act
const actual$ = entity.getById$(user.id, remote$);
// assert
expect(actual$).toBeObservable(expected$);
});
test('should not load instance if exist with single option ', () => {
// arrange
entity.add(user);
const remote$ = cold(' --a', {a: user});
const expected$ = cold('a ', {a: user});
// act
const actual$ = entity.getById$(user.id, remote$, {single: true});
// assert
expect(actual$).toBeObservable(expected$);
});
test('should not subscribe to remote$ if exist with single option ', () => {
// arrange
entity.add(user);
const remote$ = cold(' --a', {a: user});
// act
entity.getById$(user.id, remote$, {single: true}).subscribe();
// assert
expect(remote$).toHaveNoSubscriptions();
});
});
describe('getLoadingState$', () => {
test('should return loading state', () => {
// act
const actual$ = entity.getLoadingState$();
// assert
const expected$ = cold('a', {a: {isLoading: false}});
expect(actual$).toBeObservable(expected$);
});
test.each`
actionName | action
${'get$'} | ${(remote$: ColdObservable<any>) => entity.get$(filter, remote$)}
${'getById$'} | ${(remote$: ColdObservable<any>) => entity.getById$(user.id, remote$)}
${'addRemote$'} | ${(remote$: ColdObservable<any>) => entity.addRemote$(user, user.id, remote$)}
${'changeRemote$'} | ${(remote$: ColdObservable<any>) => entity.changeRemote$(user.id, {}, remote$)}
${'removeRemote$'} | ${(remote$: ColdObservable<any>) => entity.removeRemote$(user.id, remote$)}
`('should set loading state during remote$ for entity.$actionName', ({actionName, action}) => {
// arrange
const remoteMarble = ' --a';
const loadingStateMarble = 'a-b';
const response = actionName === 'get$' ? {data: undefined} : undefined;
const remote$ = cold(remoteMarble, {a: response});
// act
const actual$ = entity.getLoadingState$();
action(remote$).subscribe();
// assert
const expected$ = cold(loadingStateMarble, {
a: {isLoading: true},
b: {isLoading: false},
});
expect(actual$).toBeObservable(expected$);
});
});
describe('getLoadingStates$', () => {
test('should return loading states', () => {
// act
const actual$ = entity.getLoadingStates$();
// assert
const expected$ = cold('a', {a: {}});
expect(actual$).toBeObservable(expected$);
});
test.each`
actionName | action
${'getById$'} | ${(remote$: ColdObservable<any>) => entity.getById$(user.id, remote$)}
${'changeRemote$'} | ${(remote$: ColdObservable<any>) => entity.changeRemote$(user.id, {}, remote$)}
`('should set loading state during remote$ for entity.$actionName', ({action}) => {
// arrange
const remoteMarble = ' --a';
const loadingStateMarble = 'a-b';
const remote$ = cold(remoteMarble, {a: undefined});
// act
const actual$ = entity.getLoadingStates$();
action(remote$).subscribe();
// assert
const expected$ = cold(loadingStateMarble, {
a: {[user.id]: {isLoading: true}},
b: {[user.id]: {isLoading: false}},
});
expect(actual$).toBeObservable(expected$);
});
test('should set and then remove loading state for entity.removeRemote$', () => {
// arrange
const remoteMarble = ' --a';
const loadingStateMarble = 'a-(bc)';
const remote$ = cold(remoteMarble, {a: undefined});
// act
const actual$ = entity.getLoadingStates$();
entity.removeRemote$(user.id, remote$).subscribe();
// assert
const expected$ = cold(loadingStateMarble, {
a: {[user.id]: {isLoading: true}},
b: {[user.id]: {isLoading: false}},
c: {},
});
expect(actual$).toBeObservable(expected$);
});
});
describe('#changeRemote$', () => {
const patch = {name: 'Alice'};
const remotePatch = {name: 'Mike'};
beforeEach(() => {
entity.add(user);
});
test.each`
useResponsePatch
${false}
${true}
`(
'should apply either patch or remotePatch after response based on useResponsePatch: $useResponsePatch',
({useResponsePatch}) => {
// arrange
const resultPatch = useResponsePatch ? remotePatch : patch;
const remote$ = cold(' --a', {a: remotePatch});
const expected$ = cold('a-b ', {a: [user], b: [{...user, ...resultPatch}]});
const actual$ = entity.get$(filter);
const options = {useResponsePatch};
// act
entity.changeRemote$(user.id, patch, remote$, options).subscribe();
// assert
expect(actual$).toBeObservable(expected$);
},
);
test('should ignore in case of response fail', () => {
// arrange
const remote$ = cold(' --#');
const expected$ = cold('a', {a: [user]});
const actual$ = entity.get$(filter);
// act
entity.changeRemote$(user.id, patch, remote$).subscribe();
// assert
expect(actual$).toBeObservable(expected$);
});
test.each`
useResponsePatch
${false}
${true}
`(
'should apply patch immediately and based on useResponsePatch: $useResponsePatch apply remotePatch',
({useResponsePatch}) => {
// arrange
const remoteMarble = ' --a';
const expectedMarble = 'a-b-c';
const actMarble = ' --a';
const remote$ = cold(remoteMarble, {a: remotePatch});
const patched = {...user, ...patch};
const resultPatched = useResponsePatch ? {...patched, ...remotePatch} : patched;
const expected$ = cold(expectedMarble, {
a: [user],
b: [patched],
c: [resultPatched],
});
const actual$ = entity.get$(filter);
const options = {optimistic: true, useResponsePatch};
// act
cold(actMarble).pipe(
switchMap(() => entity.changeRemote$(user.id, patch, remote$, options)),
).subscribe();
// assert
expect(actual$).toBeObservable(expected$);
},
);
});
describe('#removeRemote', () => {
beforeEach(() => {
entity.add(user);
});
test.each`
remoteMarble | expectedMarble
${'--a'} | ${'a-b-b'}
${'--#'} | ${'a-b-a'}
`('should optimistic remove or undo for $remoteMarble', ({remoteMarble, expectedMarble}) => {
// arrange
const remote$ = cold(remoteMarble);
const expected$ = cold(expectedMarble, {a: [user], b: []});
const actual$ = entity.get$(filter);
const options: EntityRemoveOptions = {optimistic: true};
// act
cold('--a', {a: null}).pipe(
switchMap(() => entity.removeRemote$(user.id, remote$, options)),
).subscribe();
// assert
expect(actual$).toBeObservable(expected$);
});
});
describe('#addRemote', () => {
test.each`
remoteMarble | expectedMarble
${'--a'} | ${'a-b-c'}
${'--#'} | ${'a-b-d'}
`('should optimistic add with response.id or undo for $remoteMarble', ({remoteMarble, expectedMarble}) => {
// arrange
const newId = '999';
const tempId = '666';
mocked(uuid.v4).mockReturnValueOnce(tempId);
const response = {id: newId};
const remote$ = cold(remoteMarble, {a: response});
const expected$ = cold(expectedMarble, {
a: undefined,
b: [{...user, id: tempId}],
c: [{...user, id: newId}],
d: [],
});
const actual$ = entity.get$(filter);
const options: EntityAddOptions = {optimistic: true};
// act
cold('--a').pipe(
switchMap(() => entity.addRemote$(user, filter, remote$, options)),
).subscribe();
// assert
expect(actual$).toBeObservable(expected$);
});
});
describe('#getAll$', () => {
test('should return all entities', () => {
// arrange
const user1 = {id: 1, name: 'User 1'};
const user2 = {id: 2, name: 'User 2'};
const actual$ = entity.getAll$();
const actMarble = ' -a-b';
const expectedMarble = 'ab-c';
const expected$ = cold(expectedMarble, {a: [], b: [user1], c: [user1, user2]});
// act
cold(actMarble, {a: user1, b: user2}).subscribe((value) => {
entity.add(value, Math.random());
});
// assert
expect(actual$).toBeObservable(expected$);
});
});
describe('#clear', () => {
test('should clear entities from specific page', () => {
// arrange
const user1 = {id: 1, name: 'User 1'};
const user2 = {id: 2, name: 'User 2'};
const filter1 = Math.random();
const filter2 = Math.random();
entity.add(user1, filter1);
entity.add(user2, filter2);
const actual$ = entity.get$(filter1);
const actMarble = ' --a';
const expectedMarble = 'a-b';
const expected$ = cold(expectedMarble, {a: [user1], b: undefined});
// act
cold(actMarble).subscribe(() => {
entity.clear(filter1);
});
// assert
expect(actual$).toBeObservable(expected$);
});
});
describe('#clearAll', () => {
test('should clear all entities', () => {
// arrange
const user1 = {id: 1, name: 'User 1'};
const user2 = {id: 2, name: 'User 2'};
entity.add(user1, Math.random());
entity.add(user2, Math.random());
const actual$ = entity.getAll$();
const actMarble = ' --a';
const expectedMarble = 'a-b';
const expected$ = cold(expectedMarble, {a: [user1, user2], b: []});
// act
cold(actMarble).subscribe(() => {
entity.clearAll();
});
// assert
expect(actual$).toBeObservable(expected$);
});
});
describe('#maxPagesCount', () => {
test('should shift all pages order and remove first', () => {
// arrange
const user1 = {id: 1, name: 'User 1'};
const user2 = {id: 2, name: 'User 2'};
const user3 = {id: 3, name: 'User 2'};
entity.add(user1, Math.random());
entity.add(user2, Math.random());
const actual$ = entity.getPages$();
const actMarble = ' --a';
const expectedMarble = 'a-b';
const page1: Page<TPageMetadata> = {ids: [user1.id], order: 0};
const page2: Page<TPageMetadata> = {ids: [user2.id], order: 1};
const page3: Page<TPageMetadata> = {ids: [user3.id]};
const expected$ = cold(expectedMarble, {
a: [page1, page2],
b: [{...page2, order: 0}, {...page3, order: 1}],
});
// act
cold(actMarble).subscribe(() => {
entity.add(user3, Math.random());
});
// assert
expect(actual$).toBeObservable(expected$);
});
});
});
================================================
FILE: src/entity/entity.spec.ts
================================================
import { Id, LoadingState } from '../system-types';
import { getOrderedMarbleStream, initStoreWithPr } from '../testing/helpers';
import { cold } from 'jest-marbles';
import { TestStore } from '../testing/store';
import { createEntity } from './entity';
import { createPainlessRedux } from '../painless-redux/painless-redux';
import { PainlessRedux } from '../painless-redux/types';
import { Entity, EntityInternalSetLoadingStateOptions, EntitySchema, Pagination } from './types';
import { BehaviorSubject } from 'rxjs';
import { mocked } from 'ts-jest/utils';
import * as uuid from 'uuid';
import { ChangeOptions } from '../shared/change/types';
import { RequestOptions } from '../shared/types';
jest.mock('uuid');
interface TestEntity {
name: string;
}
describe('Entity', () => {
let entity: Entity<TestEntity, void>;
let pr: PainlessRedux;
let store: TestStore<any>;
let user: any;
let schema: Partial<EntitySchema<TestEntity>>;
let user2: any;
let user3: any;
let reducer: any;
let idFn = jest.fn((data) => data.id);
const setLoadingStateActionFactory = (
state: LoadingState,
id?: Id,
options?: RequestOptions,
) => entity.actionCreators.SET_LOADING_STATE(
state,
undefined,
id,
undefined,
options as unknown as EntityInternalSetLoadingStateOptions,
);
beforeEach(() => {
store = new TestStore(undefined, (state) => state);
pr = createPainlessRedux(store, { useAsapSchedulerInLoadingGuards: false });
schema = {
name: 'test',
pageSize: 2,
id: idFn,
};
entity = createEntity(pr, schema);
reducer = initStoreWithPr(store, pr);
user = { id: 1, name: 'John' };
user2 = { id: 2, name: 'Alex' };
user3 = { id: 3, name: 'Frank' };
});
describe('#add', () => {
test('should add entity', () => {
// arrange
const addAction = entity.actionCreators.ADD(user);
const actions$ = getOrderedMarbleStream(addAction);
// act
entity.add(user);
// assert
expect(store.actions$).toBeObservable(actions$);
});
test('should resolve an id from schema.id even it has id already, then add an entity', () => {
// arrange
mocked(idFn).mockImplementationOnce((data): string | number => `$${data.name}`);
const userWithIgnoredId = { name: user.name, id: 'should not be used' };
const addAction = entity.actionCreators.ADD({
...userWithIgnoredId,
id: `$${userWithIgnoredId.name}`,
});
const actions$ = getOrderedMarbleStream(addAction);
// act
entity.add(userWithIgnoredId);
// assert
expect(store.actions$).toBeObservable(actions$);
});
xtest('should resolve an id if it is not existed and schema.id is not defined, then add an entity', () => {
// arrange
const randomId = 'adav3r2brh35';
mocked(uuid.v4).mockReturnValueOnce(randomId);
const userWithoutId = { name: user.name };
const addAction = entity.actionCreators.ADD({
...userWithoutId,
id: randomId,
});
const actions$ = getOrderedMarbleStream(addAction);
// act
entity.add(userWithoutId);
// assert
expect(store.actions$).toBeObservable(actions$);
});
});
describe('#addList', () => {
test('should add entities', () => {
// arrange
const users = [user, user2];
const addListAction = entity.actionCreators.ADD_LIST(users);
const actions$ = getOrderedMarbleStream(addListAction);
// act
entity.addList(users);
// assert
expect(store.actions$).toBeObservable(actions$);
});
});
describe('#change', () => {
test('should change entity', () => {
// arrange
const patch = { name: 'Frank' };
const changeAction = entity.actionCreators.CHANGE(user.id, patch);
const actions$ = getOrderedMarbleStream(changeAction);
// act
entity.change(user.id, patch);
// assert
expect(store.actions$).toBeObservable(actions$);
});
});
describe('#changeRemote', () => {
const patch = { name: 'Frank' };
const responsePatch = { name: 'Alice' };
const changeId = 'vn3n5k';
mocked(uuid.v4).mockReturnValue(changeId);
test.each`
useResponsePatch
${false}
${true}
`('should change entity remotely with useResponsePatch: $useResponsePatch', ({ useResponsePatch }) => {
// arrange
const options: ChangeOptions = { useResponsePatch, rethrow: false };
const id = user.id;
const remote$ = cold(' --a| ', { a: responsePatch });
const resolvePatch = useResponsePatch ? responsePatch : patch;
const changeAction = entity.actionCreators.CHANGE(id, resolvePatch, changeId, options);
const actions$ = cold('a-(bc)', {
a: setLoadingStateActionFactory({ isLoading: true }, id, options),
b: changeAction,
c: setLoadingStateActionFactory({ isLoading: false }, id, options),
});
// act
entity.changeRemote$(id, patch, remote$, options).subscribe();
// assert
expect(store.actions$).toBeObservable(actions$);
});
describe('#optimistic', () => {
test.each`
useResponsePatch
${false}
${true}
`('should change entity remotely with useResponsePatch: $useResponsePatch', ({ useResponsePatch }) => {
const options: ChangeOptions = { optimistic: true, useResponsePatch, rethrow: false };
const remote$ = cold(' --a| ', { a: responsePatch });
const id = user.id;
const changeAction = entity.actionCreators.CHANGE(id, patch, changeId, options);
const resolvePatch = useResponsePatch ? responsePatch : undefined;
const resolveChangeAction = entity.actionCreators.RESOLVE_CHANGE(
id,
changeId,
true,
resolvePatch,
options,
);
const actions$ = cold('a-b', {
a: changeAction,
b: resolveChangeAction,
});
// act
entity.changeRemote$(id, patch, remote$, options).subscribe();
// assert
expect(store.actions$).toBeObservable(actions$);
});
test('should resolve unsuccessfully if response failed', () => {
const options: ChangeOptions = { optimistic: true, rethrow: false };
const error = 'Error';
const failedRemote$ = cold(' --#|', undefined, error);
const id = user.id;
const changeAction = entity.actionCreators.CHANGE(id, patch, changeId, options);
const resolveChangeAction = entity.actionCreators.RESOLVE_CHANGE(
id,
changeId,
false,
undefined,
options,
);
const actions$ = cold('a-(bc)', {
a: changeAction,
b: resolveChangeAction,
c: setLoadingStateActionFactory({ isLoading: false, error }, id, options),
});
// act
entity.changeRemote$(id, patch, failedRemote$, options).subscribe();
// assert
expect(store.actions$).toBeObservable(actions$);
});
});
});
describe('#remove', () => {
test('should remove entity', () => {
// arrange
const removeAction = entity.actionCreators.REMOVE(user.id);
const actions$ = getOrderedMarbleStream(removeAction);
// act
entity.remove(user.id);
// assert
expect(store.actions$).toBeObservable(actions$);
});
test('should remote remove entity', () => {
// arrange
const options = { rethrow: false };
const removeAction = entity.actionCreators.REMOVE(user.id, options);
const remote$ = cold(' --a| ', { a: null });
const actions$ = cold('a-(bc)', {
a: setLoadingStateActionFactory({ isLoading: true }, user.id, options),
b: setLoadingStateActionFactory({ isLoading: false }, user.id, options),
c: removeAction,
});
// act
entity.removeRemote$(user.id, remote$, options).subscribe();
// assert
expect(store.actions$).toBeObservable(actions$);
});
test('should optimistic remove entity', () => {
// arrange
const options = { optimistic: true, rethrow: false };
const removeAction = entity.actionCreators.REMOVE(user.id, options);
const resolveRemoveAction = entity.actionCreators.RESOLVE_REMOVE(user.id, true, options);
const remote$ = cold(' --a| ', { a: null });
const actions$ = cold('a-b', {
a: removeAction,
b: resolveRemoveAction,
});
// act
entity.removeRemote$(user.id, remote$, options).subscribe();
// assert
expect(store.actions$).toBeObservable(actions$);
});
test('should optimistic undo if failed', () => {
// arrange
const options = { optimistic: true, rethrow: false };
const error = 'Failed';
const id = user.id;
const removeAction = entity.actionCreators.REMOVE(id, options);
const resolveRemoveAction = entity.actionCreators.RESOLVE_REMOVE(id, false, options);
const remote$ = cold(' --#| ', undefined, error);
const actions$ = cold('a-(bc)', {
a: removeAction,
b: resolveRemoveAction,
c: setLoadingStateActionFactory({ isLoading: false, error }, id, options),
});
// act
entity.removeRemote$(id, remote$, options).subscribe();
// assert
expect(store.actions$).toBeObservable(actions$);
});
});
describe('#removeList', () => {
test('should remove entities', () => {
// arrange
const ids = [user.id, user2.id];
const removeAction = entity.actionCreators.REMOVE_LIST(ids);
const actions$ = getOrderedMarbleStream(removeAction);
// act
entity.removeList(ids);
// assert
expect(store.actions$).toBeObservable(actions$);
});
test('should remote remove entities', () => {
// arrange
const options = { rethrow: false };
const ids = [user.id, user2.id];
const removeAction = entity.actionCreators.REMOVE_LIST(ids, options);
const remote$ = cold(' --a| ', { a: null });
const actions$ = cold('a-(bc)', {
a: entity.actionCreators.SET_LOADING_STATES({ isLoading: true }, ids),
b: entity.actionCreators.SET_LOADING_STATES({ isLoading: false }, ids),
c: removeAction,
});
// act
entity.removeListRemote$(ids, remote$, options).subscribe();
// assert
expect(store.actions$).toBeObservable(actions$);
});
test('should optimistic remove entities', () => {
// arrange
const ids = [user.id, user2.id];
const options = { optimistic: true, rethrow: false };
const removeAction = entity.actionCreators.REMOVE_LIST(ids, options);
const resolveRemoveAction = entity.actionCreators.RESOLVE_REMOVE_LIST(ids, true, options);
const remote$ = cold(' --a| ', { a: null });
const actions$ = cold('a-b', {
a: removeAction,
b: resolveRemoveAction,
});
// act
entity.removeListRemote$(ids, remote$, options).subscribe();
// assert
expect(store.actions$).toBeObservable(actions$);
});
test('should optimistic undo if failed', () => {
// arrange
const ids = [user.id, user2.id];
const options = { optimistic: true, rethrow: false };
const error = 'Failed';
const removeAction = entity.actionCreators.REMOVE_LIST(ids, options);
const resolveRemoveAction = entity.actionCreators.RESOLVE_REMOVE_LIST(ids, false, options);
const remote$ = cold(' --#| ', undefined, error);
const actions$ = cold('a-(bc)', {
a: removeAction,
b: resolveRemoveAction,
c: entity.actionCreators.SET_LOADING_STATES({ isLoading: false, error }, ids),
});
// act
entity.removeListRemote$(ids, remote$, options).subscribe();
// assert
expect(store.actions$).toBeObservable(actions$);
});
});
describe('#create', () => {
test('should create entity', () => {
// arrange
const createAction = entity.actionCreators.ADD(user);
const actions$ = cold('a', { a: createAction });
// act
entity.add(user);
// assert
expect(store.actions$).toBeObservable(actions$);
});
test('should remote create entity', () => {
// arrange
const options = { rethrow: false };
const remote$ = cold('--a|', { a: user });
const createAction = entity.actionCreators.ADD(user, undefined, undefined, options);
const actions$ = cold('a-(bc)', {
a: setLoadingStateActionFactory({ isLoading: true }, undefined, options),
b: createAction,
c: setLoadingStateActionFactory({ isLoading: false }, undefined, options),
});
// act
const actual = entity.addRemote$(user, undefined, remote$, options);
// assert
expect(actual).toBeObservable(remote$);
expect(store.actions$).toBeObservable(actions$);
});
});
describe('#getById$', () => {
test('should return observable to entity', () => {
// arrange
const storeObs = cold('a', { a: undefined });
// act
const actual = entity.getById$(1);
// assert
expect(actual).toBeObservable(storeObs);
});
test('should load entity', () => {
// arrange
const remote$ = cold('--a|', { a: user });
const addAction = entity.actionCreators.ADD(user);
const actions$ = cold('a-(bc)', {
a: setLoadingStateActionFactory({ isLoading: true }, user.id),
b: addAction,
c: setLoadingStateActionFactory({ isLoading: false }, user.id),
});
// act
entity.getById$(user.id, remote$).subscribe();
// assert
expect(store.actions$).toBeObservable(actions$);
});
test('should load entity with tuple id', () => {
// arrange
const data = { objectId: '23a4123', type: 5, name: 'Some object 1' };
const remote$ = cold('--a|', { a: data });
const id = [data.objectId, data.type].toString();
const addAction = entity.actionCreators.ADD({ id, ...data });
const actions$ = cold('a-(bc)', {
a: setLoadingStateActionFactory({ isLoading: true }, id),
b: addAction,
c: setLoadingStateActionFactory({ isLoading: false }, id),
});
// act
entity.getById$(id, remote$).subscribe();
// assert
expect(store.actions$).toBeObservable(actions$);
});
});
describe('#get$', () => {
test('should return observable to entity', () => {
// arrange
const storeObs = cold('a', { a: undefined });
// act
const actual = entity.get$(null);
// assert
expect(actual).toBeObservable(storeObs);
});
test('should load entities', () => {
// arrange
const users = [user];
const remote$ = cold('--a|', { a: { data: users } });
const filter = null;
const addAction = entity.actionCreators.ADD_LIST(users, filter, true, false);
const actions$ = cold('a-(bc)', {
a: setLoadingStateActionFactory({ isLoading: true }),
b: addAction,
c: setLoadingStateActionFactory({ isLoading: false }),
});
// act
entity.get$(filter, remote$).subscribe();
// assert
expect(store.actions$).toBeObservable(actions$);
});
xtest('should log if observable source throws error', () => {
// arrange
const error = new Error('Some error');
const remote$ = cold('#|', null, error);
global.console.error = jest.fn();
// act
entity.get$(null, remote$).subscribe();
// assert
expect(global.console.error).toBeCalled();
});
test('should set error if observable source throws error', () => {
// arrange
const error = 'Some error';
const remote$ = cold('--#|', null, error);
const actions$ = cold('a-b', {
a: setLoadingStateActionFactory({ isLoading: true }),
b: setLoadingStateActionFactory({ isLoading: false, error }),
});
// act
entity.get$(null, remote$).subscribe();
// assert
expect(store.actions$).toBeObservable(actions$);
});
test('should page entities', () => {
// arrange
const remoteMarble = ' --a| --a| --a| --a| ';
const paginationMarble = '-------a------b------b-----';
const actionsMarble = ' a-(bc)-a-(ec)-a-(bc)-a-(bc)';
const filter = null;
const options = undefined;
const users1 = [user, user2];
const users2 = [user3];
const remote$ = (value: Pagination) => {
const users = value.index ? users2 : users1;
return cold('--a|', { a: { data: users } });
};
const addAction = entity.actionCreators.ADD_LIST(users1, filter, true, true);
const addAction2 = entity.actionCreators.ADD_LIST(users2, filter);
const paginator = new BehaviorSubject<boolean>(true);
const setLoadingStateTrueAction = setLoadingStateActionFactory({ isLoading: true });
const setLoadingStateFalseAction = setLoadingStateActionFactory({ isLoading: false });
cold(paginationMarble, {
a: true,
b: false,
}).subscribe((value) => {
paginator.next(value);
});
const actions$ = cold(actionsMarble, {
a: setLoadingStateTrueAction,
b: addAction,
c: setLoadingStateFalseAction,
e: addAction2,
});
// act
entity.get$(filter, remote$, options, paginator).subscribe();
// assert
expect(store.actions$).toBeObservable(actions$);
});
});
describe('#getDictionary$', () => {
test('should return page as dictionary observable', () => {
// arrange
const storeObs = cold('a', { a: {} });
// act
const actual = entity.getDictionary$(undefined);
// assert
expect(actual).toBeObservable(storeObs);
});
});
});
================================================
FILE: src/entity/entity.ts
================================================
import { Entity, EntityActionTypes, EntitySchema, EntityState } from './types';
import { createEntitySelectors } from './selectors';
import { PainlessRedux, SlotTypes } from '../painless-redux/types';
import { createEntityActionTypes, createIdResolver, getFullEntitySchema } from './utils';
import { createEntityReducer } from './reducer';
import { EntityActions } from './actions';
import { createDispatchEntityMethods } from './methods/dispatch/dispatch';
import { createEntityActionCreators } from './action-creators';
import { createSelectEntityMethods } from './methods/select/select';
import { createMixedEntityMethods } from './methods/mixed/mixed';
export const createEntity = <T, TPageMetadata = void>(
pr: PainlessRedux,
schema?: Partial<EntitySchema<T>>,
): Entity<T, TPageMetadata> => {
const fullSchema = getFullEntitySchema<T>(schema);
const actionTypes = createEntityActionTypes(fullSchema.name);
const actionCreators = createEntityActionCreators<T, TPageMetadata>(actionTypes, fullSchema);
const reducer = createEntityReducer<T, TPageMetadata>(actionTypes, fullSchema);
const {
selector,
dispatcher,
selectManager,
} = pr.registerSlot<EntityState<T, TPageMetadata>, EntityActionTypes, EntityActions>(
SlotTypes.Entity,
fullSchema.name,
reducer,
actionCreators,
);
const selectors = createEntitySelectors<T, TPageMetadata>(selector, fullSchema.hashFn);
const idResolver = createIdResolver<T>(fullSchema);
const selectMethods = createSelectEntityMethods<T, TPageMetadata>(selectManager, selectors);
const dispatchMethods = createDispatchEntityMethods<T, TPageMetadata>(dispatcher, idResolver, selectMethods, fullSchema);
const mixedMethods = createMixedEntityMethods<T, TPageMetadata>(dispatchMethods, selectMethods, fullSchema, pr.schema);
const { changeWithId, resolveChange, ...publicDispatchMethods } = dispatchMethods;
const { get$, getDictionary$, getById$, ...publicSelectMethods } = selectMethods;
return {
...publicDispatchMethods,
...mixedMethods,
...publicSelectMethods,
actionCreators,
};
};
================================================
FILE: src/entity/methods/dispatch/dispatch.ts
================================================
import { Dispatcher } from '../../../dispatcher/types';
import {
EntityActionTypes,
EntityAddListOptions,
EntityAddOptions,
EntityInternalAddListOptions,
EntityInternalAddOptions,
EntityInternalSetLoadingStateOptions,
EntityRemoveListOptions,
EntityRemoveOptions,
EntitySchema,
EntitySetLoadingStateOptions,
EntityType,
IdPatch,
IdPatchRequest,
} from '../../types';
import { EntityActions } from '../../actions';
import { DeepPartial, Id, LoadingState } from '../../../system-types';
import { DispatchEntityMethods } from './types';
import { isNil } from 'lodash-es';
import { affectLoadingStateFactory } from '../../../affect-loading-state/affect-loading-state';
import { ChangeOptions, PatchRequest } from '../../../shared/change/types';
import { normalizePatch } from '../../../shared/change/utils';
import { SelectEntityMethods } from '../select/types';
import { AffectLoadingStateFactory } from '../../..';
export const createDispatchEntityMethods = <T, TPageMetadata>(
dispatcher: Dispatcher<EntityActionTypes, EntityActions>,
idResolver: (data: T) => EntityType<T>,
selectMethods: SelectEntityMethods<T, TPageMetadata>,
schema: EntitySchema<T>,
): DispatchEntityMethods<T, TPageMetadata> => {
const add = (
entity: T,
config?: unknown,
options?: EntityAddOptions,
) => {
entity = idResolver(entity);
const internalOptions: EntityInternalAddOptions = {
...options,
maxPagesCount: schema.maxPagesCount,
};
return dispatcher.createAndDispatch('ADD', [entity, config, undefined], internalOptions);
};
const addWithId = (
entity: T,
tempId: string,
config?: unknown,
options?: EntityAddOptions,
) => {
const internalOptions: EntityInternalAddOptions = {
...options,
maxPagesCount: schema.maxPagesCount,
};
return dispatcher.createAndDispatch('ADD', [entity, config, tempId], internalOptions);
};
const resolveAdd = (
result: T,
success: boolean,
tempId: string,
config?: unknown,
options?: EntityAddOptions,
) => {
return dispatcher.createAndDispatch('RESOLVE_ADD', [result, success, tempId, config], options);
};
const addList = (
entities: T[],
config?: unknown,
isReplace: boolean = false,
hasMore: boolean = false,
metadata?: TPageMetadata,
options?: EntityAddListOptions,
) => {
const internalOptions: EntityInternalAddListOptions = {
...options,
maxPagesCount: schema.maxPagesCount,
};
entities = entities.map((entity) => idResolver(entity));
return dispatcher.createAndDispatch('ADD_LIST', [entities, config, isReplace, hasMore, metadata], internalOptions);
};
const changeWithId = (
id: Id,
patch: PatchRequest<T>,
changeId: string | undefined,
options?: ChangeOptions,
) => {
const normalizedPatch = normalizePatch(patch, selectMethods.getById$(id));
return dispatcher.createAndDispatch('CHANGE', [id, normalizedPatch, changeId], options);
};
const change = (
id: Id,
patch: PatchRequest<T>,
options?: ChangeOptions,
) => {
return changeWithId(id, patch, undefined, options);
};
const changeListWithId = (
patches: IdPatchRequest<T>[],
changeId: string | undefined,
options?: ChangeOptions,
) => {
const normalizedPatches: IdPatch<T>[] = patches.map((patch) => ({
...patch,
patch: normalizePatch(patch.patch, selectMethods.getById$(patch.id)),
}));
return dispatcher.createAndDispatch('CHANGE_LIST', [normalizedPatches, changeId], options);
};
const changeList = (
patches: IdPatchRequest<T>[],
options?: ChangeOptions,
) => {
return changeListWithId(patches, undefined, options);
};
const resolveChange = (
id: Id,
changeId: Id,
success: boolean,
remotePatch: DeepPartial<T>,
options?: ChangeOptions,
) => {
return dispatcher.createAndDispatch('RESOLVE_CHANGE', [id, changeId, success, remotePatch], options);
};
const resolveChangeList = (
patches: IdPatch<T>[],
changeId: string,
success: boolean,
options?: ChangeOptions,
) => {
return dispatcher.createAndDispatch('RESOLVE_CHANGE_LIST', [patches, changeId, success], options);
};
const remove = (
id: Id,
options?: EntityRemoveOptions,
) => {
return dispatcher.createAndDispatch('REMOVE', [id], options);
};
const resolveRemove = (
id: Id,
success: boolean,
options?: EntityRemoveOptions,
) => {
return dispatcher.createAndDispatch('RESOLVE_REMOVE', [id, success], options);
};
const restoreRemoved = (
id: Id,
) => {
return dispatcher.createAndDispatch('RESTORE_REMOVED', [id]);
};
const removeList = (
ids: Id[],
options?: EntityRemoveListOptions,
) => {
return dispatcher.createAndDispatch('REMOVE_LIST', [ids], options);
};
const resolveRemoveList = (
ids: Id[],
success: boolean,
options?: EntityRemoveListOptions,
) => {
return dispatcher.createAndDispatch('RESOLVE_REMOVE_LIST', [ids, success], options);
};
const restoreRemovedList = (
ids: Id[],
) => {
return dispatcher.createAndDispatch('RESTORE_REMOVED_LIST', [ids]);
};
const setLoadingState = (
state: LoadingState,
config?: unknown,
options?: EntitySetLoadingStateOptions,
) => {
const internalOptions: EntityInternalSetLoadingStateOptions = {
...options,
maxPagesCount: schema.maxPagesCount,
};
return dispatcher.createAndDispatch('SET_LOADING_STATE', [state, config, undefined, undefined], internalOptions);
};
const setLoadingStateById = (
id: Id,
state: LoadingState,
options?: EntitySetLoadingStateOptions,
) => {
const internalOptions: EntityInternalSetLoadingStateOptions = {
...options,
maxPagesCount: schema.maxPagesCount,
};
return dispatcher.createAndDispatch('SET_LOADING_STATE', [state, undefined, id, undefined], internalOptions);
};
const setLoadingStateForKey = (
id: Id,
key: string,
state: LoadingState,
options?: EntitySetLoadingStateOptions,
) => {
const internalOptions: EntityInternalSetLoadingStateOptions = {
...options,
maxPagesCount: schema.maxPagesCount,
};
return dispatcher.createAndDispatch('SET_LOADING_STATE', [state, undefined, id, key], internalOptions);
};
const setLoadingStateByIds = (
ids: Id[],
state: LoadingState,
options?: EntitySetLoadingStateOptions,
) => {
const internalOptions: EntityInternalSetLoadingStateOptions = {
...options,
maxPagesCount: schema.maxPagesCount,
};
return dispatcher.createAndDispatch('SET_LOADING_STATES', [state, ids], internalOptions);
};
const clear = (config: unknown) => {
return dispatcher.createAndDispatch('CLEAR', [config]);
};
const clearAll = () => {
return dispatcher.createAndDispatch('CLEAR_ALL', []);
};
const setLoadingStateBus = (
state: LoadingState,
id?: Id,
config?: unknown,
key?: string,
options?: EntitySetLoadingStateOptions,
) => {
if (!isNil(id)) {
if (!isNil(key)) {
return setLoadingStateForKey(id, key, state, options);
} else {
return setLoadingStateById(id, state, options);
}
} else {
if (state.error) {
console.error(state.error);
}
return setLoadingState(state, config, options);
}
};
const affectLoadingState = (
config?: unknown,
key?: string,
rethrow?: boolean,
) => {
const setter = (state: LoadingState) => {
setLoadingStateBus(state, undefined, config, key);
};
return affectLoadingStateFactory(setter, rethrow);
};
const affectLoadingStateById = (
id?: Id,
key?: string,
rethrow?: boolean,
) => {
const setter = (state: LoadingState) => {
setLoadingStateBus(state, id, undefined, key);
};
return affectLoadingStateFactory(setter, rethrow);
};
const affectLoadingStateByConfigOrId = (
config?: unknown,
id?: Id,
key?: string,
rethrow?: boolean,
): AffectLoadingStateFactory => {
const setter = (state: LoadingState) => {
setLoadingStateBus(state, id, config, key);
};
return affectLoadingStateFactory(setter, rethrow);
};
const batch = (
actions: EntityActions[],
) => {
return dispatcher.createAndDispatch('BATCH', [actions]);
};
return {
add,
addWithId,
addList,
change,
changeList,
changeListWithId,
changeWithId,
resolveChange,
resolveChangeList,
resolveAdd,
remove,
resolveRemove,
restoreRemoved,
removeList,
resolveRemoveList,
restoreRemovedList,
setLoadingState,
clear,
clearAll,
setLoadingStateBus,
setLoadingStateById,
setLoadingStateForKey,
affectLoadingState,
affectLoadingStateById,
affectLoadingStateByConfigOrId,
batch,
setLoadingStateByIds,
};
};
================================================
FILE: src/entity/methods/dispatch/types.ts
================================================
import {
EntityAddListOptions,
EntityAddOptions,
EntityRemoveListOptions,
EntityRemoveOptions,
EntitySetLoadingStateOptions,
IdPatch,
IdPatchRequest,
} from '../../types';
import { DeepPartial, Id, LoadingState } from '../../../system-types';
import { EntityActions } from '../../actions';
import { ChangeOptions, PatchRequest } from '../../../shared/change/types';
import { AffectLoadingStateFactory } from '../../..';
export interface DispatchEntityMethods<T, TPageMetadata> {
add(
data: T,
config?: unknown,
options?: EntityAddOptions,
): EntityActions;
addWithId(
data: T,
tempId: string,
config?: unknown,
options?: EntityAddOptions,
): EntityActions;
resolveAdd(
data: T | undefined,
success: boolean,
tempId: string,
config?: unknown,
options?: EntityAddOptions,
): EntityActions;
addList(
data: T[],
config?: unknown,
isReplace?: boolean,
hasMore?: boolean,
metadata?: TPageMetadata,
options?: EntityAddListOptions,
): EntityActions;
change(
id: Id,
patch: PatchRequest<T>,
options?: ChangeOptions,
): EntityActions;
changeList(
patches: IdPatchRequest<T>[],
options?: ChangeOptions,
): EntityActions;
changeListWithId(
patches: IdPatchRequest<T>[],
changeId: string,
options?: ChangeOptions,
): EntityActions;
changeWithId(
id: Id,
patch: PatchRequest<T>,
changeId: string,
options?: ChangeOptions,
): EntityActions;
resolveChange(
id: Id,
changeId: string,
success: boolean,
remotePatch?: DeepPartial<T>,
options?: ChangeOptions,
): EntityActions;
resolveChangeList(
patches: IdPatch<T>[],
changeId: string,
success: boolean,
options?: ChangeOptions,
): EntityActions;
remove(
id: Id,
options?: EntityRemoveOptions,
): EntityActions;
resolveRemove(
id: Id,
success: boolean,
options?: EntityRemoveOptions,
): EntityActions;
restoreRemoved(
id: Id,
): EntityActions;
removeList(
ids: Id[],
options?: EntityRemoveListOptions,
): EntityActions;
resolveRemoveList(
ids: Id[],
success: boolean,
options?: EntityRemoveListOptions,
): EntityActions;
restoreRemovedList(
ids: Id[],
): EntityActions;
setLoadingState(
state: LoadingState,
config?: unknown,
options?: EntitySetLoadingStateOptions,
): EntityActions;
clear(config: unknown): EntityActions;
clearAll(): EntityActions;
setLoadingStateById(
id: Id,
state: LoadingState,
options?: EntitySetLoadingStateOptions,
): EntityActions;
setLoadingStateByIds(
ids: Id[],
state: LoadingState,
options?: EntitySetLoadingStateOptions,
): EntityActions;
setLoadingStateForKey(
id: Id,
key: string,
state: LoadingState,
options?: EntitySetLoadingStateOptions,
): EntityActions;
setLoadingStateBus(
state: LoadingState,
id?: Id,
config?: unknown,
key?: string,
options?: EntitySetLoadingStateOptions,
): EntityActions;
batch(
actions: EntityActions[],
): EntityActions;
affectLoadingState(
config?: unknown,
key?: string,
rethrow?: boolean,
): AffectLoadingStateFactory;
affectLoadingStateById(
id?: Id,
key?: string,
rethrow?: boolean,
): AffectLoadingStateFactory;
affectLoadingStateByConfigOrId(
config?: unknown,
id?: Id,
key?: string,
rethrow?: boolean,
): AffectLoadingStateFactory;
}
================================================
FILE: src/entity/methods/mixed/mixed.ts
================================================
import {
EntityAddOptions,
EntityGetListOptions,
EntityGetOptions,
EntityLoadListOptions,
EntityLoadOptions,
EntityRemoveListOptions,
EntityRemoveOptions,
EntitySchema,
IdPatch,
IdPatchRequest,
PaginatedResponse,
Pagination,
Response$Factory,
ResponseArray,
} from '../../types';
import { BehaviorSubject, EMPTY, merge, Observable, of } from 'rxjs';
import { DispatchEntityMethods } from '../dispatch/types';
import { SelectEntityMethods } from '../select/types';
import { getPaginated$ } from '../../utils';
import { DeepPartial, Dictionary, Id, LoadingState } from '../../../system-types';
import { createMixedEntityMethodsUtils } from './utils';
import { MixedEntityMethods } from './types';
import { v4 } from 'uuid';
import { PainlessReduxSchema } from '../../../painless-redux/types';
import { ChangeOptions, PatchRequest } from '../../../shared/change/types';
import { getPatchByOptions, getResolvePatchByOptions, normalizePatch } from '../../../shared/change/utils';
import { getRemotePipe, guardIfLoading } from '../../../shared/utils';
import { switchMap } from 'rxjs/operators';
import { typedDefaultsDeep } from '../../../utils';
export const createMixedEntityMethods = <T, TPageMetadata>(
dispatchMethods: DispatchEntityMethods<T, TPageMetadata>,
selectMethods: SelectEntityMethods<T, TPageMetadata>,
schema: EntitySchema<T>,
prSchema: PainlessReduxSchema,
): MixedEntityMethods<T, TPageMetadata> => {
const {
getPaginator,
tryInvoke,
} = createMixedEntityMethodsUtils<T, TPageMetadata>(dispatchMethods, selectMethods, schema, prSchema);
const loadList$ = (
config: unknown,
dataSource: Observable<ResponseArray<T, TPageMetadata>> | Response$Factory<T, TPageMetadata>,
options?: EntityLoadListOptions,
paginatorSubj?: BehaviorSubject<boolean>,
): Observable<PaginatedResponse<T, TPageMetadata>> => {
const store$ = selectMethods.get$(config);
const sourcePipe = getRemotePipe<Pagination, T[] | undefined, PaginatedResponse<T, TPageMetadata>, PaginatedResponse<T, TPageMetadata>>({
options,
store$,
emitOnSuccess: true,
remoteObsOrFactory: (pagination: Pagination) => getPaginated$(dataSource, pagination),
success: (result?: PaginatedResponse<T, TPageMetadata>) => {
if (!result) return;
const { index, size, response } = result;
const data = response.data ?? [];
const isReplace = index === 0;
const hasMore = response.hasMore ?? data.length >= size;
const metadata = response.metadata;
return dispatchMethods.addList(data, config, isReplace, hasMore, metadata, options);
},
setLoadingState: (state) => dispatchMethods.setLoadingStateBus(state, undefined, config, undefined, options),
},
);
const paginator = getPaginator(config, paginatorSubj, options);
return paginator.pipe(sourcePipe);
};
const loadById$ = (
id: Id,
dataSource$: Observable<T>,
options?: EntityLoadOptions,
): Observable<T> => {
const store$ = selectMethods.getById$(id);
const sourcePipe = getRemotePipe<LoadingState | undefined, T | undefined, T, T>({
options,
store$,
emitOnSuccess: true,
remoteObsOrFactory: dataSource$,
success: (response) => {
if (!response) return;
const entity = { ...response, id };
return dispatchMethods.add(entity, undefined, options);
},
setLoadingState: (state) => dispatchMethods.setLoadingStateBus(state, id, undefined, undefined, options),
});
const loadingState$ = selectMethods.getLoadingStateById$(id, prSchema.useAsapSchedulerInLoadingGuards);
return guardIfLoading(loadingState$).pipe(sourcePipe);
};
const tryInvokeList$ = <S>(
store$: Observable<S>,
config: unknown,
dataSource?: Observable<ResponseArray<T, TPageMetadata>> | Response$Factory<T, TPageMetadata>,
options?: EntityGetListOptions,
paginatorSubj?: BehaviorSubject<boolean>,
) => {
const invoker = (ds: Observable<ResponseArray<T, TPageMetadata>> | Response$Factory<T, TPageMetadata>) => loadList$(
config,
ds,
options,
paginatorSubj,
);
return tryInvoke(store$, invoker, dataSource);
};
const get$ = (
config: unknown,
dataSource?: Observable<ResponseArray<T, TPageMetadata>> | Response$Factory<T, TPageMetadata>,
options?: EntityGetListOptions,
paginatorSubj?: BehaviorSubject<boolean>,
): Observable<T[] | undefined> => {
const store$ = selectMethods.get$(config);
return tryInvokeList$(
store$,
config,
dataSource,
options,
paginatorSubj,
);
};
const getDictionary$ = (
config: unknown,
dataSource?: Observable<ResponseArray<T, TPageMetadata>> | Response$Factory<T, TPageMetadata>,
options?: EntityGetListOptions,
paginatorSubj?: BehaviorSubject<boolean>,
): Observable<Dictionary<T>> => {
const store$ = selectMethods.getDictionary$(config);
return tryInvokeList$(
store$,
config,
dataSource,
options,
paginatorSubj,
);
};
const getById$ = (
id: Id,
dataSource?: Observable<T>,
options?: EntityGetOptions,
): Observable<T | undefined> => {
const store$ = selectMethods.getById$(id);
if (dataSource) {
const remote$ = loadById$(id, dataSource, options).pipe(switchMap(() => EMPTY));
return merge(store$, remote$);
}
return store$;
};
const addRemote$ = (
entity: T,
config: unknown,
dataSource$: Observable<T>,
options?: EntityAddOptions,
): Observable<T> => {
const tempId = v4();
options = typedDefaultsDeep(options, { rethrow: true });
const { addWithId, resolveAdd } = dispatchMethods;
const sourcePipe = getRemotePipe<null, unknown, T, T>({
options,
remoteObsOrFactory: dataSource$,
success: (result) => {
const newEntity = options?.optimistic ? entity : result;
if (!newEntity) return;
return addWithId(newEntity, tempId, config, options);
},
emitOnSuccess: true,
optimistic: options.optimistic,
optimisticResolve: (success, result) => resolveAdd(result, success, tempId, config, options),
setLoadingState: (state) => dispatchMethods.setLoadingStateBus(state, undefined, config, undefined, options),
});
return of(null).pipe(sourcePipe);
};
const changeRemote$ = (
id: Id,
patch: PatchRequest<T>,
dataSource$: Observable<DeepPartial<T> | undefined>,
options?: ChangeOptions,
): Observable<DeepPartial<T> | undefined> => {
options = typedDefaultsDeep(options, { rethrow: true });
const changeId = v4();
const { changeWithId, resolveChange, setLoadingStateBus } = dispatchMethods;
const { getLoadingStateById$, getById$ } = selectMethods;
const normalizedPatch = normalizePatch(patch, getById$(id));
const sourcePipe = getRemotePipe<LoadingState | undefined, unknown, DeepPartial<T> | undefined, DeepPartial<T> | undefined>({
options,
remoteObsOrFactory: dataSource$,
success: (
response?: DeepPartial<T>,
) => {
const patchToApply = getPatchByOptions(normalizedPatch, response, options) ?? {};
return changeWithId(id, patchToApply, changeId, options);
},
emitOnSuccess: true,
optimistic: options.optimistic,
optimisticResolve: (
success: boolean,
response?: DeepPartial<T>,
) => {
const patchToApply = getResolvePatchByOptions(normalizedPatch, response, options);
return resolveChange(id, changeId, success, patchToApply, options);
},
setLoadingState: (state) => setLoadingStateBus(state, id, undefined, undefined, options),
});
const loadingState$ = getLoadingStateById$(id, prSchema.useAsapSchedulerInLoadingGuards);
return guardIfLoading(loadingState$).pipe(sourcePipe);
};
const changeListRemote$ = (
patches: IdPatchRequest<T>[],
dataSource$: Observable<IdPatch<T>[] | undefined>,
options?: ChangeOptions,
): Observable<IdPatch<T>[] | undefined> => {
options = typedDefaultsDeep(options, { rethrow: true });
const changeId = v4();
const { changeListWithId, resolveChangeList, setLoadingStateByIds } = dispatchMethods;
const { getLoadingStateByIds$, getById$ } = selectMethods;
const normalizedPatches: IdPatch<T>[] = patches.map((patch) => ({
...patch,
patch: normalizePatch(patch.patch, getById$(patch.id)),
}));
const ids = normalizedPatches.map((patch) => patch.id);
const sourcePipe = getRemotePipe<LoadingState | undefined, unknown, IdPatch<T>[] | undefined, IdPatch<T>[]>({
options,
remoteObsOrFactory: dataSource$,
success: (
response?: IdPatch<T>[] | undefined,
) => {
const patchesToApply = normalizedPatches.map((idPatch) => {
const responsePatch = response?.find((r) => r.id === idPatch.id);
const patch = getPatchByOptions(idPatch.patch, responsePatch?.patch, options) ?? {};
return { ...idPatch, patch };
});
return changeListWithId(patchesToApply, changeId, options);
},
emitOnSuccess: true,
optimistic: options?.optimistic,
optimisticResolve: (
success: boolean,
response?: IdPatch<T>[] | undefined,
) => {
const patchesToApply = normalizedPatches.map((idPatch) => {
const responsePatch = response?.find((r) => r.id === idPatch.id);
const patch = getResolvePatchByOptions(idPatch.patch, responsePatch?.patch, options) ?? {};
return { ...idPatch, patch };
});
return resolveChangeList(patchesToApply, changeId, success, options);
},
setLoadingState: (state) => setLoadingStateByIds(ids, state, options),
});
const loadingState$ = getLoadingStateByIds$(ids, prSchema.useAsapSchedulerInLoadingGuards);
return guardIfLoading(loadingState$).pipe(sourcePipe);
};
const removeRemote$ = <R>(
id: Id,
observable: Observable<R>,
options?: EntityRemoveOptions,
): Observable<R> => {
options = typedDefaultsDeep(options, { rethrow: true });
const { remove, resolveRemove } = dispatchMethods;
const sourcePipe = getRemotePipe<LoadingState | undefined, unknown, R, R>({
options,
remoteObsOrFactory: observable,
success: () => remove(id, options),
emitSuccessOutsideAffectState: true,
emitOnSuccess: true,
optimistic: options?.optimistic,
optimisticResolve: (success: boolean) => resolveRemove(id, success, options),
setLoadingState: (state) => dispatchMethods.setLoadingStateBus(state, id, undefined, undefined, options),
});
const loadingState$ = selectMethods.getLoadingStateById$(id, prSchema.useAsapSchedulerInLoadingGuards);
return guardIfLoading(loadingState$).pipe(sourcePipe);
};
const removeListRemote$ = <R>(
ids: Id[],
observable: Observable<R>,
options?: EntityRemoveListOptions,
): Observable<R> => {
options = typedDefaultsDeep(options, { rethrow: true });
const { removeList, setLoadingStateByIds, resolveRemoveList } = dispatchMethods;
const sourcePipe = getRemotePipe<LoadingState | undefined, unknown, R, R>({
options,
remoteObsOrFactory: observable,
success: () => removeList(ids, options),
emitSuccessOutsideAffectState: true,
emitOnSuccess: true,
optimistic: options?.optimistic,
optimisticResolve: (success: boolean) => resolveRemoveList(ids, success, options),
setLoadingState: (state) => setLoadingStateByIds(ids, state, options),
});
const loadingState$ = selectMethods.getLoadingStateByIds$(ids, prSchema.useAsapSchedulerInLoadingGuards);
return guardIfLoading(loadingState$).pipe(sourcePipe);
};
return {
loadList$,
loadById$,
get$,
getDictionary$,
getById$,
addRemote$,
changeRemote$,
removeRemote$,
removeListRemote$,
changeListRemote$,
};
};
================================================
FILE: src/entity/methods/mixed/types.ts
================================================
import {
EntityAddOptions,
EntityGetListOptions,
EntityGetOptions,
EntityLoadListOptions,
EntityLoadOptions,
EntityRemoveListOptions,
EntityRemoveOptions,
IdPatch,
IdPatchRequest,
PaginatedResponse,
Response$Factory,
ResponseArray,
} from '../../types';
import { BehaviorSubject, Observable } from 'rxjs';
import { DeepPartial, Dictionary, Id } from '../../../system-types';
import { ChangeOptions, PatchRequest } from '../../../shared/change/types';
export interface MixedEntityMethods<T, TPageMetadata> {
loadList$(
config: unknown,
dataSource: Observable<ResponseArray<T, TPageMetadata>> | Response$Factory<T, TPageMetadata>,
options?: EntityLoadListOptions,
paginatorSubj?: BehaviorSubject<boolean>,
): Observable<PaginatedResponse<T, TPageMetadata>>;
loadById$(
id: Id,
dataSource$: Observable<T>,
options?: EntityLoadOptions,
): Observable<T>;
get$(
config: unknown,
dataSource?: Observable<ResponseArray<T, TPageMetadata>> | Response$Factory<T, TPageMetadata>,
options?: EntityGetListOptions,
paginatorSubj?: BehaviorSubject<boolean>,
): Observable<T[] | undefined>;
getDictionary$(
config: unknown,
dataSource?: Observable<ResponseArray<T, TPageMetadata>> | Response$Factory<T, TPageMetadata>,
options?: EntityGetListOptions,
paginatorSubj?: BehaviorSubject<boolean>,
): Observable<Dictionary<T>>;
getById$(
id: Id,
dataSource?: Observable<T>,
options?: EntityGetOptions,
): Observable<T | undefined>;
addRemote$(
entity: T,
config: unknown,
dataSource$: Observable<T>,
options?: EntityAddOptions,
): Observable<T>;
changeRemote$(
id: Id,
patch: PatchRequest<T>,
dataSource$: Observable<DeepPartial<T> | undefined>,
options?: ChangeOptions,
): Observable<DeepPartial<T> | undefined>;
changeListRemote$(
patches: IdPatchRequest<T>[],
dataSource$: Observable<IdPatch<T>[] | undefined>,
options?: ChangeOptions,
): Observable<IdPatch<T>[] | undefined>;
removeRemote$<R>(
id: Id,
observable: Observable<R>,
options?: EntityRemoveOptions,
): Observable<R>;
removeListRemote$<R>(
ids: Id[],
observable: Observable<R>,
options?: EntityRemoveListOptions,
): Observable<R>;
}
================================================
FILE: src/entity/methods/mixed/utils.ts
================================================
import { BehaviorSubject, EMPTY, merge, Observable, of } from 'rxjs';
import { EntityGetListOptions, EntitySchema, Page, Pagination } from '../../types';
import { map, scan, switchMap, take } from 'rxjs/operators';
import { DispatchEntityMethods } from '../dispatch/types';
import { SelectEntityMethods } from '../select/types';
import { PainlessReduxSchema } from '../../../painless-redux/types';
import { ObservableOrFactory } from '../../../shared/types';
import { guardIfLoading } from '../../../shared/utils';
export const createMixedEntityMethodsUtils = <T, TPageMetadata>(
dispatchMethods: DispatchEntityMethods<T, TPageMetadata>,
selectMethods: SelectEntityMethods<T, TPageMetadata>,
schema: EntitySchema<T>,
prSchema: PainlessReduxSchema,
) => {
const { getPage$, getPageLoadingState$ } = selectMethods;
const getPaginator = (
config: unknown,
paginatorSubj?: BehaviorSubject<boolean>,
options?: EntityGetListOptions,
): Observable<Pagination> => {
paginatorSubj = paginatorSubj ?? new BehaviorSubject<boolean>(false);
const page$ = getPage$(config);
const loadingState$ = getPageLoadingState$(config, prSchema.useAsapSchedulerInLoadingGuards);
return paginatorSubj.pipe(
switchMap((isNext) => guardIfLoading(loadingState$).pipe(map(() => isNext))),
scan((
prevIndex: number,
isNext: boolean,
) => isNext ? prevIndex + 1 : 0, -1),
map((index: number) => {
const size = options?.pageSize ?? schema.pageSize;
const from = index * size;
const to = from + size - 1;
return { index, size, from, to };
}),
switchMap((paging: Pagination) => page$.pipe(
take(1),
map((page: Page<TPageMetadata> | undefined) => !page || page.hasMore !== false),
switchMap((hasMore: boolean) => paging.index === 0 || hasMore ? of(paging) : EMPTY),
)),
);
};
const tryInvoke = <T, R, S>(
store$: Observable<S>,
invoker: (dataSource: ObservableOrFactory<T, R>) => Observable<unknown>,
dataSource?: ObservableOrFactory<T, R>,
) => {
if (dataSource) {
const result$ = invoker(dataSource).pipe(switchMap(() => EMPTY));
return merge(store$, result$);
}
return store$;
};
return { getPaginator, tryInvoke };
};
================================================
FILE: src/entity/methods/select/select.ts
================================================
import { EntitySelectors } from '../../types';
import { Observable } from 'rxjs';
import { SelectManager } from '../../../select-manager/types';
import { Dictionary, Id } from '../../../system-types';
import { SelectEntityMethods } from './types';
import { toDictionary } from '../../../utils';
export const createSelectEntityMethods = <T, TPageMetadata>(
selectManager: SelectManager,
selectors: EntitySelectors<T, TPageMetadata>,
): SelectEntityMethods<T, TPageMetadata> => {
const get$ = (config: unknown): Observable<T[] | undefined> => {
const selector = selectors.createPageListByConfig(config);
return selectManager.select$(selector);
};
const getAll$ = (): Observable<T[] | undefined> => {
const selector = selectors.all;
return selectManager.select$(selector);
};
const getDictionary$ = (config: unknown): Observable<Dictionary<T>> => {
return get$(config).pipe(toDictionary());
};
const getById$ = (id: Id): Observable<T | undefined> => {
const selector = selectors.createActual(id);
return selectManager.select$(selector);
};
const getPage$ = (
config: unknown,
isAsap: boolean = false,
) => {
const selector = selectors.createPage(config);
return selectManager.select$(selector, isAsap);
};
const getPages$ = (
isAsap: boolean = false,
) => {
const selector = selectors.allPages;
return selectManager.select$(selector, isAsap);
};
const getPageLoadingState$ = (
config: unknown,
isAsap: boolean = false,
) => {
const selector = selectors.createPageLoadingState(config);
return selectManager.select$(selector, isAsap);
};
const getLoadingStateById$ = (
id: Id,
isAsap: boolean = false,
) => {
const selector = selectors.createLoadingStateById(id);
return selectManager.select$(selector, isAsap);
};
const getLoadingState$ = () => {
const selector = selectors.loadingState;
return selectManager.select$(selector);
};
const getLoadingStates$ = () => {
const selector = selectors.loadingStates;
return selectManager.select$(selector);
};
const getLoadingStateByIds$ = (ids: Id[]) => {
const selector = selectors.createLoadingStateByIds(ids);
return selectManager.select$(selector);
};
return {
get$,
getById$,
getDictionary$,
getPage$,
getPageLoadingState$,
getLoadingStateById$,
getLoadingStateByIds$,
getAll$,
getPages$,
getLoadingState$,
getLoadingStates$,
};
};
================================================
FILE: src/entity/methods/select/types.ts
================================================
import { Observable } from 'rxjs';
import { Dictionary, Id, LoadingState } from '../../../system-types';
import { Page } from '../../types';
export interface SelectEntityMethods<T, TPageMetadata> {
get$(config: unknown): Observable<T[] | undefined>;
getDictionary$(config: unknown): Observable<Dictionary<T>>;
getById$(id: Id): Observable<T | undefined>;
getLoadingState$(): Observable<LoadingState | undefined>;
getLoadingStates$(): Observable<Dictionary<LoadingState>>;
getLoadingStateById$(
id: Id,
isAsap?: boolean,
): Observable<LoadingState | undefined>;
getLoadingStateByIds$(
ids: Id[],
isAsap?: boolean,
): Observable<LoadingState | undefined>;
getPage$(
config: unknown,
isAsap?: boolean,
): Observable<Page<TPageMetadata> | undefined>;
getPageLoadingState$(
config: unknown,
isAsap?: boolean,
): Observable<LoadingState | undefined>;
getAll$(): Observable<T[] | undefined>;
getPages$(): Observable<Page<TPageMetadata>[]>;
}
================================================
FILE: src/entity/reducer.ts
================================================
import { EntityActionTypes, EntitySchema, EntityState } from './types';
import { Reducer } from '../system-types';
import { combineReducers } from '../shared/utils';
import { createDictionaryReducer } from './reducers/dictionary';
import { createIdsReducer } from './reducers/ids';
import { createPagesReducer } from './reducers/pages';
import { createChange, createResolveChange, createSetLoadingState, EntityActions } from './actions';
import { createByIdLoadingStatesReducer } from './reducers/loading-states';
import { createEntityLoadingStateReducer } from './reducers/loading-state';
import { batchActionsReducerFactory } from '../shared/system/reducers';
const createBaseReducer = <T, TPageMetadata>(
actionTypes: EntityActionTypes,
): Reducer<EntityState<T, TPageMetadata>, EntityActions> => combineReducers<EntityState<T, TPageMetadata>, EntityActions>({
dictionary: createDictionaryReducer(actionTypes),
ids: createIdsReducer(actionTypes),
pages: createPagesReducer(actionTypes),
loadingStates: createByIdLoadingStatesReducer(actionTypes),
loadingState: createEntityLoadingStateReducer(actionTypes),
});
const createListReducer = <T, TPageMetadata>(
actionTypes: EntityActionTypes,
schema: EntitySchema<T>,
): Reducer<EntityState<T, TPageMetadata>, EntityActions> => {
const baseReducer = createBaseReducer<T, TPageMetadata>(actionTypes);
return (state: EntityState<T, TPageMetadata>, action: EntityActions) => {
switch (action.type) {
case actionTypes.CHANGE_LIST: {
const actionCreator = createChange(actionTypes);
const { payload: { patches, changeId }, options } = action;
const actions = patches.map((patch) => actionCreator(
patch.id,
patch.patch,
changeId,
options,
));
return actions.reduce(baseReducer, state);
}
case actionTypes.RESOLVE_CHANGE_LIST: {
const actionCreator = createResolveChange(actionTypes);
const { payload: { patches, changeId, success }, options } = action;
const actions = patches.map((patch) => actionCreator(
patch.id,
changeId,
success,
patch.patch,
options,
));
return actions.reduce(baseReducer, state);
}
case actionTypes.SET_LOADING_STATES: {
const actionCreator = createSetLoadingState(actionTypes, schema);
const { payload: { ids, state: loadingState }, options } = action;
const actions = ids.map((id) => actionCreator(
loadingState,
undefined,
id,
undefined,
options,
));
return actions.reduce(baseReducer, state);
}
default:
return baseReducer(state, action);
}
};
};
export const createEntityReducer = <T, TPageMetadata>(
actionTypes: EntityActionTypes,
schema: EntitySchema<T>,
): Reducer<EntityState<T, TPageMetadata>, EntityActions> => {
const listReducer = createListReducer<T, TPageMetadata>(actionTypes, schema);
return batchActionsReducerFactory(actionTypes, listReducer);
};
================================================
FILE: src/entity/reducers/dictionary.spec.ts
================================================
import { Id } from '../../system-types';
import { createTestHelpers } from '../../testing/helpers';
import { createDictionaryReducer } from './dictionary';
type TestEntity = {
id: Id;
profile?: {
image?: string;
age?: number;
name?: string;
},
}
type TPageMetadata = any;
const {
reducer,
actionCreators,
} = createTestHelpers<TestEntity, TPageMetadata>(createDictionaryReducer);
describe('dictionary', () => {
test('should return default state', () => {
// act
const actual = reducer(undefined, { type: 'INIT' } as any);
// assert
expect(actual).toEqual({});
});
test('should add entity', () => {
// arrange
const entity: TestEntity = { id: 1 };
const action = actionCreators.ADD(entity);
// act
const actual = reducer(undefined, action);
// assert
const expected = { [entity.id]: { actual: entity } };
expect(actual).toEqual(expected);
});
test('should add entities', () => {
// arrange
const entities = [{ id: 1 }, { id: 2 }];
const action = actionCreators.ADD_LIST(entities, null);
// act
const actual = reducer(undefined, action);
// assert
const expected = entities.reduce((
memo: any,
entity: any,
) => {
memo[entity.id] = { actual: entity };
return memo;
}, {});
expect(actual).toEqual(expected);
});
test.each`
options
${{ merge: true }}
${{ merge: false }}
`('should merge entity with the same if options.merge option passed, otherwise replace ($options)', ({ options }) => {
// arrange
const entity = { id: 1, name: 'entity 1' };
const action = actionCreators.ADD(entity);
const entity2 = { id: 1, age: 1 };
const action2 = actionCreators.ADD(entity2, undefined, undefined, options);
// act
const instances = [action, action2].reduce(reducer, undefined);
const actual = instances[entity.id].actual;
// assert
const expected = options.merge ? { id: 1, name: 'entity 1', age: 1 } : { id: 1, age: 1 };
expect(actual).toEqual(expected);
});
test('should remove entity', () => {
// arrange
const entity: TestEntity = { id: 1 };
const action = actionCreators.REMOVE(entity.id);
// act
const actual = reducer({
[entity.id]: {
actual: entity,
changes: [],
},
}, action);
// assert
const expected = {};
expect(actual).toEqual(expected);
});
test('should remove entities', () => {
// arrange
const entity1: TestEntity = { id: 1 };
const entity2: TestEntity = { id: 2 };
const action = actionCreators.REMOVE_LIST([entity1.id, entity2.id]);
// act
const actual = reducer({
[entity1.id]: {
actual: entity1,
changes: [],
},
[entity2.id]: {
actual: entity2,
changes: [],
},
}, action);
// assert
const expected = {};
expect(actual).toEqual(expected);
});
describe('#CHANGE', () => {
test.each`
options
${{ merge: true }}
${{ merge: false }}
`('should override entity when no merge option passed and merge otherwise ($options)', ({ options }) => {
// arrange
const entity: TestEntity = { id: 1, profile: { image: '1.png' } };
const patch = { profile: { age: 18 } };
const action = actionCreators.CHANGE(entity.id, patch, undefined, options);
// act
const instances = reducer({
[entity.id]: {
actual: entity,
changes: [],
},
}, action);
// assert
const expected = options.merge ? { image: '1.png', age: 18 } : { age: 18 };
const actual = instances[entity.id].actual.profile;
expect(actual).toEqual(expected);
});
test.each`
ifNotExist
${undefined}
${true}
`(
'should either create entity or ignore based on options.ifNotExist=$ifNotExist',
({ ifNotExist }) => {
// arrange
const id = 1;
const patch = { profile: { age: 18 } };
const action = actionCreators.CHANGE(id, patch, undefined, { ifNotExist });
// act
const actual = reducer(undefined, action);
// assert
const expected = ifNotExist ? {
[id]: { actual: { id, profile: { age: 18 } } },
} : {};
const expectedKeys = ifNotExist ? [id.toString()] : [];
expect(Object.keys(actual)).toEqual(expectedKeys);
expect(actual).toEqual(expected);
},
);
});
});
================================================
FILE: src/entity/reducers/dictionary.ts
================================================
import { Dictionary } from '../../system-types';
import { createAddByHash, createResolveChange, EntityActions } from '../actions';
import { EntityActionTypes, EntityInstanceState } from '../types';
import { keyBy } from 'lodash-es';
import { createInstanceReducer } from './instance';
const addInstances = <T>(
state: Dictionary<EntityInstanceState<T>>,
instances: EntityInstanceState<T>[],
): Dictionary<EntityInstanceState<T>> => {
const newInstances = keyBy<EntityInstanceState<T>>(instances, 'actual.id');
return { ...state, ...newInstances };
};
export const createDictionaryReducer = <T>(
types: EntityActionTypes,
) => {
const instanceReducer = createInstanceReducer<T>(types);
return (
state: Dictionary<EntityInstanceState<T>> = {},
action: EntityActions,
): Dictionary<EntityInstanceState<T>> => {
switch (action.type) {
case types.ADD: {
const entity = action.payload.entity;
const instanceState = state[entity.id];
const instance = instanceReducer(instanceState, action);
if (!instance) return state;
return addInstances(state, [instance]);
}
case types.RESOLVE_ADD: {
const {
payload: { success, result, tempId },
options,
} = action;
const optimisticCreated = state[tempId];
if (!optimisticCreated) return state;
const { [tempId]: deleted, ...rest } = state;
if (!success) return rest;
const id = result.id;
const patch = { ...optimisticCreated.actual, id };
const resolveChangeAction = createResolveChange(types)(tempId, tempId, true, patch, options);
const instance = instanceReducer(state[tempId], resolveChangeAction);
if (!instance) return state;
return addInstances(state, [instance]);
}
case types.ADD_LIST: {
const { payload: { entities, configHash }, options } = action;
const add = createAddByHash(types);
const instances = entities.map((entity) => {
const action = add(entity, configHash, undefined, options);
const instanceState = state[entity.id];
return instanceReducer(instanceState, action) as EntityInstanceState<T>;
});
return addInstances(state, instances);
}
case types.CHANGE:
case types.RESOLVE_CHANGE: {
const { payload: { id }, options: { ifNotExist } } = action;
const instanceState = state[id];
if (instanceState === undefined && !ifNotExist) return state;
const instance = instanceReducer(instanceState, action) as EntityInstanceState<T>;
return { ...state, [id]: instance };
}
case types.REMOVE:
case types.RESOLVE_REMOVE:
case types.RESTORE_REMOVED: {
const { payload: { id } } = action;
const instanceState = state[id];
const instance = instanceReducer(instanceState, action);
if (instance) return { ...state, [id]: instance };
const { [id]: deleted, ...rest } = state;
return rest;
}
case types.REMOVE_LIST:
case types.RESOLVE_REMOVE_LIST:
case types.RESTORE_REMOVED_LIST: {
const { payload: { ids } } = action;
return ids.reduce((memo, id) => {
const instanceState = state[id];
const instance = instanceReducer(instanceState, action);
if (instance) {
memo[id] = instance;
} else {
delete memo[id];
}
return memo;
}, { ...state });
}
case types.CLEAR_ALL: {
return {};
}
default:
return state;
}
};
};
================================================
FILE: src/entity/reducers/ids.spec.ts
================================================
import { createIdsReducer } from './ids';
import { createTestHelpers } from '../../testing/helpers';
const {
reducer,
actionCreators,
} = createTestHelpers(createIdsReducer);
describe('ids', () => {
describe('#ADD', () => {
test('should add entity id', () => {
// arrange
const entity = { id: 1 };
const action = actionCreators.ADD(entity);
// act
const actual = reducer(undefined, action);
// assert
const expected = [entity.id];
expect(actual).toEqual(expected);
});
test('should add entity id to options.pasteIndex', () => {
// arrange
const action = actionCreators.ADD({ id: 99 }, undefined, undefined, { pasteIndex: 2 });
// act
const actual = reducer([1, 2, 3, 4], action);
// assert
const expected = [1, 2, 99, 3, 4];
expect(actual).toEqual(expected);
});
});
test('should return default state', () => {
// act
const actual = reducer(undefined, { type: 'INIT' } as any);
// assert
expect(actual).toEqual([]);
});
test('should add entity ids from payload.$source', () => {
// arrange
const entities = [{ id: 1 }, { id: 2 }];
const action = actionCreators.ADD_LIST(entities);
// act
const actual = reducer(undefined, action);
// assert
const expected = entities.map((entity) => entity.id);
expect(actual).toEqual(expected);
});
test('should remove entity id', () => {
// arrange
const entity = { id: 3 };
const action = actionCreators.REMOVE(entity.id);// act
const actual = reducer([1, 2, entity.id, 4], action);
// assert
const expected = [1, 2, 4];
expect(actual).toEqual(expected);
});
test('should remove entities ids', () => {
// arrange
const entity1 = { id: 3 };
const entity2 = { id: 4 };
const action = actionCreators.REMOVE_LIST([entity1.id, entity2.id]);// act
const actual = reducer([1, 2, entity1.id, entity2.id], action);
// assert
const expected = [1, 2];
expect(actual).toEqual(expected);
});
test.each`
exist
${true}
${false}
`('should add entity id when change if options.ifNotExist options passed and not exist($exist)', ({ exist }) => {
// arrange
const action = actionCreators.CHANGE(1, {}, undefined, { ifNotExist: true });
const initialState = exist ? [1] : [];
// act
const actual = reducer(initialState, action);
// assert
const expected = [1];
expect(actual).toEqual(expected);
});
});
================================================
FILE: src/entity/reducers/ids.ts
================================================
import { EntityActionTypes, EntityInsertOptions } from '../types';
import { EntityActions } from '../actions';
import { Id } from '../../system-types';
import { isNil } from 'lodash-es';
import { removeFromArray } from '../../utils';
const getOnlyNewIds = (
state: Id[],
ids: Id[],
): Id[] => {
return ids.filter((id: Id) => !state.includes(id));
};
const addIds = (
state: Id[],
ids: Id[],
options?: EntityInsertOptions,
): Id[] => {
const newIds = getOnlyNewIds(state, ids);
if (options && !isNil(options.pasteIndex)) {
const pre = state.slice(0, options.pasteIndex);
const post = state.slice(options.pasteIndex);
return pre.concat(newIds, post);
}
return state.concat(newIds);
};
export const createIdsReducer = (
types: EntityActionTypes,
) => (
state: Id[] = [],
action: EntityActions,
): Id[] => {
switch (action.type) {
case types.ADD: {
const entity = action.payload.entity;
return addIds(state, [entity.id], action.options);
}
case types.CHANGE: {
const index = state.indexOf(action.payload.id);
if (index === -1) {
if (!action.options.ifNotExist) return state;
return addIds(state, [action.payload.id]);
}
return state;
}
case types.ADD_LIST: {
const entities = action.payload.entities;
const ids = entities.map((e) => e.id);
return addIds(state, ids, action.options);
}
case types.REMOVE: {
const { payload: { id }, options: { safe, optimistic } } = action;
if (optimistic || safe) return state;
return removeFromArray(state, [id]);
}
case types.REMOVE_LIST: {
const { payload: { ids }, options: { safe, optimistic } } = action;
if (optimistic || safe) return state;
return removeFromArray(state, ids);
}
case types.RESOLVE_REMOVE: {
const { payload: { success, id }, options: { safe } } = action;
if (!success || safe) return state;
return removeFromArray(state, [id]);
}
case types.RESOLVE_ADD: {
const { payload: { success, result, tempId } } = action;
if (!success) return removeFromArray(state, [tempId]);
return state.map((id) => {
if (id === tempId) return result.id;
return id;
});
}
case types.CLEAR_ALL: {
return [];
}
default:
return state;
}
};
================================================
FILE: src/entity/reducers/instance.spec.ts
================================================
import { createTestHelpers } from '../../testing/helpers';
import { createInstanceReducer } from './instance';
import { EntityInstanceState, EntityType } from '../types';
import { Change } from '../../shared/change/types';
const {
reducer,
actionCreators,
} = createTestHelpers(createInstanceReducer);
describe('instance', () => {
test('should return default state', () => {
// act
const actual = reducer(undefined, { type: 'INIT' } as any);
// assert
expect(actual).toEqual(undefined);
});
describe('#ADD', () => {
test.each`
merge
${true}
${false}
`('should add entity as stable change (without id) and merge=$merge if there are changes', ({ merge }) => {
// arrange
const entity: EntityType<any> = { id: 1 };
const action = actionCreators.ADD(entity, undefined, undefined, { merge });
const existChange = { stable: false, patch: {}, merge: false };
const state: EntityInstanceState<any> = {
actual: entity, changes: [existChange],
};
// act
const actual = reducer(state, action);
// assert
const addChange: Change<any> = { stable: true, patch: entity, merge };
const expected = {
actual: entity,
changes: [existChange, addChange],
};
expect(actual).toEqual(expected);
});
});
});
================================================
FILE: src/entity/reducers/instance.ts
================================================
import { EntityActionTypes, EntityInstanceState, EntityType } from '../types';
import { EntityActions } from '../actions';
import { createInstanceByChanges, getMergedChanges } from '../../shared/change/utils';
import { createChangeReducer } from '../../shared/change/reducer';
import { createChange } from '../../shared/change/actions';
export const createInstanceReducer = <T>(types: EntityActionTypes) => {
const changeReducer = createChangeReducer(types);
const createChangeAction = createChange<EntityType<T>>(types);
return (
state: EntityInstanceState<T> | undefined,
action: EntityActions,
): EntityInstanceState<T> | undefined => {
switch (action.type) {
case types.ADD: {
const {
options: { optimistic, merge },
payload: { entity, tempId },
} = action;
const instance = createInstanceByChanges(
state,
entity as EntityType<T>,
merge,
!optimistic,
tempId,
);
return getMergedChanges(instance, true);
}
case types.CHANGE: {
const {
options,
payload: { id, patch, changeId },
} = action;
const { ifNotExist } = options;
if (!ifNotExist && !state) return state;
const patchWithId = { id, ...patch };
const resultPatch = ifNotExist ? patchWithId : patch;
const changeAction = createChangeAction(resultPatch, changeId, options);
return changeReducer(state, changeAction) as EntityInstanceState<T>;
}
case types.RESOLVE_CHANGE: {
return changeReducer(state, action) as EntityInstanceState<T>;
}
case types.REMOVE:
case types.REMOVE_LIST: {
if (!state) return state;
const { options: { optimistic, safe } } = action;
if (safe || optimistic) return { ...state, removed: true };
return undefined;
}
case types.RESOLVE_REMOVE:
case types.RESOLVE_REMOVE_LIST: {
if (!state) return state;
const {
payload: { success },
options: { safe },
} = action;
if (safe || state.removed === false) return state;
if (success) return undefined;
return { ...state, removed: false };
}
case types.RESTORE_REMOVED:
case types.RESTORE_REMOVED_LIST: {
if (!state) return state;
return { ...state, removed: false };
}
default:
return state;
}
};
};
================================================
FILE: src/entity/reducers/loading-state.ts
================================================
import { LoadingState } from '../../system-types';
import { createLoadingStateReducer } from '../../shared/loading-state/reducers';
import { EntityActionTypes } from '../types';
import { EntityActions } from '../actions';
import { RequestOptions } from '../../shared/types';
export const createEntityLoadingStateReducer = (
types: EntityActionTypes,
) => (
state: LoadingState | undefined,
action: EntityActions,
) => {
const loadingStateReducer = createLoadingStateReducer(types);
if (action.type !== types.SET_LOADING_STATE) return state;
if ((action.options as RequestOptions)?.globalLoadingState === false) return state;
return loadingStateReducer(state, action);
};
================================================
FILE: src/entity/reducers/loading-states.ts
================================================
import { EntityActionTypes } from '../types';
import { Dictionary, Id, LoadingState } from '../../system-types';
import { EntityActions } from '../actions';
import { createLoadingStateReducer } from '../../shared/loading-state/reducers';
import { isNil } from 'lodash-es';
import { removeFromObject } from '../../utils';
const removeState = (
state: Dictionary<LoadingState>,
ids: Id[] = [],
condition: boolean = true,
): Dictionary<LoadingState> => {
if (!condition) return state;
return removeFromObject(state, ids);
};
export const createByIdLoadingStatesReducer = (
types: EntityActionTypes,
) => {
const entityLoadingStateReducer = createLoadingStateReducer(types);
return (
state: Dictionary<LoadingState> = {},
action: EntityActions,
): Dictionary<LoadingState> => {
switch (action.type) {
case types.SET_LOADING_STATE: {
const id = action.payload.id;
if (isNil(id)) return state;
const byId = entityLoadingStateReducer(state[id], action) as LoadingState;
return { ...state, [id]: byId };
}
case types.RESOLVE_ADD: {
const { payload: { tempId } } = action;
return removeState(state, [tempId]);
}
case types.REMOVE: {
const { payload: { id }, options: { optimistic } } = action;
return removeState(state, [id], !optimistic);
}
case types.RESOLVE_REMOVE: {
const { payload: { success, id } } = action;
return removeState(state, [id], !success);
}
case types.REMOVE_LIST: {
const { payload: { ids }, options: { optimistic } } = action;
return removeState(state, ids, !optimistic);
}
case types.RESOLVE_REMOVE_LIST: {
const { payload: { success, ids } } = action;
return removeState(state, ids, !success);
}
case types.CLEAR_ALL: {
return {};
}
default:
return state;
}
};
};
================================================
FILE: src/entity/reducers/pages.spec.ts
================================================
import { createPagesReducer } from './pages';
import { EntityActions } from '../actions';
import { createTestHelpers } from '../../testing/helpers';
jest.mock('../utils');
const {
reducer,
actionCreators,
} = createTestHelpers(createPagesReducer);
describe('pages', () => {
test('should return default value', () => {
// act
const actual = reducer(undefined, { type: 'INIT' } as any);
// assert
expect(actual).toEqual({});
});
test('should remove id for all pages, otherwise default', () => {
// arrange
const action: EntityActions = actionCreators.REMOVE(1);
const initialState = {
1: { ids: [1, 2] },
2: { ids: [2, 1] },
};
// act
const actual = reducer(initialState, action);
// assert
const expected = {
1: { ids: [2] },
2: { ids: [2] },
};
expect(actual).toEqual(expected);
});
test('should remove ids for all pages, otherwise default', () => {
// arrange
const action: EntityActions = actionCreators.REMOVE_LIST([1, 2]);
const initialState = {
1: { ids: [1, 2, 3] },
2: { ids: [2, 1, 5] },
};
// act
const actual = reducer(initialState, action);
// assert
const expected = {
1: { ids: [3] },
2: { ids: [5] },
};
expect(actual).toEqual(expected);
});
});
================================================
FILE: src/entity/reducers/pages.ts
================================================
import { EntityActionTypes, EntityType, Page } from '../types';
import { Dictionary } from '../../system-types';
import { EntityActions } from '../actions';
import { isNil, uniq } from 'lodash-es';
import { createLoadingStateReducer } from '../../shared/loading-state/reducers';
import { MAX_PAGES_COUNT } from '../constants';
import { removeFromArray } from '../../utils';
const addList = <T, TPageMetadata>(
state: Page<TPageMetadata> | undefined,
data: EntityType<T>[],
): Page<TPageMetadata> => {
const newIds = data.map(entity => entity.id);
const oldIds = state?.ids ?? [];
return {
...state,
ids: uniq(oldIds.concat(newIds)),
};
};
const createPageReducer = <TPageMetadata>(
types: EntityActionTypes,
) => {
const loadingStateReducer = createLoadingStateReducer(types);
return (
state: Page<TPageMetadata> | undefined,
action: EntityActions,
): Page<TPageMetadata> | undefined => {
switch (action.type) {
case types.ADD_LIST: {
let newState = state ?? { ids: [] };
if (!isNil(action.payload.hasMore)) {
newState = { ...newState, hasMore: action.payload.hasMore };
}
if (!isNil(action.payload.metadata)) {
newState = { ...newState, metadata: action.payload.metadata as TPageMetadata }
}
if (action.payload.isReplace) {
newState = { ...newState, ids: undefined };
}
return addList(newState, action.payload.entities);
}
case types.ADD: {
return addList(state, [action.payload.entity]);
}
case types.REMOVE: {
const { payload: { id }, options: { safe, optimistic } } = action;
if (optimistic || safe) return state;
// this check needs to clear immutable reference updating.
// It means, no state mutating if this id doesn't exist here
if (!state?.ids?.includes(id)) return state;
return {
...state,
ids: removeFromArray(state.ids, [id]),
};
}
case types.RESOLVE_REMOVE: {
const { payload: { success, id }, options: { safe } } = action;
if (!success || safe) return state;
// this check needs to clear immutable reference updating.
// It means, no state mutating if this id doesn't exist here
if (!state?.ids?.includes(id)) return state;
return {
...state,
ids: removeFromArray(state.ids, [id]),
};
}
case types.REMOVE_LIST: {
const { payload: { ids }, options: { safe, optimistic } } = action;
if (optimistic || safe) return state;
if (!state?.ids) return state;
// this check needs to clear immutable reference updating.
// It means, no state mutating if this id doesn't exist here
const hasIds = ids.some((id) => state.ids?.includes(id));
if (!hasIds) return state;
return {
...state,
ids: removeFromArray(state.ids, ids),
};
}
case types.RESOLVE_REMOVE_LIST: {
const { payload: { success, ids }, options: { safe } } = action;
if (!success || safe) return state;
if (!state?.ids) return state;
// this check needs to clear immutable reference updating.
// It means, no state mutating if this id doesn't exist here
const hasIds = ids.some((id) => state.ids?.includes(id));
if (!hasIds) return state;
return {
...state,
ids: removeFromArray(state.ids, ids),
};
}
case types.RESOLVE_ADD: {
const { payload: { success, tempId, result } } = action;
if (!state) return state;
if (success) return {
...state,
ids: state.ids?.map((id) => {
if (id === tempId) return result.id;
return id;
}),
};
return {
...state,
ids: removeFromArray(state?.ids ?? [], [tempId]),
};
}
case types.SET_LOADING_STATE: {
return {
...state,
ids: state?.ids,
loadingState: loadingStateReducer(state?.loadingState, action),
};
}
default:
return state;
}
};
};
export const createPagesReducer = <TPageMetadata>(
types: EntityActionTypes,
) => {
const pageReducer = createPageReducer<TPageMetadata>(types);
return (
state: Dictionary<Page<TPageMetadata>> = {},
action: EntityActions,
): Dictionary<Page<TPageMetadata>> => {
switch (action.type) {
case types.SET_LOADING_STATE:
case types.ADD_LIST:
case types.ADD: {
const {
payload: { configHash },
options: { maxPagesCount = MAX_PAGES_COUNT },
} = action;
if (isNil(configHash)) return state;
const pageExist = configHash in state;
const page = pageReducer(state[configHash], action);
if (!page) return state;
const newState = { ...state, [configHash]: page };
if (pageExist) return newState;
const pageHashes = Object.keys(newState);
const pagesCount = pageHashes.length;
page.order = pagesCount - 1;
if (pagesCount <= maxPagesCount) return newState;
return pageHashes.reduce((memo: Dictionary<Page<TPageMetadata>>, hash: string) => {
const existPage = newState[hash];
if (existPage.order === 0) return memo;
memo[hash] = {
...existPage,
order: (existPage.order ?? pagesCount) - 1,
};
return memo;
}, {});
}
case types.RESOLVE_ADD:
case types.REMOVE:
case types.RESOLVE_REMOVE:
case types.REMOVE_LIST:
case types.RESOLVE_REMOVE_LIST: {
return Object.keys(state).reduce((
memo: Dictionary<Page<TPageMetadata>>,
hash: string,
) => {
const page = pageReducer(state[hash], action);
if (!page) return memo;
memo[hash] = page;
return memo;
}, {});
}
case types.CLEAR: {
const hash = action.payload.configHash;
const { [hash]: deleted, ...rest } = state;
return rest;
}
case types.CLEAR_ALL: {
return {};
}
default:
return state;
}
};
};
================================================
FILE: src/entity/selectors.ts
================================================
import { createSelector, Selector } from 'reselect';
import {
ActualSelector,
BaseEntitySelectors,
DictionarySelector,
EntityInstanceState,
EntitySelectors,
EntityState,
EntityType,
IdsSelector,
ListSelector,
LoadingStatesSelector,
Page,
PageSelector,
PagesListSelector,
PagesSelector,
} from './types';
import { PainlessReduxState } from '../painless-redux/types';
import { HashFn, Id, LoadingState } from '../system-types';
import { createLoadingStateSelector } from '../shared/loading-state/selectors';
import { LoadingStateSelector } from '../shared/loading-state/types';
import { isNil, values } from 'lodash-es';
import { getChangeableActual } from '../shared/change/selectors';
export const createDictionarySelector = <T, TPageMetadata>(
selector: Selector<PainlessReduxState, EntityState<T, TPageMetadata>>,
): DictionarySelector<T, TPageMetadata> =>
createSelector(selector, (s) => s.dictionary);
export const createIdsSelector = <T, TPageMetadata>(
selector: Selector<PainlessReduxState, EntityState<T, TPageMetadata>>,
): IdsSelector<T, TPageMetadata> =>
createSelector(selector, (s) => s.ids);
export const createPagesSelector = <T, TPageMetadata>(
selector: Selector<PainlessReduxState, EntityState<T, TPageMetadata>>,
): PagesSelector<T, TPageMetadata> =>
createSelector(selector, (s) => s.pages);
export const createLoadingStatesSelector = <T, TPageMetadata>(
selector: Selector<PainlessReduxState, EntityState<T, TPageMetadata>>,
): LoadingStatesSelector<T, TPageMetadata> =>
createSelector(selector, (s) => s.loadingStates);
const createCreateLoadingStateById = <T, TPageMetadata>(
selector: LoadingStatesSelector<T, TPageMetadata>,
) => (id: Id) => createSelector(
selector,
(loadingStates) => loadingStates[id],
);
const createCreateLoadingStateByIds = <T, TPageMetadata>(
selector: LoadingStatesSelector<T, TPageMetadata>,
) => (ids: Id[]) => createSelector(
selector,
(loadingStates) => ids.reduce((memo: LoadingState, id: Id) => {
const loadingState = loadingStates[id];
memo.isLoading = memo.isLoading || (loadingState?.isLoading ?? false);
memo.error = memo.error || loadingState?.error;
return memo;
}, { isLoading: false }),
);
export const createBaseEntitySelectors = <T, TPageMetadata>(
selector: Selector<PainlessReduxState, EntityState<T, TPageMetadata>>,
): BaseEntitySelectors<T, TPageMetadata> => {
const ids = createIdsSelector(selector);
const dictionary = createDictionarySelector(selector);
const pages = createPagesSelector(selector);
const loadingState = createLoadingStateSelector<EntityState<T, TPageMetadata>>(selector);
const loadingStates = createLoadingStatesSelector(selector);
const createLoadingStateById = createCreateLoadingStateById(loadingStates);
const createLoadingStateByIds = createCreateLoadingStateByIds(loadingStates);
return {
ids,
dictionary,
pages,
loadingState,
loadingStates,
createLoadingStateById,
createLoadingStateByIds,
};
};
const getActual = <T>(
instance: EntityInstanceState<T> | undefined,
): EntityType<T> | undefined => {
if (!instance || instance.removed) return undefined;
return getChangeableActual(instance);
};
export const createCreateActualSelector = <T, TPageMetadata>(
dictionarySelector: DictionarySelector<T, TPageMetadata>,
) => (
id: Id,
): ActualSelector<T, TPageMetadata> => createSelector(
dictionarySelector,
(dictionary) => getActual(dictionary[id]),
);
export const createListSelectorFromPages = <T, TPageMetadata>(
pagesSelector: PagesSelector<T, TPageMetadata>,
): PagesListSelector<T, TPageMetadata> => createSelector(
pagesSelector,
(pages) => values(pages),
);
export const createListSelector = <T, TPageMetadata>(
dictionarySelector: DictionarySelector<T, TPageMetadata>,
) => (
idsSelector: IdsSelector<T, TPageMetadata>,
): ListSelector<T, TPageMetadata> => createSelector(
idsSelector,
dictionarySelector,
(ids, dict) => {
if (!ids) return undefined;
return ids.map(id => getActual(dict[id]))
.filter((actual) => !isNil(actual)) as T[];
},
);
export const createPageSelector = <T, TPageMetadata>(
pagesSelector: PagesSelector<T, TPageMetadata>,
hash: string,
): PageSelector<T, TPageMetadata> =>
createSelector(pagesSelector, (pages) => pages[hash]);
export const createCreatePageIdsSelector = <T, TPageMetadata>(pagesSelector: PagesSelector<T, TPageMetadata>) => (
hash: string,
): IdsSelector<T, TPageMetadata> => {
const pageSelector = createPageSelector<T, TPageMetadata>(pagesSelector, hash);
return createSelector(
pageSelector,
(page: Page<TPageMetadata> | undefined) => {
if (!page) return undefined;
return page.ids;
},
);
};
export const createCreatePageIdsByConfigSelector = <T, TPageMetadata>(
pagesSelector: PagesSelector<T, TPageMetadata>,
hashFn: HashFn,
) => (
config: unknown,
): IdsSelector<T, TPageMetadata> => {
const hash = hashFn(config);
return createCreatePageIdsSelector<T, TPageMetadata>(pagesSelector)(hash);
};
export const createCreatePageByConfigSelector = <T, TPageMetadata>(
pagesSelector: PagesSelector<T, TPageMetadata>,
hashFn: HashFn,
) => (
config: unknown,
): PageSelector<T, TPageMetadata> => {
const hash = hashFn(config);
return createPageSelector<T, TPageMetadata>(pagesSelector, hash);
};
export const createCreatePageLoadingState = <T, TPageMetadata>(
pagesSelector: PagesSelector<T, TPageMetadata>,
hashFn: HashFn,
) => (
config: unknown,
): LoadingStateSelector<EntityState<T, TPageMetadata>> => {
const hash = hashFn(config);
const pageSelector = createPageSelector<T, TPageMetadata>(pagesSelector, hash);
return createSelector(
pageSelector,
(page) => page?.loadingState,
);
};
// TODO(egorgrushin): refactor here
export const createEntitySelectors = <T, TPageMetadata>(
selector: Selector<PainlessReduxState, EntityState<T, TPageMetadata>>,
hashFn: HashFn,
): EntitySelectors<T, TPageMetadata> => {
const {
dictionary,
loadingStates,
loadingState,
pages,
ids,
createLoadingStateById,
createLoadingStateByIds,
} = createBaseEntitySelectors<T, TPageMetadata>(selector);
const createListSelectorByIds = createListSelector(dictionary);
const all = createListSelectorByIds(ids);
const allPages = createListSelectorFromPages(pages);
const createActual = createCreateActualSelector(dictionary);
const createPageIds = createCreatePageIdsSelector(pages);
const createPageIdsByConfig = createCreatePageIdsByConfigSelector(pages, hashFn);
const createPage = createCreatePageByConfigSelector(pages, hashFn);
const createPageLoadingState = createCreatePageLoadingState(pages, hashFn);
const createPageListByConfig = (config: unknown) => {
const pageIdsSelector = createPageIdsByConfig(config);
return createListSelectorByIds(pageIdsSelector);
};
return {
dictionary,
ids,
pages,
loadingStates,
loadingState,
all,
createActual,
createPageIds,
createPageIdsByConfig,
createListSelectorByIds,
createPageListByConfig,
createPage,
createPageLoadingState,
createLoadingStateById,
createLoadingStateByIds,
allPages,
};
};
================================================
FILE: src/entity/types.ts
================================================
import {Observable} from 'rxjs';
import {Selector} from 'reselect';
import {DeepPartial, Dictionary, HashFn, Id, LoadingState} from '../system-types';
import {ChangeableState, PatchRequest} from '../shared/change/types';
import {
LoadingStateActionTypes,
LoadingStateSelector,
LoadingStateSelectors,
LoadingStateSetOptions,
LoadingStateState,
} from '../shared/loading-state/types';
import {SelectEntityMethods} from './methods/select/types';
import {DispatchEntityMethods} from './methods/dispatch/types';
import {MixedEntityMethods} from './methods/mixed/types';
import {SystemActionTypes} from '../shared/system/types';
import {EntityActionCreators} from './action-creators.types';
import {RequestOptions} from '../shared/types';
export type EntityType<T> = T & { id: Id };
export interface EntitySchema<T> {
name: string;
hashFn: HashFn;
pageSize: number;
maxPagesCount: number;
id?(data: T): Id;
}
export interface EntityLoadOptions extends EntityInsertOptions, RequestOptions {
}
export interface EntityGetOptions extends EntityAddOptions {
}
export interface EntityGetListOptions extends EntityLoadListOptions {
}
export interface EntityLoadListOptions extends EntityAddListOptions, RequestOptions {
pageSize?: number;
}
export interface EntityAddOptions extends EntityOptimisticOptions, EntityInsertOptions, RequestOptions {
}
interface EntityInternalOptions {
maxPagesCount?: number;
}
export interface EntityInternalAddOptions extends EntityAddOptions, EntityInternalOptions {
}
export interface EntityInternalAddListOptions extends EntityAddListOptions, EntityInternalOptions {
}
export interface EntityOptimisticOptions {
optimistic?: boolean;
}
export interface EntityRemoveOptions extends EntityOptimisticOptions, RequestOptions {
safe?: boolean;
}
export interface EntityRemoveListOptions extends EntityOptimisticOptions, RequestOptions {
safe?: boolean;
}
export interface EntitySetLoadingStateOptions extends LoadingStateSetOptions {
}
export interface EntityInternalSetLoadingStateOptions extends EntitySetLoadingStateOptions, EntityInternalOptions {
}
export interface EntityInsertOptions {
pasteIndex?: number;
merge?: boolean;
single?: boolean;
}
export interface EntityAddListOptions extends EntityInsertOptions {
}
export interface Pagination {
index: number;
size: number;
from: number;
to: number;
}
export interface ResponseArray<T, TPageMetadata> {
data: T[];
hasMore?: boolean;
metadata?: TPageMetadata;
}
export interface PaginatedResponse<T, TPageMetadata> extends Pagination {
response: ResponseArray<T, TPageMetadata>;
}
export type Response$Factory<T, TPageMetadata> = (pagination: Pagination) => Observable<ResponseArray<T, TPageMetadata>>;
export interface Page<TPageMetadata> {
ids: Id[] | undefined;
order?: number;
hasMore?: boolean;
loadingState?: LoadingState;
metadata?: TPageMetadata;
}
export interface IdPatchRequest<T> {
id: Id;
patch: PatchRequest<T>;
}
export interface IdPatch<T> {
id: Id;
patch: DeepPartial<T>;
}
export interface EntityInstanceState<T> extends ChangeableState<EntityType<T>> {
removed?: boolean;
}
export interface EntityState<T, TPageMetadata> extends LoadingStateState {
ids: Id[];
dictionary: Dictionary<EntityInstanceState<T>>;
pages: Dictionary<Page<TPageMetadata>>;
loadingStates: Dictionary<LoadingState>;
}
export type IdsSelector<T, TPageMetadata> = Selector<EntityState<T, TPageMetadata>, Id[] | undefined>;
export type DictionarySelector<T, TPageMetadata> = Selector<EntityState<T, TPageMetadata>, Dictionary<EntityInstanceState<T>>>;
export type PagesSelector<T, TPageMetadata> = Selector<EntityState<T, TPageMetadata>, Dictionary<Page<TPageMetadata>>>;
export type PagesListSelector<T, TPageMetadata> = Selector<EntityState<T, TPageMetadata>, Page<TPageMetadata>[]>;
export type PageSelector<T, TPageMetadata> = Selector<EntityState<T, TPageMetadata>, Page<TPageMetadata> | undefined>;
export type LoadingStatesSelector<T, TPageMetadata> = Selector<EntityState<T, TPageMetadata>, Dictionary<LoadingState>>;
export type ActualSelector<T, TPageMetadata> = Selector<EntityState<T, TPageMetadata>, T | undefined>;
export type ListSelector<T, TPageMetadata> = Selector<EntityState<T, TPageMetadata>, T[] | undefined>;
export interface BaseEntitySelectors<T, TPageMetadata> extends LoadingStateSelectors<EntityState<T, TPageMetadata>> {
ids: IdsSelector<T, TPageMetadata>;
dictionary: DictionarySelector<T, TPageMetadata>;
pages: PagesSelector<T, TPageMetadata>;
loadingStates: LoadingStatesSelector<T, TPageMetadata>;
createLoadingStateById: (id: Id) => LoadingStateSelector<EntityState<T, TPageMetadata>>;
createLoadingStateByIds: (ids: Id[]) => LoadingStateSelector<EntityState<T, TPageMetadata>>;
}
export interface EntitySelectors<T, TPageMetadata> extends BaseEntitySelectors<T, TPageMetadata> {
createActual: (id: Id) => ActualSelector<T, TPageMetadata>;
createPage: (config: unknown) => PageSelector<T, TPageMetadata>;
createPageIds: (hash: string) => IdsSelector<T, TPageMetadata>;
createPageLoadingState: (config: unknown) => LoadingStateSelector<EntityState<T, TPageMetadata>>;
createPageIdsByConfig: (config: unknown) => IdsSelector<T, TPageMetadata>;
createListSelectorByIds: (idsSelector: IdsSelector<T, TPageMetadata>) => ListSelector<T, TPageMetadata>;
createPageListByConfig: (config: unknown) => ListSelector<T, TPageMetadata>;
allPages: PagesListSelector<T, TPageMetadata>;
all: ListSelector<T, TPageMetadata>;
}
export interface EntityActionTypes extends SystemActionTypes {
ADD: 'ADD';
RESOLVE_ADD: 'RESOLVE_ADD';
ADD_LIST: 'ADD_LIST';
SET_LOADING_STATE: LoadingStateActionTypes['SET_LOADING_STATE'];
CHANGE: 'CHANGE';
RESOLVE_CHANGE: 'RESOLVE_CHANGE';
REMOVE: 'REMOVE';
RESOLVE_REMOVE: 'RESOLVE_REMOVE';
RESTORE_REMOVED: 'RESTORE_REMOVED';
REMOVE_LIST: 'REMOVE_LIST';
RESOLVE_REMOVE_LIST: 'RESOLVE_REMOVE_LIST';
RESTORE_REMOVED_LIST: 'RESTORE_REMOVED_LIST';
CLEAR: 'CLEAR';
CLEAR_ALL: 'CLEAR_ALL';
CHANGE_LIST: 'CHANGE_LIST';
RESOLVE_CHANGE_LIST: 'RESOLVE_CHANGE_LIST';
SET_LOADING_STATES: 'SET_LOADING_STATES';
}
export type PublicDispatchEntityMethods<T, TPageMetadata> = Omit<DispatchEntityMethods<T, TPageMetadata>,
'changeWithId' | 'changeListWithId' | 'resolveChange' | 'resolveAdd' | 'resolveRemove' | 'resolveChangeList'>
export type PublicSelectEntityMethods<T, TPageMetadata> = Omit<SelectEntityMethods<T, TPageMetadata>, 'get$' | 'getDictionary$' | 'getById$'>
export interface Entity<T, TPageMetadata = void> extends PublicSelectEntityMethods<T, TPageMetadata>, PublicDispatchEntityMethods<T, TPageMetadata>, MixedEntityMethods<T, TPageMetadata> {
actionCreators: EntityActionCreators<T, TPageMetadata>;
}
================================================
FILE: src/entity/utils.ts
================================================
import { createActionTypes, hashIt, typedDefaultsDeep } from '../utils';
import {
EntityActionTypes,
EntitySchema,
EntityType,
PaginatedResponse,
Pagination,
Response$Factory,
ResponseArray,
} from './types';
import { DEFAULT_PAGE_SIZE, ENTITY_TYPE_NAMES, MAX_PAGES_COUNT } from './constants';
import { v4 } from 'uuid';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { getObservable$ } from '../shared/utils';
export const getHash = (config: unknown): string => hashIt(config);
export const getFullEntitySchema = <T>(
schema?: Partial<EntitySchema<T>>,
): EntitySchema<T> => typedDefaultsDeep(schema, {
name: '',
hashFn: getHash,
pageSize: DEFAULT_PAGE_SIZE,
maxPagesCount: MAX_PAGES_COUNT,
}) as EntitySchema<T>;
export const createIdResolver = <T>(
schema: EntitySchema<T>,
) => (data: T): EntityType<T> => {
if (schema.id) return { ...data, id: schema.id(data) };
if ('id' in data) return data as EntityType<T>;
return { ...data, id: v4() };
};
export const createEntityActionTypes = (
entityName: string,
): EntityActionTypes => createActionTypes(ENTITY_TYPE_NAMES, entityName);
export const getPaginated$ = <T, TPageMetadata>(
dataSource: Observable<ResponseArray<T, TPageMetadata>> | Response$Factory<T, TPageMetadata>,
pagination: Pagination,
): Observable<PaginatedResponse<T, TPageMetadata>> => getObservable$(dataSource, pagination).pipe(
map((response) => ({ ...pagination, response })),
);
================================================
FILE: src/index.ts
================================================
export * from './painless-redux/painless-redux';
export * from './painless-redux/types';
export * from './shared/types';
export * from './shared/change/types';
export * from './shared/change/actions';
export * from './shared/loading-state/types';
export * from './shared/system/types';
export * from './workspace/workspace';
export * from './workspace/types';
export { WorkspaceActions } from './workspace/actions';
export * from './workspace/action-creators';
export * from './entity/entity';
export * from './entity/types';
export * from './entity/action-creators';
export * from './entity/action-creators.types';
export { EntityActions } from './entity/actions';
export * from './affect-loading-state/affect-loading-state';
export * from './affect-loading-state/types';
export * from './system-types';
export {
hashString,
hashIt,
snapshot,
merge,
} from './utils';
export {
actionSanitizer,
} from './workspace/utils';
================================================
FILE: src/painless-redux/action-creators.ts
================================================
================================================
FILE: src/painless-redux/constants.ts
================================================
import { SystemActionTypes } from '../shared/system/types';
export const SYSTEM_TYPE_NAMES: Array<keyof SystemActionTypes> = [];
================================================
FILE: src/painless-redux/painless-redux.ts
================================================
import { PainlessRedux, PainlessReduxSchema, SlotTypes } from './types';
import { ActionCreator, AnyAction, Reducer, RxStore, SameShaped } from '../system-types';
import { createDispatcher } from '../dispatcher/dispatcher';
import { typedDefaultsDeep } from '../utils';
import { createSlotSelector, createSlotsSelector } from './selectors';
import { getWrappedReducer } from './reducers';
import { createSelectManager } from '../select-manager/select-manager';
import { createRegister } from './register';
import { createSelector } from 'reselect';
import { createSystemActionTypes } from './utils';
export const createPainlessRedux = (
rxStore: RxStore,
schema?: Partial<PainlessReduxSchema>,
): PainlessRedux => {
const fullSchema = typedDefaultsDeep(schema, {
name: '@store',
entityDomainName: 'entities',
workspaceDomainName: 'workspaces',
useAsapSchedulerInLoadingGuards: true,
selector: (state: any) => state,
}) as PainlessReduxSchema;
const domainName = fullSchema.name;
const initialValue = {};
const selector = createSelector(fullSchema.selector, (state: any) => state[domainName]);
const slotsSelector = createSlotsSelector(fullSchema, selector);
const systemActionTypes = createSystemActionTypes(domainName);
const { value: register, checkSlotUniq, addNewSlotToRegister } = createRegister();
const getReducer = () => getWrappedReducer(fullSchema, register, systemActionTypes, initialValue);
const registerSlotReducer = <TState, TActions extends AnyAction>(
type: SlotTypes,
name: string,
reducer: Reducer<TState, TActions>,
) => {
checkSlotUniq(type, name);
addNewSlotToRegister<TState, TActions>(type, name, reducer);
const fullReducer = getReducer();
rxStore.addReducer(domainName, fullReducer);
};
const registerSlot = <TState, TActionTypes, TActions extends AnyAction>(
type: SlotTypes,
name: string,
reducer: Reducer<TState, TActions>,
actionCreators: SameShaped<TActionTypes, ActionCreator<TActionTypes, TActions>>,
) => {
const dispatcher = createDispatcher<TActionTypes, TActions>(rxStore, actionCreators);
const selectManager = createSelectManager(rxStore);
const currentSlotsSelector = slotsSelector<TState>(type);
const selector = createSlotSelector<TState>(currentSlotsSelector, name);
registerSlotReducer<TState, TActions>(type, name, reducer);
return {
dispatcher,
selector,
selectManager,
};
};
return {
registerSlot,
getReducer,
name: domainName,
schema: fullSchema,
};
};
================================================
FILE: src/painless-redux/reducers.ts
================================================
import { PainlessReduxRegister, PainlessReduxSchema, PainlessReduxState, SlotTypes } from './types';
import { AnyAction, PayloadAction, Reducer } from '../system-types';
import { combineReducers } from '../shared/utils';
import { SystemActionTypes } from '../shared/system/types';
export const getSlotReducer = (
register: PainlessReduxRegister,
type: SlotTypes,
): Reducer<any, AnyAction> => {
const slots = register[type];
return combineReducers(slots);
};
export const createFullReducer = (
schema: PainlessReduxSchema,
register: PainlessReduxRegister,
): Reducer<PainlessReduxState, AnyAction> => combineReducers({
[schema.entityDomainName]: getSlotReducer(register, SlotTypes.Entity),
[schema.workspaceDomainName]: getSlotReducer(register, SlotTypes.Workspace),
});
export const getWrappedReducer = (
schema: PainlessReduxSchema,
register: PainlessReduxRegister,
types: SystemActionTypes,
initialState: PainlessReduxState,
): Reducer<PainlessReduxState, PayloadAction> => {
return createFullReducer(schema, register);
// const reducerMiddlewareFactory = undoReducerFactory(types, initialState);
// return reducerMiddlewareFactory(reducer);
};
================================================
FILE: src/painless-redux/register.ts
================================================
import { PainlessReduxRegister, SlotTypes } from './types';
import { isNil } from 'lodash-es';
import { AnyAction, Reducer } from '../system-types';
export const createRegister = () => {
const value: PainlessReduxRegister = {
[SlotTypes.Entity]: {},
[SlotTypes.Workspace]: {},
};
const checkSlotUniq = (
type: SlotTypes,
name: string,
) => {
const existSlotReducer = value[type][name];
if (isNil(existSlotReducer)) return;
throw new Error('Slot name is not uniq');
};
const addNewSlotToRegister = <TState, TActions extends AnyAction>(
type: SlotTypes,
name: string,
reducer: Reducer<TState, TActions>,
) => {
value[type] = {
...value[type],
[name]: reducer as Reducer<any, AnyAction>,
};
return value;
};
return { addNewSlotToRegister, checkSlotUniq, value };
};
================================================
FILE: src/painless-redux/selectors.ts
================================================
import { PainlessReduxSchema, PainlessReduxState, SlotTypes } from './types';
import { createSelector, Selector } from 'reselect';
import { Dictionary, StrictDictionary } from '../system-types';
export const createEntitiesSelector = (
schema: PainlessReduxSchema,
selector: Selector<any, PainlessReduxState>,
) => createSelector(
selector,
(state: PainlessReduxState) => state[schema.entityDomainName],
);
export const createWorkspacesSelector = (
schema: PainlessReduxSchema,
selector: Selector<any, PainlessReduxState>,
) => createSelector(
selector,
(state: PainlessReduxState) => state[schema.workspaceDomainName],
);
const SLOTS_SELECTOR_FACTORIES: StrictDictionary<SlotTypes, any> = {
[SlotTypes.Entity]: createEntitiesSelector,
[SlotTypes.Workspace]: createWorkspacesSelector,
};
export const createSlotsSelector = (
schema: PainlessReduxSchema,
selector: Selector<any, PainlessReduxState>,
) => <TState>(
type: SlotTypes,
): Selector<PainlessReduxState, Dictionary<TState>> => {
const slotSelectorFactory = SLOTS_SELECTOR_FACTORIES[type];
return slotSelectorFactory(schema, selector);
};
export const createSlotSelector = <TState>(
slotsSelector: Selector<PainlessReduxState, Dictionary<TState>>,
name: string,
): Selector<PainlessReduxState, TState> => createSelector(
slotsSelector,
(state) => state[name],
);
================================================
FILE: src/painless-redux/types.ts
================================================
import { Selector } from 'reselect';
import {
ActionCreator,
AnyAction,
Dictionary,
PayloadAction,
Reducer,
SameShaped,
StrictDictionary,
} from '../system-types';
import { Dispatcher } from '../dispatcher/types';
import { SelectManager } from '../select-manager/types';
export enum SlotTypes {
Entity,
Workspace
}
export interface PainlessReduxSchema {
name: string;
entityDomainName: string;
workspaceDomainName: string;
selector: Selector<any, any>;
useAsapSchedulerInLoadingGuards: boolean;
}
export type PainlessReduxRegister = StrictDictionary<SlotTypes, Dictionary<Reducer<any, AnyAction>>, SlotTypes>;
export type PainlessReduxState = Dictionary<any>;
export type PainlessRedux = {
name: string;
schema: PainlessReduxSchema;
registerSlot<TState, TActionTypes, TActions extends AnyAction>(
type: SlotTypes,
name: string,
reducer: Reducer<TState, TActions>,
actionCreators: SameShaped<TActionTypes, ActionCreator<TActionTypes, TActions>>,
): {
selector: Selector<PainlessReduxState, TState>;
dispatcher: Dispatcher<TActionTypes, TActions>;
selectManager: SelectManager;
};
getReducer(): Reducer<PainlessReduxState, PayloadAction>;
}
================================================
FILE: src/painless-redux/utils.ts
================================================
import { SystemActionTypes } from '../shared/system/types';
import { createActionTypes } from '../utils';
import { SYSTEM_TYPE_NAMES } from './constants';
export const createSystemActionTypes = (
name: string,
): SystemActionTypes => createActionTypes(SYSTEM_TYPE_NAMES, name);
================================================
FILE: src/select-manager/select-manager.ts
================================================
import { RxStore } from '../system-types';
import { asapScheduler, Observable } from 'rxjs';
import { subscribeOn } from 'rxjs/operators';
import { Selector } from 'reselect';
import { select, snapshot } from '../utils';
import { SelectManager } from './types';
export const createSelectManager = (rxStore: RxStore): SelectManager => {
const select$ = <T, R>(
selector: Selector<T, R>,
isAsap: boolean = false,
): Observable<R> => {
const selectObs = rxStore.pipe(select(selector));
if (!isAsap) return selectObs;
return selectObs.pipe(subscribeOn(asapScheduler));
};
const snapshotFn = <T, R>(
selector: Selector<T, R>,
): R | undefined => {
const source$ = select$<T, R>(selector);
return snapshot<R>(source$);
};
return {
select$,
snapshot: snapshotFn,
};
};
================================================
FILE: src/select-manager/types.ts
================================================
import { Selector } from 'reselect';
import { Observable } from 'rxjs';
export interface SelectManager {
select$<T, R>(selector: Selector<T, R>, isAsap?: boolean): Observable<R>;
snapshot<T, R>(selector: Selector<T, R>): R | undefined;
}
================================================
FILE: src/shared/change/actions.ts
================================================
import { ChangeActionTypes, ChangeOptions } from './types';
import { typedDefaultsDeep } from '../../utils';
import { DeepPartial } from '../../system-types';
export const createChange = <T>(types: ChangeActionTypes) => (
patch: DeepPartial<T>,
changeId?: string,
options?: ChangeOptions,
) => {
options = typedDefaultsDeep(options, { merge: true });
const payload = { patch, changeId };
return { type: types.CHANGE, payload, options } as const;
};
export const createResolveChange = <T>(types: ChangeActionTypes) => (
changeId: string,
success: boolean,
remotePatch?: DeepPartial<T>,
options?: ChangeOptions,
) => {
options = typedDefaultsDeep(options, { merge: true });
const payload = { changeId, success, remotePatch };
return { type: types.RESOLVE_CHANGE, payload, options } as const;
};
type SelfActionCreators = ReturnType<typeof createChange>
| ReturnType<typeof createResolveChange>;
export type ChangeActions = ReturnType<SelfActionCreators>
================================================
FILE: src/shared/change/reducer.spec.ts
================================================
import { ChangeActionTypes } from './types';
import { createChangeReducer } from './reducer';
import { createChange } from './actions';
const types: ChangeActionTypes = {
CHANGE: 'CHANGE',
RESOLVE_CHANGE: 'RESOLVE_CHANGE',
};
describe('change', () => {
const initialActual = { values: [0, 1, 2], key: 'test' };
const initialValue = { actual: initialActual };
const reducer = createChangeReducer(types, initialActual);
test('should init value', () => {
// act
const actual = reducer(undefined, { type: 'INIT' } as any);
// assert
expect(actual?.actual).toBe(initialActual);
});
test('should override arrays while changing even if new array is smaller', () => {
// arrange
const values = [3, 4];
const action = createChange(types)({ values });
// act
const actual = reducer(initialValue, action);
// assert
expect(actual?.actual.values).toEqual(values);
});
test('should override arrays while changing even if new array is bigger', () => {
// arrange
const values = [3, 4, 5, 6];
const action = createChange(types)({ values });
// act
const actual = reducer(initialValue, action);
// assert
expect(actual?.actual.values).toEqual(values);
});
test('should set only given properties to undefined', () => {
// arrange
const action = createChange(types)({ values: undefined });
// act
const actual = reducer(initialValue, action);
// assert
expect(actual?.actual).toEqual({ ...initialActual, values: undefined });
});
});
================================================
FILE: src/shared/change/reducer.ts
================================================
import { ChangeableState, ChangeActionTypes } from './types';
import { ChangeActions } from './actions';
import { createInstanceByChanges, getMergedChanges, resolveChanges } from './utils';
export const createChangeReducer = <T>(
types: ChangeActionTypes,
initialActual?: T,
) => (
state: ChangeableState<T> | undefined,
action: ChangeActions,
): ChangeableState<T> | undefined => {
if (!state && initialActual) {
state = { actual: initialActual };
}
switch (action.type) {
case types.CHANGE: {
const {
payload: { patch, changeId },
options: { merge, optimistic },
} = action;
const instance = createInstanceByChanges<T>(
state,
patch,
merge,
!optimistic,
changeId,
);
return getMergedChanges(instance, true);
}
case types.RESOLVE_CHANGE: {
if (!state) return state;
const {
payload: { success, changeId, remotePatch },
options: { merge, optimistic },
} = action;
const instance = createInstanceByChanges<T>(
state,
remotePatch,
merge,
!optimistic,
changeId,
) as ChangeableState<T>;
const changes = resolveChanges(instance?.changes, success, changeId);
const newState = { ...instance, changes };
return getMergedChanges(newState, true);
}
default:
return state;
}
};
================================================
FILE: src/shared/change/selectors.ts
================================================
import { getMergedChanges } from './utils';
import { ChangeableState } from './types';
export const getChangeableActual = <T>(
instance: ChangeableState<T> | undefined,
): T | undefined => {
if (!instance) return undefined;
return getMergedChanges(instance)?.actual;
};
================================================
FILE: src/shared/change/types.ts
================================================
import { DeepPartial } from '../../system-types';
import { RequestOptions } from '../types';
export interface ChangeOptions extends RequestOptions {
merge?: boolean;
ifNotExist?: boolean;
optimistic?: boolean;
useResponsePatch?: boolean;
}
export interface ChangeActionTypes {
CHANGE: 'CHANGE';
RESOLVE_CHANGE: 'RESOLVE_CHANGE';
}
export interface Change<T> {
stable: boolean;
patch: DeepPartial<T>;
merge: boolean;
id?: string;
}
export interface ChangeableState<T> {
actual: T;
changes?: Change<T>[];
}
export type PatchRequest<T> = DeepPartial<T> | ((value: DeepPartial<T> | undefined) => DeepPartial<T>);
================================================
FILE: src/shared/change/utils.ts
================================================
import { DeepPartial } from '../../system-types';
import { Change, ChangeableState, ChangeOptions, PatchRequest } from './types';
import { merge as mergeFn, snapshot } from '../../utils';
import { Observable } from 'rxjs';
export const createEntityChange = <T>(
patch: DeepPartial<T>,
stable = false,
merge = true,
id?: string,
): Change<T> => ({ patch, stable, merge, id });
export const getMergedChanges = <T>(
state: ChangeableState<T> | undefined,
onlyStable?: boolean,
): ChangeableState<T> | undefined => {
if (!state) return state;
let { actual, changes = [] } = state;
if (changes.length === 0) return state;
let change: Change<T> | undefined;
while ((change = changes[0])) {
if (onlyStable && !change.stable) break;
changes = changes.slice(1);
const { merge, patch } = change;
actual = merge ? mergeFn(actual, patch) : patch as T;
}
if (changes.length === 0) return { actual };
return { actual, changes: changes };
};
export const createInstanceByChanges = <T>(
state: ChangeableState<T> | undefined,
patch: DeepPartial<T> | undefined,
merge: boolean = true,
success: boolean = true,
id?: string,
): ChangeableState<T> | undefined => {
if (!patch) return state;
const actual = state?.actual ?? patch as T;
const change = createEntityChange(patch, success, merge, id);
const changes = state?.changes ?? [];
return { actual, changes: changes.concat(change) };
};
export const resolveChanges = <T>(
changes: Change<T>[] | undefined,
success: boolean,
id: string,
): Change<T>[] | undefined => {
if (!changes) return;
if (success) {
return changes.map((change) => {
if (change.id === id) return { ...change, stable: true };
return change;
});
}
return changes.filter((change) => change.id !== id);
};
export const getPatchByOptions = <T>(
patch: DeepPartial<T>,
response: DeepPartial<T> | undefined,
options?: ChangeOptions,
): DeepPartial<T> => {
if (options?.optimistic) return patch;
if (options?.useResponsePatch) return response ?? {};
return patch;
};
export const getResolvePatchByOptions = <T>(
patch: DeepPartial<T>,
response: DeepPartial<T> | undefined,
options?: ChangeOptions,
): DeepPartial<T> | undefined => {
if (options?.useResponsePatch) return response;
};
export const normalizePatch = <T>(
patch: PatchRequest<T>,
oldValue$: Observable<DeepPartial<T> | undefined>,
): DeepPartial<T> => {
if (typeof patch === 'function') {
const oldValue = snapshot(oldValue$);
return patch(oldValue as DeepPartial<T>);
}
return patch;
};
================================================
FILE: src/shared/loading-state/actions.ts
================================================
import { LoadingStateActionTypes, LoadingStateSetOptions } from './types';
import { LoadingState } from '../../system-types';
import { typedDefaultsDeep } from '../../utils';
export const createSetLoadingState = (types: LoadingStateActionTypes) => (
state: LoadingState,
key?: string,
options?: LoadingStateSetOptions,
) => {
options = typedDefaultsDeep(options);
const payload = { state, key };
return { type: types.SET_LOADING_STATE, payload, options } as const;
};
export type LoadingStateActions = ReturnType<ReturnType<typeof createSetLoadingState>>;
================================================
FILE: src/shared/loading-state/reducers.spec.ts
================================================
import { createLoadingStateReducer, loadingStateReducer } from './reducers';
import { LoadingState } from '../../system-types';
import { LoadingStateActionTypes } from './types';
import { LoadingStateActions } from './actions';
const types: LoadingStateActionTypes = {
SET_LOADING_STATE: 'SET_LOADING_STATE',
};
describe('loadingState', () => {
let initialState: any;
const newState = { isLoading: true };
const someKey = 'some-key';
beforeEach(() => {
initialState = {};
});
describe('#createLoadingStateReducer', () => {
const reducer = createLoadingStateReducer(types);
test('should return default state', () => {
// act
const actual = reducer(undefined, { type: 'INIT' } as any);
// assert
expect(actual).toEqual(undefined);
});
test('should correct set state', () => {
// arrange
const action: LoadingStateActions = {
type: types.SET_LOADING_STATE,
payload: {
state: newState,
key: undefined,
},
options: {},
};
// act
const actual = reducer(undefined, action);
// assert
const expected = newState;
expect(actual).toEqual(expected);
});
});
describe('#loadingStateReducer', () => {
test('should set state for a key', () => {
// act
const actual = loadingStateReducer(initialState, someKey, newState);
// assert
const expected: LoadingState = {
byKeys: { [someKey]: newState },
isLoading: false,
};
expect(actual).toEqual(expected);
});
test('should set state as is', () => {
// act
const actual = loadingStateReducer(initialState, undefined, newState);
// assert
expect(actual).toEqual(newState);
});
});
});
================================================
FILE: src/shared/loading-state/reducers.ts
================================================
import { Dictionary, LoadingState } from '../../system-types';
import { LoadingStateActionTypes } from './types';
import { LoadingStateActions } from './actions';
const pureLoadingStateReducer = (
state: LoadingState | undefined,
newState: LoadingState,
): LoadingState => ({ ...state, ...newState });
export const loadingStateByKeysReducer = (
state: Dictionary<LoadingState> = {},
key: string,
newState: LoadingState,
): Dictionary<LoadingState> => {
return {
...state,
[key]: pureLoadingStateReducer(state[key], newState),
};
};
export const loadingStateReducer = (
state: LoadingState | undefined,
key: string | undefined,
newState: LoadingState,
): LoadingState => {
if (!key) return pureLoadingStateReducer(state, newState);
return {
...state,
byKeys: loadingStateByKeysReducer(state?.byKeys, key, newState),
isLoading: state?.isLoading ?? false,
};
};
export const createLoadingStateReducer = (
types: LoadingStateActionTypes,
) => (
state: LoadingState | undefined,
action: LoadingStateActions,
) => {
switch (action.type) {
case types.SET_LOADING_STATE: {
const { key, state: newState } = action.payload;
return loadingStateReducer(state, key, newState);
}
default:
return state;
}
};
================================================
FILE: src/shared/loading-state/selectors.ts
================================================
import { createSelector, Selector } from 'reselect';
import { LoadingStateState } from './types';
export const createLoadingStateSelector = <T extends LoadingStateState>(
selector: Selector<any, T>,
) => createSelector(
selector,
(state: T) => state.loadingState ?? { isLoading: false },
);
================================================
FILE: src/shared/loading-state/types.ts
================================================
import { LoadingState } from '../../system-types';
import { Selector } from 'reselect';
export interface LoadingStateState {
loadingState?: LoadingState;
}
export interface LoadingStateSetOptions {
}
export interface LoadingStateActionTypes {
SET_LOADING_STATE: 'SET_LOADING_STATE';
}
export type LoadingStateSelector<T> = Selector<T, LoadingState | undefined>
export interface LoadingStateSelectors<T> {
loadingState: LoadingStateSelector<T>;
}
================================================
FILE: src/shared/system/actions.ts
================================================
import { SystemActionTypes } from './types';
export const createBatch = <T>(types: SystemActionTypes) => (actions: T[]) => {
const payload = { actions };
return { type: types.BATCH, payload } as const;
};
export type SystemActions = ReturnType<ReturnType<typeof createBatch>>;
================================================
FILE: src/shared/system/reducers.ts
================================================
import { AnyAction, Reducer } from '../../system-types';
import { SystemActionTypes } from './types';
export const batchActionsReducerFactory = <TState, TAction extends AnyAction>(
types: SystemActionTypes,
reducer: Reducer<TState, TAction>,
) => (state: TState, action: TAction): TState => {
if (action.type === types.BATCH) {
const { actions } = (action as any).payload;
return actions.reduce(reducer, state);
}
return reducer(state, action);
};
================================================
FILE: src/shared/system/system.ts
================================================
================================================
FILE: src/shared/system/types.ts
================================================
export interface SystemActionTypes {
BATCH: 'BATCH';
}
================================================
FILE: src/shared/types.ts
================================================
import { AnyAction, LoadingState } from '../system-types';
import { Observable } from 'rxjs';
export type ObservableOrFactory<S, R> = (Observable<R>) | ((value: S) => Observable<R>);
export interface RemotePipeConfig<TSource, TStore, TResponse> {
store$?: Observable<TStore>;
remoteObsOrFactory: ObservableOrFactory<TSource, TResponse>;
options?: RemoteOptions;
success: (result?: TResponse) => AnyAction | undefined;
emitSuccessOutsideAffectState?: boolean;
emitOnSuccess?: boolean;
optimistic?: boolean;
optimisticResolve?: (
success: boolean,
result?: TResponse,
) => AnyAction | undefined;
setLoadingState?: (loadingState: LoadingState) => void;
}
export interface OptimisticOptions {
optimistic?: boolean;
}
export interface RequestOptions {
rethrow?: boolean;
globalLoadingState?: boolean;
}
export interface RemoteOptions extends OptimisticOptions, RequestOptions {
single?: boolean;
}
================================================
FILE: src/shared/utils.ts
================================================
import { EMPTY, MonoTypeOperatorFunction, Observable, of, OperatorFunction } from 'rxjs';
import { catchError, filter, first, map, switchMap, tap } from 'rxjs/operators';
import { ObservableOrFactory, RemoteOptions, RemotePipeConfig } from './types';
import { isFunction, isNil } from 'lodash-es';
import { AnyAction, CombinedReducers, LoadingState, Reducer } from '../system-types';
import { affectLoadingStateFactory } from '../affect-loading-state/affect-loading-state';
export const getObservable$ = <S, R>(
observableOrFactory: ObservableOrFactory<S, R>,
value: S,
): Observable<R> => isFunction(observableOrFactory)
? observableOrFactory(value)
: observableOrFactory;
export const guardByOptions = <T>(
options?: RemoteOptions,
): MonoTypeOperatorFunction<T | T[]> => (
source: Observable<T | T[]>,
): Observable<T | T[]> => source.pipe(
filter((storeValue) => !options?.single || isNil(storeValue)),
);
export const getRemotePipe = <TSource, TStore, TResponse, TOutput>(
{
store$,
remoteObsOrFactory,
options,
success,
emitSuccessOutsideAffectState,
emitOnSuccess,
optimistic,
optimisticResolve,
setLoadingState,
}: RemotePipeConfig<TSource, TStore, TResponse>,
): OperatorFunction<TSource, TOutput> => {
const trailPipe: OperatorFunction<TResponse, TOutput> = emitOnSuccess
? map((result: TResponse) => result as unknown as TOutput)
: switchMap(() => EMPTY);
return (source: Observable<TSource>): Observable<TOutput> => source.pipe(
switchMap((value: TSource) => {
const remote$ = getObservable$(remoteObsOrFactory, value);
if (optimistic) {
success();
return remote$.pipe(
tap((response) => optimisticResolve?.(true, response)),
catchError((error: any) => {
optimisticResolve?.(false);
setLoadingState?.({ error, isLoading: false });
return EMPTY;
}),
);
}
const successPipe = tap((result: TResponse) => success(result));
const pipesToAffect: OperatorFunction<any, any>[] = [
switchMap(() => remote$),
];
const resultPipes: OperatorFunction<any, any>[] = [];
if (emitSuccessOutsideAffectState) {
resultPipes.push(successPipe);
} else {
pipesToAffect.push(successPipe);
}
if (setLoadingState) {
const rethrow = options?.rethrow ?? false;
const affectStateObsFactory = affectLoadingStateFactory(setLoadingState, rethrow);
const affectPipe = affectStateObsFactory(...pipesToAffect);
resultPipes.unshift(affectPipe);
} else {
resultPipes.unshift(...pipesToAffect);
}
if (store$) {
return (store$ as any).pipe(
first(),
guardByOptions<TSource>(options),
...resultPipes,
) as Observable<TResponse>;
}
return (of(value) as any).pipe(...resultPipes) as Observable<TResponse>;
}),
trailPipe,
);
};
export const guardIfLoading = (
loadingStateObs: Observable<LoadingState | undefined>,
): Observable<LoadingState | undefined> => loadingStateObs.pipe(
first(),
filter((loadingState: LoadingState | undefined) => !loadingState?.isLoading),
);
export const combineReducers = <TState extends object, TAction extends AnyAction>(
reducers: CombinedReducers<TState>,
): Reducer<TState, TAction> => {
const reducerKeys = Object.keys(reducers) as (keyof TState)[];
return (state: TState, action: any) => {
state = state ?? {};
let hasChanged = false;
const nextState: Partial<TState> = {};
reducerKeys.forEach((reducerKey) => {
const subReducer = reducers[reducerKey];
const subState = state[reducerKey];
nextState[reducerKey] = subReducer(subState, action);
hasChanged = hasChanged || nextState[reducerKey] !== subState;
})
return hasChanged ? nextState as TState : state;
};
}
================================================
FILE: src/system-types.ts
================================================
import { Observable } from 'rxjs';
export type Dictionary<T> = Record<string, T>;
export type Id = string | number;
export interface LoadingState<E = string> {
byKeys?: Dictionary<LoadingState>;
isLoading: boolean;
error?: E;
}
export interface RxStore<T = any> extends Observable<T> {
dispatch(action: { type: any }): void;
addReducer(
key: string,
reducer: any,
): void;
}
export interface AnyAction {
type: string;
}
export interface PayloadAction<T = any> {
type: string;
payload: T;
}
export type Reducer<S, A extends AnyAction> = (
state: S,
action: A,
) => S;
export type CombinedReducers<TState> = {
[K in keyof TState]: Reducer<TState[K], any>;
}
export type ActionCreator<TActionTypes, TActions> = (...args: any) => TActions;
export type HashFn = (ob: any) => string;
export type SameShaped<T, V> = {
[K in keyof T]: V
}
type Key = string | number | symbol;
export type StrictDictionary<K extends Key,
V,
K2 extends Key = string,
> = Record<K, V> & Record<K2, V>;
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends Array<infer U>
? Array<DeepPartial<U>>
: T[P] extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: DeepPartial<T[P]>
};
================================================
FILE: src/testing/helpers.ts
================================================
import { cold } from 'jest-marbles';
import { TestStore } from './store';
import { combineReducers } from '../shared/utils';
import { PainlessRedux } from '../painless-redux/types';
import { EntityActionTypes } from '../entity/types';
import { createEntityActionCreators } from '../entity/action-creators';
import { Reducer } from '../system-types';
import { EntityActions } from '../entity/actions';
import { getHash } from '../entity/utils';
export const initStoreWithPr = (
store: TestStore<any>,
pr: PainlessRedux,
) => {
const reducer = pr.getReducer();
const globalReducer = combineReducers({ [pr.name]: reducer });
const state = globalReducer({}, { type: 'any' });
store.setState(state);
return globalReducer;
};
export const getOrderedMarbleStream = (...items: any) => {
const { marble, values } = items.reduce((
memo: any,
item: any,
index: number,
) => {
const isFrames = typeof item === 'string';
const isArray = Array.isArray(item);
let marblePart: string = index.toString();
if (isFrames) {
marblePart = item;
}
if (isArray) {
marblePart = `(${item.map((
subItem: any,
subIndex: number,
) => `${index}${subIndex}`)})`;
}
memo.marble += marblePart;
if (!isFrames) {
if (isArray) {
memo.values = item.reduce((
subMemo: any,
subItem: any,
subIndex: number,
) => {
subMemo[`${index}${subIndex}`] = subItem;
return subMemo;
}, memo.values);
} else {
memo.values[index] = item;
}
}
return memo;
}, { marble: '', values: {} });
return cold(marble, values);
};
export const createTestHelpers = <T, TPageMetadata>(
reducerFactory: <T>(types: EntityActionTypes) => Reducer<any, EntityActions>,
) => {
const types: EntityActionTypes = {
ADD: 'ADD',
RESOLVE_ADD: 'RESOLVE_ADD',
ADD_LIST: 'ADD_LIST',
SET_LOADING_STATE: 'SET_LOADING_STATE',
CHANGE: 'CHANGE',
RESOLVE_CHANGE: 'RESOLVE_CHANGE',
REMOVE: 'REMOVE',
RESOLVE_REMOVE: 'RESOLVE_REMOVE',
RESTORE_REMOVED: 'RESTORE_REMOVED',
REMOVE_LIST: 'REMOVE_LIST',
RESTORE_REMOVED_LIST: 'RESTORE_REMOVED_LIST',
RESOLVE_REMOVE_LIST: 'RESOLVE_REMOVE_LIST',
CLEAR: 'CLEAR',
CLEAR_ALL: 'CLEAR_ALL',
BATCH: 'BATCH',
SET_LOADING_STATES: 'SET_LOADING_STATES',
CHANGE_LIST: 'CHANGE_LIST',
RESOLVE_CHANGE_LIST: 'RESOLVE_CHANGE_LIST',
};
const actionCreators = createEntityActionCreators<T, TPageMetadata>(
types,
{ hashFn: getHash, name: 'test', pageSize: 10, maxPagesCount: 10 },
);
const reducer = reducerFactory<T>(types);
return { actionCreators, reducer };
};
================================================
FILE: src/testing/store.ts
================================================
import { BehaviorSubject, ReplaySubject } from 'rxjs';
import { AnyAction, Reducer, RxStore } from '../system-types';
import { combineReducers } from '../shared/utils';
export class TestStore<T = any> extends BehaviorSubject<T> implements RxStore<T> {
actions$: ReplaySubject<AnyAction> = new ReplaySubject();
state: T;
constructor(
private initialState: T,
private reducer: Reducer<T, AnyAction>,
) {
super(initialState);
this.state = initialState;
}
setState(data: T) {
this.next(data);
}
dispatch(
action: AnyAction,
) {
this.actions$.next(action);
this.performDispatch(action);
}
addReducer(
key: string,
reducer: any,
) {
this.reducer = combineReducers<any, AnyAction>({ [key]: reducer });
this.performDispatch({ type: 'ADD_REDUCER' });
}
clear() {
this.performDispatch({ type: 'TEST_CLEAR' }, this.initialState);
}
private performDispatch(
action: AnyAction,
state = this.state,
) {
this.state = this.reducer(state, action);
this.setState(this.state);
}
}
================================================
FILE: src/utils.ts
================================================
import { MD5 } from 'object-hash';
import { capitalize, defaultsDeep, isNil, isObject, keyBy, lowerCase } from 'lodash-es';
import { Observable, OperatorFunction } from 'rxjs';
import { distinctUntilChanged, map, take } from 'rxjs/operators';
import { DeepPartial, Dictionary, Id } from './system-types';
export const capitalizeAll = (str: string | number | symbol): string =>
lowerCase(str.toString()).split(' ').map((part: string) => capitalize(part)).join(' ');
export const hashString = (value: string): string => MD5(value).toString();
export const hashIt = (value?: unknown): string => {
if (typeof value === 'string') return hashString(value);
return MD5(value ?? {});
};
export const getHeadedActionName = (
header: string,
...rest: string[]
) => {
const actionString = capitalizeAll(rest.join(' '));
const typeString = capitalizeAll(header);
return `[${typeString}] ${actionString}`;
};
export const createActionTypes = <T>(
actionTypeNames: (keyof T)[],
entityName: string,
): T => {
return actionTypeNames.reduce((
memo: any,
actionName: keyof T,
) => {
memo[actionName] = getHeadedActionName(entityName, actionName as string);
return memo;
}, {});
};
export const merge = <T>(
src: T,
patch: DeepPartial<T>,
): T => {
if (isNil(src)) return { ...patch } as T;
const newObject: T = { ...src };
for (const key in patch) {
const srcValue = src[key];
const patchValue = patch[key];
if (patchValue === undefined) {
delete newObject[key];
} else if (isObject(patchValue) && !Array.isArray(patchValue)) {
newObject[key] = merge(srcValue, patchValue as any);
} else {
(newObject[key] as unknown) = patchValue;
}
}
return newObject;
};
export const typedDefaultsDeep = <T>(
obj: Partial<T> | undefined,
...args: Partial<T>[]
): Partial<T> => defaultsDeep({}, obj, ...args) as Partial<T>;
export const snapshot = <T>(obs$: Observable<T>): T | undefined => {
let value;
obs$.pipe(take(1)).subscribe((v) => value = v);
return value;
};
export const select = <T, R>(
selector: (
value: T,
index: number,
) => R,
): OperatorFunction<T, R> => (
source: Observable<T>,
): Observable<R> => source.pipe(
map
gitextract__my3mksh/ ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── package.json ├── src/ │ ├── affect-loading-state/ │ │ ├── affect-loading-state.ts │ │ └── types.ts │ ├── dispatcher/ │ │ ├── dispatcher.ts │ │ └── types.ts │ ├── entity/ │ │ ├── action-creators.ts │ │ ├── action-creators.types.ts │ │ ├── actions.ts │ │ ├── constants.ts │ │ ├── entity.int-spec.ts │ │ ├── entity.spec.ts │ │ ├── entity.ts │ │ ├── methods/ │ │ │ ├── dispatch/ │ │ │ │ ├── dispatch.ts │ │ │ │ └── types.ts │ │ │ ├── mixed/ │ │ │ │ ├── mixed.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils.ts │ │ │ └── select/ │ │ │ ├── select.ts │ │ │ └── types.ts │ │ ├── reducer.ts │ │ ├── reducers/ │ │ │ ├── dictionary.spec.ts │ │ │ ├── dictionary.ts │ │ │ ├── ids.spec.ts │ │ │ ├── ids.ts │ │ │ ├── instance.spec.ts │ │ │ ├── instance.ts │ │ │ ├── loading-state.ts │ │ │ ├── loading-states.ts │ │ │ ├── pages.spec.ts │ │ │ └── pages.ts │ │ ├── selectors.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── index.ts │ ├── painless-redux/ │ │ ├── action-creators.ts │ │ ├── constants.ts │ │ ├── painless-redux.ts │ │ ├── reducers.ts │ │ ├── register.ts │ │ ├── selectors.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── select-manager/ │ │ ├── select-manager.ts │ │ └── types.ts │ ├── shared/ │ │ ├── change/ │ │ │ ├── actions.ts │ │ │ ├── reducer.spec.ts │ │ │ ├── reducer.ts │ │ │ ├── selectors.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── loading-state/ │ │ │ ├── actions.ts │ │ │ ├── reducers.spec.ts │ │ │ ├── reducers.ts │ │ │ ├── selectors.ts │ │ │ └── types.ts │ │ ├── system/ │ │ │ ├── actions.ts │ │ │ ├── reducers.ts │ │ │ ├── system.ts │ │ │ └── types.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── system-types.ts │ ├── testing/ │ │ ├── helpers.ts │ │ └── store.ts │ ├── utils.ts │ └── workspace/ │ ├── action-creators.ts │ ├── actions.ts │ ├── constants.ts │ ├── methods/ │ │ ├── dispatch/ │ │ │ ├── dispatch.ts │ │ │ └── types.ts │ │ ├── mixed/ │ │ │ ├── mixed.ts │ │ │ └── types.ts │ │ └── select/ │ │ ├── select.ts │ │ └── types.ts │ ├── reducer.ts │ ├── selectors.ts │ ├── types.ts │ ├── utils.ts │ ├── workspace.spec.ts │ └── workspace.ts └── tsconfig.json
SYMBOL INDEX (125 symbols across 33 files)
FILE: src/affect-loading-state/types.ts
type AffectStateSetter (line 4) | interface AffectStateSetter<T = any, E = any> {
type AffectLoadingStateFactory (line 12) | interface AffectLoadingStateFactory {
FILE: src/dispatcher/types.ts
type Dispatcher (line 1) | interface Dispatcher<TActionTypes, TActions> {
FILE: src/entity/action-creators.types.ts
type EntityActionCreators (line 15) | interface EntityActionCreators<T, TPageMetadata> {
FILE: src/entity/actions.ts
type SelfActionCreators (line 217) | type SelfActionCreators = ReturnType<typeof createAdd>
type EntityActions (line 235) | type EntityActions = ReturnType<SelfActionCreators> | SystemActions;
FILE: src/entity/constants.ts
constant DEFAULT_PAGE_SIZE (line 3) | const DEFAULT_PAGE_SIZE = 300;
constant MAX_PAGES_COUNT (line 4) | const MAX_PAGES_COUNT = Infinity;
constant ENTITY_TYPE_NAMES (line 5) | const ENTITY_TYPE_NAMES: Array<keyof EntityActionTypes> = [
FILE: src/entity/entity.int-spec.ts
type TestEntity (line 15) | type TestEntity = {
type TPageMetadata (line 22) | type TPageMetadata = any;
FILE: src/entity/entity.spec.ts
type TestEntity (line 17) | interface TestEntity {
FILE: src/entity/methods/dispatch/types.ts
type DispatchEntityMethods (line 15) | interface DispatchEntityMethods<T, TPageMetadata> {
FILE: src/entity/methods/mixed/types.ts
type MixedEntityMethods (line 19) | interface MixedEntityMethods<T, TPageMetadata> {
FILE: src/entity/methods/select/types.ts
type SelectEntityMethods (line 5) | interface SelectEntityMethods<T, TPageMetadata> {
FILE: src/entity/reducers/dictionary.spec.ts
type TestEntity (line 5) | type TestEntity = {
type TPageMetadata (line 13) | type TPageMetadata = any;
FILE: src/entity/types.ts
type EntityType (line 19) | type EntityType<T> = T & { id: Id };
type EntitySchema (line 21) | interface EntitySchema<T> {
type EntityLoadOptions (line 30) | interface EntityLoadOptions extends EntityInsertOptions, RequestOptions {
type EntityGetOptions (line 33) | interface EntityGetOptions extends EntityAddOptions {
type EntityGetListOptions (line 36) | interface EntityGetListOptions extends EntityLoadListOptions {
type EntityLoadListOptions (line 39) | interface EntityLoadListOptions extends EntityAddListOptions, RequestOpt...
type EntityAddOptions (line 43) | interface EntityAddOptions extends EntityOptimisticOptions, EntityInsert...
type EntityInternalOptions (line 46) | interface EntityInternalOptions {
type EntityInternalAddOptions (line 50) | interface EntityInternalAddOptions extends EntityAddOptions, EntityInter...
type EntityInternalAddListOptions (line 53) | interface EntityInternalAddListOptions extends EntityAddListOptions, Ent...
type EntityOptimisticOptions (line 56) | interface EntityOptimisticOptions {
type EntityRemoveOptions (line 60) | interface EntityRemoveOptions extends EntityOptimisticOptions, RequestOp...
type EntityRemoveListOptions (line 64) | interface EntityRemoveListOptions extends EntityOptimisticOptions, Reque...
type EntitySetLoadingStateOptions (line 68) | interface EntitySetLoadingStateOptions extends LoadingStateSetOptions {
type EntityInternalSetLoadingStateOptions (line 71) | interface EntityInternalSetLoadingStateOptions extends EntitySetLoadingS...
type EntityInsertOptions (line 74) | interface EntityInsertOptions {
type EntityAddListOptions (line 80) | interface EntityAddListOptions extends EntityInsertOptions {
type Pagination (line 83) | interface Pagination {
type ResponseArray (line 90) | interface ResponseArray<T, TPageMetadata> {
type PaginatedResponse (line 96) | interface PaginatedResponse<T, TPageMetadata> extends Pagination {
type Response$Factory (line 100) | type Response$Factory<T, TPageMetadata> = (pagination: Pagination) => Ob...
type Page (line 102) | interface Page<TPageMetadata> {
type IdPatchRequest (line 110) | interface IdPatchRequest<T> {
type IdPatch (line 115) | interface IdPatch<T> {
type EntityInstanceState (line 120) | interface EntityInstanceState<T> extends ChangeableState<EntityType<T>> {
type EntityState (line 124) | interface EntityState<T, TPageMetadata> extends LoadingStateState {
type IdsSelector (line 131) | type IdsSelector<T, TPageMetadata> = Selector<EntityState<T, TPageMetada...
type DictionarySelector (line 132) | type DictionarySelector<T, TPageMetadata> = Selector<EntityState<T, TPag...
type PagesSelector (line 133) | type PagesSelector<T, TPageMetadata> = Selector<EntityState<T, TPageMeta...
type PagesListSelector (line 134) | type PagesListSelector<T, TPageMetadata> = Selector<EntityState<T, TPage...
type PageSelector (line 135) | type PageSelector<T, TPageMetadata> = Selector<EntityState<T, TPageMetad...
type LoadingStatesSelector (line 136) | type LoadingStatesSelector<T, TPageMetadata> = Selector<EntityState<T, T...
type ActualSelector (line 137) | type ActualSelector<T, TPageMetadata> = Selector<EntityState<T, TPageMet...
type ListSelector (line 138) | type ListSelector<T, TPageMetadata> = Selector<EntityState<T, TPageMetad...
type BaseEntitySelectors (line 140) | interface BaseEntitySelectors<T, TPageMetadata> extends LoadingStateSele...
type EntitySelectors (line 149) | interface EntitySelectors<T, TPageMetadata> extends BaseEntitySelectors<...
type EntityActionTypes (line 161) | interface EntityActionTypes extends SystemActionTypes {
type PublicDispatchEntityMethods (line 181) | type PublicDispatchEntityMethods<T, TPageMetadata> = Omit<DispatchEntity...
type PublicSelectEntityMethods (line 183) | type PublicSelectEntityMethods<T, TPageMetadata> = Omit<SelectEntityMeth...
type Entity (line 185) | interface Entity<T, TPageMetadata = void> extends PublicSelectEntityMeth...
FILE: src/painless-redux/constants.ts
constant SYSTEM_TYPE_NAMES (line 3) | const SYSTEM_TYPE_NAMES: Array<keyof SystemActionTypes> = [];
FILE: src/painless-redux/selectors.ts
constant SLOTS_SELECTOR_FACTORIES (line 21) | const SLOTS_SELECTOR_FACTORIES: StrictDictionary<SlotTypes, any> = {
FILE: src/painless-redux/types.ts
type SlotTypes (line 14) | enum SlotTypes {
type PainlessReduxSchema (line 19) | interface PainlessReduxSchema {
type PainlessReduxRegister (line 27) | type PainlessReduxRegister = StrictDictionary<SlotTypes, Dictionary<Redu...
type PainlessReduxState (line 29) | type PainlessReduxState = Dictionary<any>;
type PainlessRedux (line 31) | type PainlessRedux = {
FILE: src/select-manager/types.ts
type SelectManager (line 4) | interface SelectManager {
FILE: src/shared/change/actions.ts
type SelfActionCreators (line 26) | type SelfActionCreators = ReturnType<typeof createChange>
type ChangeActions (line 29) | type ChangeActions = ReturnType<SelfActionCreators>
FILE: src/shared/change/types.ts
type ChangeOptions (line 4) | interface ChangeOptions extends RequestOptions {
type ChangeActionTypes (line 11) | interface ChangeActionTypes {
type Change (line 16) | interface Change<T> {
type ChangeableState (line 23) | interface ChangeableState<T> {
type PatchRequest (line 28) | type PatchRequest<T> = DeepPartial<T> | ((value: DeepPartial<T> | undefi...
FILE: src/shared/loading-state/actions.ts
type LoadingStateActions (line 15) | type LoadingStateActions = ReturnType<ReturnType<typeof createSetLoading...
FILE: src/shared/loading-state/types.ts
type LoadingStateState (line 4) | interface LoadingStateState {
type LoadingStateSetOptions (line 8) | interface LoadingStateSetOptions {
type LoadingStateActionTypes (line 12) | interface LoadingStateActionTypes {
type LoadingStateSelector (line 16) | type LoadingStateSelector<T> = Selector<T, LoadingState | undefined>
type LoadingStateSelectors (line 18) | interface LoadingStateSelectors<T> {
FILE: src/shared/system/actions.ts
type SystemActions (line 8) | type SystemActions = ReturnType<ReturnType<typeof createBatch>>;
FILE: src/shared/system/types.ts
type SystemActionTypes (line 1) | interface SystemActionTypes {
FILE: src/shared/types.ts
type ObservableOrFactory (line 4) | type ObservableOrFactory<S, R> = (Observable<R>) | ((value: S) => Observ...
type RemotePipeConfig (line 6) | interface RemotePipeConfig<TSource, TStore, TResponse> {
type OptimisticOptions (line 21) | interface OptimisticOptions {
type RequestOptions (line 25) | interface RequestOptions {
type RemoteOptions (line 30) | interface RemoteOptions extends OptimisticOptions, RequestOptions {
FILE: src/system-types.ts
type Dictionary (line 3) | type Dictionary<T> = Record<string, T>;
type Id (line 5) | type Id = string | number;
type LoadingState (line 7) | interface LoadingState<E = string> {
type RxStore (line 13) | interface RxStore<T = any> extends Observable<T> {
type AnyAction (line 22) | interface AnyAction {
type PayloadAction (line 26) | interface PayloadAction<T = any> {
type Reducer (line 31) | type Reducer<S, A extends AnyAction> = (
type CombinedReducers (line 36) | type CombinedReducers<TState> = {
type ActionCreator (line 40) | type ActionCreator<TActionTypes, TActions> = (...args: any) => TActions;
type HashFn (line 42) | type HashFn = (ob: any) => string;
type SameShaped (line 44) | type SameShaped<T, V> = {
type Key (line 48) | type Key = string | number | symbol;
type StrictDictionary (line 49) | type StrictDictionary<K extends Key,
type DeepPartial (line 54) | type DeepPartial<T> = {
FILE: src/testing/store.ts
class TestStore (line 5) | class TestStore<T = any> extends BehaviorSubject<T> implements RxStore<T> {
method constructor (line 9) | constructor(
method setState (line 17) | setState(data: T) {
method dispatch (line 21) | dispatch(
method addReducer (line 28) | addReducer(
method clear (line 36) | clear() {
method performDispatch (line 40) | private performDispatch(
FILE: src/workspace/action-creators.ts
type WorkspaceActionCreators (line 15) | type WorkspaceActionCreators = ReturnType<typeof createWorkspaceActionCr...
FILE: src/workspace/actions.ts
type SelfActions (line 31) | type SelfActions = ReturnType<typeof createChange>
type WorkspaceChangeAction (line 34) | type WorkspaceChangeAction = ReturnType<SelfActions>
type WorkspaceActions (line 36) | type WorkspaceActions = ReturnType<SelfActions> | LoadingStateActions | ...
FILE: src/workspace/constants.ts
constant WORKSPACE_TYPE_NAMES (line 3) | const WORKSPACE_TYPE_NAMES: (keyof WorkspaceActionTypes)[] = [
FILE: src/workspace/methods/dispatch/types.ts
type DispatchWorkspaceMethods (line 6) | interface DispatchWorkspaceMethods<T> {
FILE: src/workspace/methods/mixed/types.ts
type MixedWorkspaceMethods (line 5) | interface MixedWorkspaceMethods<T> {
FILE: src/workspace/methods/select/types.ts
type SelectWorkspaceMethods (line 4) | interface SelectWorkspaceMethods<T> {
FILE: src/workspace/types.ts
type PublicDispatchWorkspaceMethods (line 10) | type PublicDispatchWorkspaceMethods<T> = Omit<DispatchWorkspaceMethods<T...
type Workspace (line 12) | interface Workspace<T> extends PublicDispatchWorkspaceMethods<T>, Select...
type WorkspaceState (line 16) | interface WorkspaceState<T> extends LoadingStateState {
type WorkspaceSchema (line 20) | interface WorkspaceSchema<T> {
type WorkspaceActionTypes (line 25) | interface WorkspaceActionTypes extends SystemActionTypes {
type ValueSelector (line 31) | type ValueSelector<T> = Selector<WorkspaceState<T>, T | undefined>;
type WorkspaceSelectors (line 33) | interface WorkspaceSelectors<T> extends LoadingStateSelectors<WorkspaceS...
type BooleanMap (line 37) | type BooleanMap<T> = {
type SelectValue (line 42) | type SelectValue<T, M> = M extends boolean ? T : SelectResult<T, M>;
type SelectResult (line 43) | type SelectResult<T, M extends BooleanMap<T>> = { [K in (keyof M & keyof...
FILE: src/workspace/workspace.spec.ts
type TestWorkspace (line 10) | interface TestWorkspace {
Condensed preview — 87 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (224K chars).
[
{
"path": ".eslintignore",
"chars": 13,
"preview": "**/*.spec.ts\n"
},
{
"path": ".eslintrc.json",
"chars": 1255,
"preview": "{\n // Настройки проекта\n \"env\": {\n // Проект для браузера\n \"browser\": true,\n // Включаем возможности ES6\n "
},
{
"path": ".gitignore",
"chars": 55,
"preview": "dist\r\nnode_modules\r\n.idea\r\n/compiled\r\n*.metadata.json\r\n"
},
{
"path": ".npmignore",
"chars": 46,
"preview": "node_modules\r\n.idea\r\n/compiled\r\n/src/testing\r\n"
},
{
"path": "LICENSE",
"chars": 1069,
"preview": "MIT License\n\nCopyright (c) 2019 Egor Grushin\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
},
{
"path": "README.md",
"chars": 5801,
"preview": "# painless-redux\nReducers-actions-selectors free reactive state management in redux-way\n\n# Overview\nThis package allows "
},
{
"path": "package.json",
"chars": 1804,
"preview": "{\n \"name\": \"painless-redux\",\n \"version\": \"4.1.17\",\n \"description\": \"Reducers-actions-selectors free reactive state ma"
},
{
"path": "src/affect-loading-state/affect-loading-state.ts",
"chars": 1832,
"preview": "import { EMPTY, Observable, of, OperatorFunction, throwError } from 'rxjs';\nimport { catchError, finalize, switchMap, ta"
},
{
"path": "src/affect-loading-state/types.ts",
"chars": 527,
"preview": "import { LoadingState } from '../system-types';\nimport { Observable, OperatorFunction } from 'rxjs';\n\nexport interface A"
},
{
"path": "src/dispatcher/dispatcher.ts",
"chars": 1154,
"preview": "import { ActionCreator, AnyAction, RxStore, SameShaped } from '../system-types';\nimport { Dispatcher } from './types';\n\n"
},
{
"path": "src/dispatcher/types.ts",
"chars": 219,
"preview": "export interface Dispatcher<TActionTypes, TActions> {\n dispatch(action: TActions): void;\n\n createAndDispatch(\n "
},
{
"path": "src/entity/action-creators.ts",
"chars": 1758,
"preview": "import { EntityActionTypes, EntitySchema } from './types';\nimport {\n createAdd,\n createAddList,\n createChange,\n"
},
{
"path": "src/entity/action-creators.types.ts",
"chars": 4380,
"preview": "import {\n EntityAddOptions,\n EntityInternalAddListOptions,\n EntityInternalAddOptions,\n EntityInternalSetLoad"
},
{
"path": "src/entity/actions.ts",
"chars": 7844,
"preview": "import {\n EntityActionTypes,\n EntityAddOptions,\n EntityInternalAddListOptions,\n EntityInternalAddOptions,\n "
},
{
"path": "src/entity/constants.ts",
"chars": 550,
"preview": "import { EntityActionTypes } from './types';\n\nexport const DEFAULT_PAGE_SIZE = 300;\nexport const MAX_PAGES_COUNT = Infin"
},
{
"path": "src/entity/entity.int-spec.ts",
"chars": 15920,
"preview": "import {Id} from '../system-types';\nimport {Entity, EntityAddOptions, EntityRemoveOptions, Page} from './types';\nimport "
},
{
"path": "src/entity/entity.spec.ts",
"chars": 20534,
"preview": "import { Id, LoadingState } from '../system-types';\nimport { getOrderedMarbleStream, initStoreWithPr } from '../testing/"
},
{
"path": "src/entity/entity.ts",
"chars": 2182,
"preview": "import { Entity, EntityActionTypes, EntitySchema, EntityState } from './types';\nimport { createEntitySelectors } from '."
},
{
"path": "src/entity/methods/dispatch/dispatch.ts",
"chars": 9960,
"preview": "import { Dispatcher } from '../../../dispatcher/types';\nimport {\n EntityActionTypes,\n EntityAddListOptions,\n En"
},
{
"path": "src/entity/methods/dispatch/types.ts",
"chars": 3940,
"preview": "import {\n EntityAddListOptions,\n EntityAddOptions,\n EntityRemoveListOptions,\n EntityRemoveOptions,\n Entit"
},
{
"path": "src/entity/methods/mixed/mixed.ts",
"chars": 13404,
"preview": "import {\n EntityAddOptions,\n EntityGetListOptions,\n EntityGetOptions,\n EntityLoadListOptions,\n EntityLoad"
},
{
"path": "src/entity/methods/mixed/types.ts",
"chars": 2493,
"preview": "import {\n EntityAddOptions,\n EntityGetListOptions,\n EntityGetOptions,\n EntityLoadListOptions,\n EntityLoad"
},
{
"path": "src/entity/methods/mixed/utils.ts",
"chars": 2502,
"preview": "import { BehaviorSubject, EMPTY, merge, Observable, of } from 'rxjs';\nimport { EntityGetListOptions, EntitySchema, Page,"
},
{
"path": "src/entity/methods/select/select.ts",
"chars": 2719,
"preview": "import { EntitySelectors } from '../../types';\nimport { Observable } from 'rxjs';\nimport { SelectManager } from '../../."
},
{
"path": "src/entity/methods/select/types.ts",
"chars": 1070,
"preview": "import { Observable } from 'rxjs';\nimport { Dictionary, Id, LoadingState } from '../../../system-types';\nimport { Page }"
},
{
"path": "src/entity/reducer.ts",
"chars": 3435,
"preview": "import { EntityActionTypes, EntitySchema, EntityState } from './types';\nimport { Reducer } from '../system-types';\nimpor"
},
{
"path": "src/entity/reducers/dictionary.spec.ts",
"chars": 5089,
"preview": "import { Id } from '../../system-types';\nimport { createTestHelpers } from '../../testing/helpers';\nimport { createDicti"
},
{
"path": "src/entity/reducers/dictionary.ts",
"chars": 4233,
"preview": "import { Dictionary } from '../../system-types';\nimport { createAddByHash, createResolveChange, EntityActions } from '.."
},
{
"path": "src/entity/reducers/ids.spec.ts",
"chars": 2783,
"preview": "import { createIdsReducer } from './ids';\nimport { createTestHelpers } from '../../testing/helpers';\n\nconst {\n reduce"
},
{
"path": "src/entity/reducers/ids.ts",
"chars": 2643,
"preview": "import { EntityActionTypes, EntityInsertOptions } from '../types';\nimport { EntityActions } from '../actions';\nimport { "
},
{
"path": "src/entity/reducers/instance.spec.ts",
"chars": 1507,
"preview": "import { createTestHelpers } from '../../testing/helpers';\nimport { createInstanceReducer } from './instance';\nimport { "
},
{
"path": "src/entity/reducers/instance.ts",
"chars": 2923,
"preview": "import { EntityActionTypes, EntityInstanceState, EntityType } from '../types';\nimport { EntityActions } from '../actions"
},
{
"path": "src/entity/reducers/loading-state.ts",
"chars": 700,
"preview": "import { LoadingState } from '../../system-types';\nimport { createLoadingStateReducer } from '../../shared/loading-state"
},
{
"path": "src/entity/reducers/loading-states.ts",
"chars": 2190,
"preview": "import { EntityActionTypes } from '../types';\nimport { Dictionary, Id, LoadingState } from '../../system-types';\nimport "
},
{
"path": "src/entity/reducers/pages.spec.ts",
"chars": 1487,
"preview": "import { createPagesReducer } from './pages';\nimport { EntityActions } from '../actions';\nimport { createTestHelpers } f"
},
{
"path": "src/entity/reducers/pages.ts",
"chars": 7475,
"preview": "import { EntityActionTypes, EntityType, Page } from '../types';\nimport { Dictionary } from '../../system-types';\nimport "
},
{
"path": "src/entity/selectors.ts",
"chars": 7634,
"preview": "import { createSelector, Selector } from 'reselect';\nimport {\n ActualSelector,\n BaseEntitySelectors,\n Dictionar"
},
{
"path": "src/entity/types.ts",
"chars": 6943,
"preview": "import {Observable} from 'rxjs';\nimport {Selector} from 'reselect';\nimport {DeepPartial, Dictionary, HashFn, Id, Loading"
},
{
"path": "src/entity/utils.ts",
"chars": 1521,
"preview": "import { createActionTypes, hashIt, typedDefaultsDeep } from '../utils';\nimport {\n EntityActionTypes,\n EntitySchem"
},
{
"path": "src/index.ts",
"chars": 943,
"preview": "export * from './painless-redux/painless-redux';\nexport * from './painless-redux/types';\nexport * from './shared/types';"
},
{
"path": "src/painless-redux/action-creators.ts",
"chars": 0,
"preview": ""
},
{
"path": "src/painless-redux/constants.ts",
"chars": 130,
"preview": "import { SystemActionTypes } from '../shared/system/types';\n\nexport const SYSTEM_TYPE_NAMES: Array<keyof SystemActionTyp"
},
{
"path": "src/painless-redux/painless-redux.ts",
"chars": 2736,
"preview": "import { PainlessRedux, PainlessReduxSchema, SlotTypes } from './types';\nimport { ActionCreator, AnyAction, Reducer, RxS"
},
{
"path": "src/painless-redux/reducers.ts",
"chars": 1211,
"preview": "import { PainlessReduxRegister, PainlessReduxSchema, PainlessReduxState, SlotTypes } from './types';\nimport { AnyAction,"
},
{
"path": "src/painless-redux/register.ts",
"chars": 932,
"preview": "import { PainlessReduxRegister, SlotTypes } from './types';\nimport { isNil } from 'lodash-es';\nimport { AnyAction, Reduc"
},
{
"path": "src/painless-redux/selectors.ts",
"chars": 1401,
"preview": "import { PainlessReduxSchema, PainlessReduxState, SlotTypes } from './types';\nimport { createSelector, Selector } from '"
},
{
"path": "src/painless-redux/types.ts",
"chars": 1280,
"preview": "import { Selector } from 'reselect';\nimport {\n ActionCreator,\n AnyAction,\n Dictionary,\n PayloadAction,\n R"
},
{
"path": "src/painless-redux/utils.ts",
"chars": 284,
"preview": "import { SystemActionTypes } from '../shared/system/types';\nimport { createActionTypes } from '../utils';\nimport { SYSTE"
},
{
"path": "src/select-manager/select-manager.ts",
"chars": 879,
"preview": "import { RxStore } from '../system-types';\nimport { asapScheduler, Observable } from 'rxjs';\nimport { subscribeOn } from"
},
{
"path": "src/select-manager/types.ts",
"chars": 247,
"preview": "import { Selector } from 'reselect';\nimport { Observable } from 'rxjs';\n\nexport interface SelectManager {\n select$<T,"
},
{
"path": "src/shared/change/actions.ts",
"chars": 1012,
"preview": "import { ChangeActionTypes, ChangeOptions } from './types';\nimport { typedDefaultsDeep } from '../../utils';\nimport { De"
},
{
"path": "src/shared/change/reducer.spec.ts",
"chars": 1668,
"preview": "import { ChangeActionTypes } from './types';\nimport { createChangeReducer } from './reducer';\nimport { createChange } fr"
},
{
"path": "src/shared/change/reducer.ts",
"chars": 1641,
"preview": "import { ChangeableState, ChangeActionTypes } from './types';\nimport { ChangeActions } from './actions';\nimport { create"
},
{
"path": "src/shared/change/selectors.ts",
"chars": 283,
"preview": "import { getMergedChanges } from './utils';\nimport { ChangeableState } from './types';\n\nexport const getChangeableActual"
},
{
"path": "src/shared/change/types.ts",
"chars": 662,
"preview": "import { DeepPartial } from '../../system-types';\nimport { RequestOptions } from '../types';\n\nexport interface ChangeOpt"
},
{
"path": "src/shared/change/utils.ts",
"chars": 2729,
"preview": "import { DeepPartial } from '../../system-types';\nimport { Change, ChangeableState, ChangeOptions, PatchRequest } from '"
},
{
"path": "src/shared/loading-state/actions.ts",
"chars": 582,
"preview": "import { LoadingStateActionTypes, LoadingStateSetOptions } from './types';\nimport { LoadingState } from '../../system-ty"
},
{
"path": "src/shared/loading-state/reducers.spec.ts",
"chars": 2045,
"preview": "import { createLoadingStateReducer, loadingStateReducer } from './reducers';\nimport { LoadingState } from '../../system-"
},
{
"path": "src/shared/loading-state/reducers.ts",
"chars": 1370,
"preview": "import { Dictionary, LoadingState } from '../../system-types';\nimport { LoadingStateActionTypes } from './types';\nimport"
},
{
"path": "src/shared/loading-state/selectors.ts",
"chars": 304,
"preview": "import { createSelector, Selector } from 'reselect';\nimport { LoadingStateState } from './types';\n\nexport const createLo"
},
{
"path": "src/shared/loading-state/types.ts",
"chars": 465,
"preview": "import { LoadingState } from '../../system-types';\nimport { Selector } from 'reselect';\n\nexport interface LoadingStateSt"
},
{
"path": "src/shared/system/actions.ts",
"chars": 287,
"preview": "import { SystemActionTypes } from './types';\n\nexport const createBatch = <T>(types: SystemActionTypes) => (actions: T[])"
},
{
"path": "src/shared/system/reducers.ts",
"chars": 485,
"preview": "import { AnyAction, Reducer } from '../../system-types';\nimport { SystemActionTypes } from './types';\n\nexport const batc"
},
{
"path": "src/shared/system/system.ts",
"chars": 0,
"preview": ""
},
{
"path": "src/shared/system/types.ts",
"chars": 59,
"preview": "export interface SystemActionTypes {\n BATCH: 'BATCH';\n}\n"
},
{
"path": "src/shared/types.ts",
"chars": 970,
"preview": "import { AnyAction, LoadingState } from '../system-types';\nimport { Observable } from 'rxjs';\n\nexport type ObservableOrF"
},
{
"path": "src/shared/utils.ts",
"chars": 4355,
"preview": "import { EMPTY, MonoTypeOperatorFunction, Observable, of, OperatorFunction } from 'rxjs';\nimport { catchError, filter, f"
},
{
"path": "src/system-types.ts",
"chars": 1312,
"preview": "import { Observable } from 'rxjs';\n\nexport type Dictionary<T> = Record<string, T>;\n\nexport type Id = string | number;\n\ne"
},
{
"path": "src/testing/helpers.ts",
"chars": 3016,
"preview": "import { cold } from 'jest-marbles';\nimport { TestStore } from './store';\nimport { combineReducers } from '../shared/uti"
},
{
"path": "src/testing/store.ts",
"chars": 1224,
"preview": "import { BehaviorSubject, ReplaySubject } from 'rxjs';\r\nimport { AnyAction, Reducer, RxStore } from '../system-types';\r\n"
},
{
"path": "src/utils.ts",
"chars": 3083,
"preview": "import { MD5 } from 'object-hash';\nimport { capitalize, defaultsDeep, isNil, isObject, keyBy, lowerCase } from 'lodash-e"
},
{
"path": "src/workspace/action-creators.ts",
"chars": 626,
"preview": "import { WorkspaceActionTypes } from './types';\nimport { createChange, createResolveChange } from './actions';\nimport { "
},
{
"path": "src/workspace/actions.ts",
"chars": 1326,
"preview": "import * as changeActions from '../shared/change/actions';\nimport { LoadingStateActions } from '../shared/loading-state/"
},
{
"path": "src/workspace/constants.ts",
"chars": 196,
"preview": "import { WorkspaceActionTypes } from './types';\n\nexport const WORKSPACE_TYPE_NAMES: (keyof WorkspaceActionTypes)[] = [\n "
},
{
"path": "src/workspace/methods/dispatch/dispatch.ts",
"chars": 2180,
"preview": "import { Dispatcher } from '../../../dispatcher/types';\nimport { WorkspaceActions } from '../../actions';\nimport { Works"
},
{
"path": "src/workspace/methods/dispatch/types.ts",
"chars": 1026,
"preview": "import { DeepPartial, Id, LoadingState } from '../../../system-types';\nimport { ChangeOptions, PatchRequest } from '../."
},
{
"path": "src/workspace/methods/mixed/mixed.ts",
"chars": 2546,
"preview": "import { DeepPartial, LoadingState } from '../../../system-types';\nimport { Observable } from 'rxjs';\nimport { ChangeOpt"
},
{
"path": "src/workspace/methods/mixed/types.ts",
"chars": 418,
"preview": "import { DeepPartial } from '../../../system-types';\nimport { ChangeOptions, PatchRequest } from '../../../shared/change"
},
{
"path": "src/workspace/methods/select/select.ts",
"chars": 1191,
"preview": "import { SelectManager } from '../../../select-manager/types';\nimport { WorkspaceSelectors } from '../../types';\nimport "
},
{
"path": "src/workspace/methods/select/types.ts",
"chars": 419,
"preview": "import { Observable } from 'rxjs';\nimport { LoadingState } from '../../../system-types';\n\nexport interface SelectWorkspa"
},
{
"path": "src/workspace/reducer.ts",
"chars": 1070,
"preview": "import { WorkspaceActionTypes, WorkspaceState } from './types';\nimport { Reducer } from '../system-types';\nimport { comb"
},
{
"path": "src/workspace/selectors.ts",
"chars": 892,
"preview": "import { createSelector, Selector } from 'reselect';\nimport { PainlessReduxState } from '../painless-redux/types';\nimpor"
},
{
"path": "src/workspace/types.ts",
"chars": 1725,
"preview": "import { ChangeableState } from '../shared/change/types';\nimport { LoadingStateActionTypes, LoadingStateSelectors, Loadi"
},
{
"path": "src/workspace/utils.ts",
"chars": 1321,
"preview": "import { createActionTypes, typedDefaultsDeep } from '../utils';\nimport { WORKSPACE_TYPE_NAMES } from './constants';\nimp"
},
{
"path": "src/workspace/workspace.spec.ts",
"chars": 4098,
"preview": "import 'jest';\nimport { cold } from 'jest-marbles';\nimport { Workspace } from './types';\nimport { TestStore } from '../t"
},
{
"path": "src/workspace/workspace.ts",
"chars": 1922,
"preview": "import { PainlessRedux, SlotTypes } from '../painless-redux/types';\nimport { Workspace, WorkspaceActionTypes, WorkspaceS"
},
{
"path": "tsconfig.json",
"chars": 407,
"preview": "{\r\n \"compilerOptions\": {\r\n \"target\": \"ES2015\",\r\n \"module\": \"ES2020\",\r\n \"declaration\": true,\r\n \"outDir\": \"./"
}
]
About this extraction
This page contains the full source code of the egorgrushin/painless-redux GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 87 files (207.6 KB), approximately 49.5k tokens, and a symbol index with 125 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.