import { ActionTypes , AnyAction , AttachAction , BatchAction , CreateAction , DeleteAction , DerivedAction , DetachAction , EntitiesByType , Entity , Id , IdsByType , InvalidAction , MoveAction , MoveAttachedAction , SetStateAction , SingularAction , SortAction , SortAttachedAction , State , UpdateAction , Reducer , } from './interfaces' ; import { ModelSchemaReader } from './schema' ; import Derivator from './derivator' ; import { ActionUtils } from './actions' ; import { Cardinalities , UpdateActionMethod } from './enums' ; import { arrayMove , arrayPut } from './util' ; export const makeReducer = < S extends State > ( schema , derivator , actionTypes , actionUtils ) => { const rootReducer = ( state = schema . getEmptyState ( ) , action ) : S => { if ( ! actionUtils . isHandleable ( action ) ) { return state ; } if ( actionUtils . isStateSetter ( action ) ) { if ( action . type === actionTypes . SET_STATE ) { return ( action as SetStateAction < S > ) . state ; } } if ( actionUtils . isBatch ( action ) ) { const batchAction = action as BatchAction ; return batchAction . actions . reduce ( ( prevState , action ) => { return singularReducer ( prevState , action ) ; } , state ) ; } else { return singularReducer ( state , action as SingularAction ) ; } } ; function singularReducer ( state , action ) { const singularAction = action as SingularAction ; let actions ; if ( actionUtils . isDerivable ( singularAction ) ) { const derivedAction = derivator . deriveAction ( state , singularAction ) as DerivedAction ; actions = derivedAction . derived ; } else { actions = [ singularAction ] ; } return actions . reduce ( ( prevState , action ) => { if ( action . type === actionTypes . SORT ) { const { entityType , compare } = action as SortAction ; const ids = prevState . ids [ entityType ] ; const entities = prevState . entities [ entityType ] ; const sortedIds = [ ... ids ] . sort ( ( idA , idB ) => { const entityA = entities [ idA ] ; const entityB = entities [ idB ] ; return compare ( entityA , entityB ) ; } ) ; return { entities : prevState . entities , ids : { ... prevState . ids , [ entityType ] : sortedIds } } as S ; } return { entities : entitiesReducer ( prevState . entities , action ) , ids : idsReducer ( prevState . ids , action ) } as S ; } , state ) ; } const defaultEntitiesState = schema . getEmptyEntitiesByTypeState ( ) ; function entitiesReducer ( state = defaultEntitiesState , action ) { if ( action . type === actionTypes . INVALID ) { return state ; } if ( ! schema . typeExists ( action . entityType ) ) { return state ; } if ( action . type === actionTypes . DETACH ) { const { entityType , id , detachableId , relation } = action as DetachAction ; const entity = state [ entityType ] [ id ] as Entity ; if ( ! entity ) { return state ; } const relationKey = schema . type ( entityType ) . resolveRelationKey ( relation ) ; if ( ! relationKey ) { return state ; } let newEntity = entity ; const cardinality = schema . type ( entityType ) . resolveRelationCardinality ( relation ) ; if ( cardinality === Cardinalities . ONE ) { const attachedId = entity [ relationKey ] as Id ; if ( detachableId !== attachedId ) { return state ; } newEntity = { ... entity , [ relationKey ] : undefined } ; } if ( cardinality === Cardinalities . MANY ) { const attachedIds = ( entity [ relationKey ] || [ ] ) as Id [ ] ; newEntity = { ... entity , [ relationKey ] : attachedIds . filter ( attachedId => attachedId !== detachableId ) } ; } return { ... state , [ entityType ] : { ... state [ entityType ] , [ id ] : newEntity } } ; } if ( action . type === actionTypes . ATTACH ) { const { entityType , id , attachableId , relation , index } = action as AttachAction ; const entity = state [ entityType ] [ id ] as Entity ; if ( ! entity ) { return state ; } const relationKey = schema . type ( entityType ) . resolveRelationKey ( relation ) ; if ( ! relationKey ) { return state ; } let newEntity = entity ; const cardinality = schema . type ( entityType ) . resolveRelationCardinality ( relation ) ; if ( cardinality === Cardinalities . ONE ) { newEntity = { ... newEntity , [ relationKey ] : attachableId } ; } if ( cardinality === Cardinalities . MANY ) { if ( ! entity [ relationKey ] || ! entity [ relationKey ] . includes ( attachableId ) ) { newEntity = { ... newEntity , [ relationKey ] : arrayPut ( attachableId , newEntity [ relationKey ] , index ) } ; } } return { ... state , [ entityType ] : { ... state [ entityType ] , [ id ] : newEntity } } ; } if ( action . type === actionTypes . DELETE ) { const { entityType , id } = action as DeleteAction ; const entity = state [ entityType ] [ id ] as Entity ; if ( ! entity ) { return state ; } const entitiesOfType = { ... state [ entityType ] } ; delete entitiesOfType [ id ] ; return { ... state , [ entityType ] : entitiesOfType } ; } if ( action . type === actionTypes . CREATE ) { const { entityType , id , data } = action as CreateAction ; const entity = state [ entityType ] [ id ] as Entity ; if ( entity ) { return state ; } return { ... state , [ entityType ] : { ... state [ entityType ] , [ id ] : data || { } } } ; } if ( action . type === actionTypes . UPDATE ) { const { entityType , id , data , method } = action as UpdateAction ; const entity = state [ entityType ] [ id ] as Entity ; if ( ! entity ) { return state ; } let newEntity = { ... entity } ; if ( method === UpdateActionMethod . PUT ) { const relationKeys = schema . type ( entityType ) . getRelationKeys ( ) ; const relationalData = relationKeys . reduce ( ( relationalData , relationKey ) => { if ( entity [ relationKey ] ) { relationalData [ relationKey ] = entity [ relationKey ] ; } return relationalData ; } , { } as { [ k ] : Id | Id [ ] ; } ) ; newEntity = { ... data , ... relationalData } ; } if ( method === UpdateActionMethod . PATCH ) { newEntity = { ... entity , ... data } ; } return { ... state , [ entityType ] : { ... state [ entityType ] , [ id ] : newEntity } } ; } if ( action . type === actionTypes . MOVE_ATTACHED ) { const { entityType , id , relation , src , dest } = action as MoveAttachedAction ; const entity = state [ entityType ] [ id ] as Entity ; if ( ! entity ) { return state ; } const relationKey = schema . type ( entityType ) . resolveRelationKey ( relation ) ; if ( ! relationKey ) { return state ; } const cardinality = schema . type ( entityType ) . resolveRelationCardinality ( relation ) ; if ( cardinality === Cardinalities . ONE ) { return state ; } const attachedIds = entity [ relationKey ] ; if ( ! Array . isArray ( attachedIds ) ) { return state ; } const newEntity = { ... entity , [ relationKey ] : arrayMove ( attachedIds , src , dest ) } ; return { ... state , [ entityType ] : { ... state [ entityType ] , [ id ] : newEntity } } ; } if ( action . type === actionTypes . SORT_ATTACHED ) { const { entityType , id , relation , compare } = action as SortAttachedAction ; const entity = state [ entityType ] [ id ] as Entity ; if ( ! entity ) { return state ; } const relationKey = schema . type ( entityType ) . resolveRelationKey ( relation ) ; const relationType = schema . type ( entityType ) . resolveRelationType ( relation ) ; if ( ! relationKey || ! relationType ) { return state ; } const cardinality = schema . type ( entityType ) . resolveRelationCardinality ( relation ) ; if ( cardinality === Cardinalities . ONE ) { return state ; } const attachedIds = entity [ relationKey ] ; if ( ! Array . isArray ( attachedIds ) ) { return state ; } const relatedEntities = state [ relationType ] ; const sortedIds = [ ... attachedIds ] . sort ( ( idA , idB ) => { const entityA = relatedEntities [ idA ] ; const entityB = relatedEntities [ idB ] ; return compare ( entityA , entityB ) ; } ) ; const newEntity = { ... entity , [ relationKey ] : sortedIds } ; return { ... state , [ entityType ] : { ... state [ entityType ] , [ id ] : newEntity } } ; } return state ; } const defaultIdsState = schema . getEmptyIdsByTypeState ( ) ; function idsReducer ( state = defaultIdsState , action ) { if ( action . type === actionTypes . INVALID ) { return state ; } if ( ! schema . typeExists ( action . entityType ) ) { return state ; } if ( action . type === actionTypes . DELETE ) { const { entityType , id } = action as DeleteAction ; const idsOfEntity = state [ entityType ] . filter ( existingId => existingId !== id ) ; return { ... state , [ entityType ] : idsOfEntity } ; } if ( action . type === actionTypes . CREATE ) { const { entityType , id , index } = action as CreateAction ; if ( state [ entityType ] . includes ( id ) ) { return state ; } return { ... state , [ entityType ] : arrayPut ( id , state [ entityType ] , index ) } ; } if ( action . type === actionTypes . MOVE ) { const { entityType , src , dest } = action as MoveAction ; return { ... state , [ entityType ] : arrayMove ( state [ entityType ] , src , dest ) } ; } return state ; } return rootReducer ; } ;