import { observable , IObservableArray , action } from 'mobx' ; import { referenceOne , referenceMany } from '.' ; import { moveItem } from 'mobx-utils' ; const map = require ( 'lodash/map' ) ; const filter = require ( 'lodash/filter' ) ; const find = require ( 'lodash/find' ) ; const some = require ( 'lodash/some' ) ; const every = require ( 'lodash/every' ) ; const reduce = require ( 'lodash/reduce' ) ; const chunk = require ( 'lodash/chunk' ) ; const forEach = require ( 'lodash/forEach' ) ; const orderBy = require ( 'lodash/orderBy' ) ; /**
 * Used in various Lodash functions such as `map`, `filter`..
 */ export interface IIteratee < T , M > { ( input , index ) : M ; } /**
 * Indexed collection used for reusing models.
 */ export interface ICollection < T > { /**
     * The actual item map.
     */ items : IObservableArray < T > ; /**
     * Getter for the items length.
     */ length : number ; /**
     * Adds one or more models to the end of the collection.
     */ add ( models ) : this ; /**
     * Multi-version of `create`.
     */ create ( data , createOpts ? ) : T [ ] ; /**
     * Like `set`, but will add regardless of whether an id is present or not.
     * This has the added risk of resulting multiple model instances if you don't make sure
     * to update the existing model once you do have an id. The model id is what makes the whole
     * one-instance-per-entity work.
     */ create ( data , createOpts ? ) : T ; /**
     * Gets items by ids.
     */ get ( id ) : Array < T | undefined > ; /**
     * Gets a item by id.
     */ get ( id ) : T | undefined ; /**
     * Same as the singular version, but with multiple.
     */ set < D > ( data ? , setOpts ? ) : D extends undefined ? undefined : T [ ] ; /**
     * Adds a single item and maps it using the mapper in the options.
     */ set < D > ( data ? , setOpts ? ) : D extends undefined ? undefined : T ; /**
     * Clears the collection.
     */ clear ( ) : this ; /**
     * Maps over the items.
     */ map < M > ( iteratee ) : M [ ] ; /**
     * Filters the items.
     */ filter ( iteratee ) : T [ ] ; /**
     * Determines if there are any items that match the predicate.
     */ some ( iteratee ) : boolean ; /**
     * Determines if all items match the predicate.
     */ every ( iteratee ) : boolean ; /**
     * Chunks the collection.
     */ chunk ( size ? ) : Array < Array < T > > ; /**
     * Reduces on the items.
     */ reduce < R > ( iteratee , seed ? ) : R ; /**
     * Finds a particular item.
     */ find ( iteratee ) : T | undefined ; /**
     * Orders the items based on iteratees and orders.
     */ orderBy ( iteratees , orders ? ) : T [ ] ; /**
     * Removes an item based on ID or the item itself.
     */ remove ( modelOrId ) : this ; /**
     * Slices the array.
     */ slice ( start ? , end ? ) : T [ ] ; /**
     * Moves an item from one index to another, using MobX's `move`.
     */ move ( fromIndex , toIndex ) : this ; /**
     * Returns the item at the specified index.
     */ at ( index ) : T | undefined ; /**
     * Runs a `forEach` over the items and returns the collection.
     */ forEach ( iteratee ) : this ; param { Array < string > } ids param { keyof T } field referenceOne < K extends keyof T > ( id ) : T | null ; referenceOne < K extends keyof T > ( ids ) : Array < T > ; referenceOne < K extends keyof T > ( id , field ? ) : T | null ; referenceOne < K extends keyof T > ( ids , field ? ) : Array < T > ; referenceOne < K extends keyof T > ( ids , field ? ) : T | null | Array < T > ; param { Array < string > } ids param { keyof T } field referenceMany < K extends keyof T > ( ids , field ) : Array < T > ; } /**
 * Called when the collection wants to add a new item.
 */ export interface ICreateFunc < T > { ( input , opts ) : T ; } /**
 * Called when the collection wants to update an existing item.
 */ export interface IUpdateFunc < T > { ( existing , input , opts ) : T ; } /**
 * Function used for getting the ID of a particular input.
 */ export interface IGetId < T > { ( obj , opts ) : any ; } /**
 * Collection options.
 */ export interface ICollectionOptions < T > { /**
     * May be used by callbacks like `getModelId` and `getDataId`
     */ idAttribute ? : string ; /**
     * Called when the collection wants to add a new item.
     */ create ? : ICreateFunc < T > ; /**
     * Called when the collection wants to update an existing item with more data.
     */ update ? : IUpdateFunc < T > ; /**
     * Used to get the ID of a model that was passed in. Whatever is returned from
     * this should be coercible to a string, and is used for indexing.
     */ getModelId ? : IGetId < T > ; /**
     * Used to get an ID from input data, used to determine whether to create
     * or update.
     */ getDataId ? : IGetId < any > ; } /**
 * Default collection options.
 */ export const defaults = { create : ( input , opts ) => input , getDataId : ( input , opts ) => input [ opts . idAttribute || 'id' ] , getModelId : ( existing , opts ) => existing [ opts . idAttribute || 'id' ] , idAttribute : 'id' , update : ( existing , input , opts ) => Object . assign ( existing , input ) } ; /**
 * Possible model IDs.
 */ export type ModelId = string | number | Date ; type { T } type { O } export function collection < T > ( opts ? ) { opts = Object . assign ( { } , defaults , opts ) ; const items = observable . array ( [ ] , { deep : false } ) ; const idMap = new Map < string , T > ( ) ; const self = { items , add : action ( add ) , get , set : action ( set ) , create : action ( create ) , remove : action ( remove ) , clear : action ( clear ) , filter : bindLodashFunc ( items , filter ) , some : bindLodashFunc ( items , some ) , every : bindLodashFunc ( items , every ) , find : bindLodashFunc ( items , find ) , orderBy : bindLodashFunc ( items , orderBy ) , map : bindLodashFunc ( items , map ) , reduce : bindLodashFunc ( items , reduce ) , chunk : bindLodashFunc ( items , chunk ) , referenceOne : ( ( ids , field ) : any => ( referenceOne as any ) ( self , ids , field ) ) as any , referenceMany : ( ( ids , field ) : any => ( referenceMany as any ) ( self , ids , field ) ) as any , forEach : ( iteratee ) => { forEach ( items , iteratee ) ; return self ; } , at : ( index ) => items [ index ] , slice , move , get length ( ) { return items . length ; } } ; function get ( id ) ; function get ( id ) ; function get ( id ) { if ( id === undefined || id === null ) { return undefined ; } if ( Array . isArray ( id ) ) { return id . map ( get ) as T [ ] ; } const idAsString = id . toString ( ) ; const fromMap = idMap . get ( idAsString ) ; if ( fromMap ) { return fromMap ; } const found = items . find ( item => { const modelId = opts ! . getModelId ! ( item , opts ! ) ; if ( ! modelId ) { return false ; } return modelId . toString ( ) === idAsString ; } ) ; if ( found !== undefined ) { idMap . set ( idAsString , found ) ; } return found ; } function set ( data ? , setOpts ? ) ; function set ( data ? , setOpts ? ) ; function set ( data ? , setOpts ? ) { setOpts = Object . assign ( { } , opts as any , setOpts ) ; if ( ! data ) { return undefined ; } if ( Array . isArray ( data ) ) { return data . map ( d => set ( d , setOpts ) as T ) ; } let dataId = opts ! . getDataId ! ( data , setOpts ! ) ; if ( dataId === undefined ) { return undefined ; } if ( dataId === null ) { throw new TypeError ( `${ dataId } is not a valid ID` ) ; } dataId = dataId . toString ( ) ; let existing = get ( dataId as string ) ; if ( existing ) { opts ! . update ! ( existing , data , Object . assign ( { } , opts as any , setOpts ) ) ; return existing ; } const created = opts ! . create ! ( data , Object . assign ( { } , opts as any , setOpts ) ) ; existing = get ( dataId as string ) ; if ( existing ) { opts ! . update ! ( existing , data , Object . assign ( { } , opts as any , setOpts ) ) ; return existing ; } items . push ( created ) ; idMap . set ( dataId , created ) ; return created ; } function create ( data , createOpts ? ) ; function create ( data , createOpts ? ) ; function create ( data , createOpts ? ) { if ( Array . isArray ( data ) ) { return data . map ( d => create ( d , createOpts ) ) ; } createOpts = Object . assign ( { } , opts as any , createOpts ) ; const dataId = createOpts ! . getDataId ! ( data , createOpts ! ) ; if ( dataId !== undefined && dataId !== null ) { return set ( data , createOpts ) as any ; } const created = createOpts ! . create ! ( data , createOpts ! ) ; add ( created ) ; return created ; } function add ( models ) { if ( ! Array . isArray ( models ) ) { return add ( [ models ] ) ; } models = models . filter ( x => items . indexOf ( x ) === - 1 ) ; items . push ( ... models ) ; return self ; } function remove ( modelOrId ) { const model = typeof modelOrId === 'string' || typeof modelOrId === 'number' || modelOrId instanceof Date ? get ( modelOrId ) : modelOrId ; if ( ! model ) { return self ; } items . remove ( model ) ; const modelId = opts ! . getModelId ! ( model , opts ! ) ; if ( modelId !== null && modelId !== undefined ) { idMap . delete ( modelId . toString ( ) ) ; } return self ; } function clear ( ) { items . clear ( ) ; idMap . clear ( ) ; return self ; } function slice ( start ? , end ? ) { return items . slice ( start , end ) ; } function move ( fromIndex , toIndex ) { moveItem ( items , fromIndex , toIndex ) ; return self ; } return self ; } /**
 * Utility for binding lodash functions.
 */ function bindLodashFunc ( items , func ) { return ( ... args ) => func ( items , ... args ) ; }