import { EntitySliceState } from '../../factories/create-entity-slice/CreateEntitySlice'; import { Comparer, EntityId, SelectIdMethod } from '../../types'; import cloneDeep from 'lodash.clonedeep'; import { createRecordFromArray } from '../../utils'; export type Update = { id: EntityId; changes: Partial; }; const createDefaultSelectId = (key: keyof TEntity): SelectIdMethod => (entity: TEntity) => { const value = entity[key]; if (!value) { throw new Error(`Missing required property: ${key}`); } if (typeof value !== 'string' && typeof value !== 'number') { throw new Error(`property: ${key} is not of type string or number`); } return value; }; type EntityAdapterProps = { selectId: SelectIdMethod | keyof TEntity; sortComparer: Comparer; }; type EntityAdapterAction< TEntity extends object, TArguments extends any[] = undefined[], TSliceState extends EntitySliceState = EntitySliceState> = (state: TSliceState, ...args: TArguments) => TSliceState; export type EntitySliceActions> = { addOne: EntityAdapterAction; addMany: EntityAdapterAction; updateOne: EntityAdapterAction], TSliceState>; updateMany: EntityAdapterAction[]], TSliceState>; upsertOne: EntityAdapterAction; upsertMany: EntityAdapterAction; removeOne: EntityAdapterAction; removeMany: EntityAdapterAction; removeAll: EntityAdapterAction; setAll: EntityAdapterAction; }; const createEntityAdapter = = EntitySliceState>(options: EntityAdapterProps) => { const selectId: SelectIdMethod = typeof options.selectId === 'string' || typeof options.selectId === 'number' || typeof options.selectId === 'symbol' ? createDefaultSelectId(options.selectId) : options.selectId; const merge = (entities: TEntity[], state: TSliceState) => { const newState = cloneDeep(state); entities.forEach(entity => { newState.entities[selectId(entity)] = entity; }); const allEntities = Object.values(newState.entities).sort(options.sortComparer); newState.ids = allEntities.map(selectId); newState.entities = createRecordFromArray(allEntities, selectId); return newState; }; const addMany: EntityAdapterAction = (state, entities) => { const newEntities = entities.filter(entity => !(selectId(entity) in state.entities)); return newEntities.length !== 0 ? merge(newEntities, state) : state; }; const addOne: EntityAdapterAction = (state, entity) => addMany(state, [entity]); const removeAll: EntityAdapterAction = (state) => { const newState = cloneDeep(state); newState.ids = []; newState.entities = {}; return newState; }; const removeMany: EntityAdapterAction = (state, ids) => { const newState = cloneDeep(state); ids.forEach(id => { if (id in newState.entities) { delete newState.entities[id]; } }); newState.ids = Object.keys(newState.entities); return newState; }; const removeOne: EntityAdapterAction = (state, id) => removeMany(state, [id]); const setAll: EntityAdapterAction = (state, entities) => addMany(removeAll(state), entities); const updateMany: EntityAdapterAction[]], TSliceState> = (state, updates) => { const newState = cloneDeep(state); const updatedEntities = updates.reduce((acc, update) => { if (update.id in newState.entities) { const original = newState.entities[update.id]; const updated: TEntity = Object.assign({}, original, update.changes); delete newState.entities[update.id]; // Side Effect Bad return [...acc, updated]; } return acc; }, [] as TEntity[]); return merge(updatedEntities, newState); }; const updateOne: EntityAdapterAction], TSliceState> = (state, update) => updateMany(state, [update]); const upsertMany: EntityAdapterAction = (state, entities) => { const { added, updated } = entities.reduce((acc, entity) => !state.entities?.[selectId(entity)] ? { ...acc, added: [...acc.added, entity], } : { ...acc, updated: [...acc.updated, { id: selectId(entity), changes: entity, }], }, { added: [] as TEntity[], updated: [] as Update[], }); return addMany(updateMany(state, updated), added); }; const upsertOne: EntityAdapterAction = (state, entity) => upsertMany(state, [entity]); const actions: EntitySliceActions = { addMany: addMany, addOne: addOne, removeAll: removeAll, removeMany, removeOne, setAll: setAll, updateMany: updateMany, updateOne: updateOne, upsertMany: upsertMany, upsertOne: upsertOne, }; return actions; }; export default createEntityAdapter;