import utils from './utils'
import {
belongsToType,
hasManyType,
hasOneType
} from './decorators'
import Collection from './Collection'
const DOMAIN = 'LinkedCollection'
/**
* TODO
*
* ```javascript
* import {LinkedCollection} from 'js-data'
* ```
*
* @class LinkedCollection
* @extends Collection
* @param {Array} [records] Initial set of records to insert into the
* collection. See {@link Collection}.
* @param {Object} [opts] Configuration options. See {@link Collection}.
* @returns {Mapper}
*/
export default Collection.extend({
constructor: function LinkedCollection (records, opts) {
utils.classCallCheck(this, LinkedCollection)
// Make sure this collection has somewhere to store "added" timestamps
Object.defineProperties(this, {
_added: {
value: {}
},
datastore: {
writable: true,
value: undefined
}
})
LinkedCollection.__super__.call(this, records, opts)
// Make sure this collection has a reference to a datastore
if (!this.datastore) {
throw utils.err(`new ${DOMAIN}`, 'opts.datastore')(400, 'DataStore', this.datastore)
}
return this
},
_onRecordEvent (...args) {
utils.getSuper(this).prototype._onRecordEvent.apply(this, args)
const event = args[0]
// This is a very brute force method
// Lots of room for optimization
if (utils.isString(event) && event.indexOf('change') === 0) {
this.updateIndexes(args[1])
}
},
add (records, opts) {
const datastore = this.datastore
const mapper = this.mapper
const relationList = mapper.relationList
const timestamp = new Date().getTime()
const usesRecordClass = !!mapper.recordClass
const idAttribute = mapper.idAttribute
let singular
if (utils.isObject(records) && !utils.isArray(records)) {
singular = true
records = [records]
}
records = utils.getSuper(this).prototype.add.call(this, records, opts)
if (relationList.length && records.length) {
// Check the currently visited record for relations that need to be
// inserted into their respective collections.
mapper.relationList.forEach(function (def) {
const relationName = def.relation
// A reference to the Mapper that this Mapper is related to
const relatedMapper = datastore.getMapper(relationName)
// The field used by the related Mapper as the primary key
const relationIdAttribute = relatedMapper.idAttribute
// Grab the foreign key in this relationship, if there is one
const foreignKey = def.foreignKey
// A lot of this is an optimization for being able to insert a lot of
// data as quickly as possible
const relatedCollection = datastore.getCollection(relationName)
const type = def.type
const isHasMany = type === hasManyType
const shouldAdd = utils.isUndefined(def.add) ? true : !!def.add
let relatedData
records.forEach(function (record) {
// Grab a reference to the related data attached or linked to the
// currently visited record
relatedData = def.getLocalField(record)
const id = utils.get(record, idAttribute)
if (utils.isFunction(def.add)) {
relatedData = def.add(datastore, def, record)
} else if (relatedData) {
// Otherwise, if there is something to be added, add it
if (isHasMany) {
// Handle inserting hasMany relations
relatedData = relatedData.map(function (toInsertItem) {
// Check that this item isn't the same item that is already in the
// store
if (toInsertItem !== relatedCollection.get(relatedCollection.recordId(toInsertItem))) {
// Make sure this item has its foreignKey
if (foreignKey) {
// TODO: slow, could be optimized? But user loses hook
def.setForeignKey(record, toInsertItem)
}
// Finally add this related item
if (shouldAdd) {
toInsertItem = relatedCollection.add(toInsertItem)
}
}
return toInsertItem
})
} else {
const relatedDataId = utils.get(relatedData, relationIdAttribute)
// Handle inserting belongsTo and hasOne relations
if (relatedData !== relatedCollection.get(relatedDataId)) {
// Make sure foreignKey field is set
def.setForeignKey(record, relatedData)
// Finally insert this related item
if (shouldAdd) {
relatedData = relatedCollection.add(relatedData)
}
}
}
}
if (!relatedData || (utils.isArray(relatedData) && !relatedData.length)) {
if (type === belongsToType) {
const relatedId = utils.get(record, foreignKey)
if (!utils.isUndefined(relatedId)) {
relatedData = relatedCollection.get(relatedId)
}
} else if (type === hasOneType) {
const _records = relatedCollection.filter({
[foreignKey]: id
})
relatedData = _records.length ? _records[0] : undefined
} else if (type === hasManyType) {
if (foreignKey) {
const _records = relatedCollection.filter({
[foreignKey]: id
})
relatedData = _records.length ? _records : undefined
} else if (def.localKeys && utils.get(record, def.localKeys)) {
const _records = relatedCollection.filter({
where: {
[relationIdAttribute]: {
'in': utils.get(record, def.localKeys)
}
}
})
relatedData = _records.length ? _records : undefined
} else if (def.foreignKeys) {
const _records = relatedCollection.filter({
where: {
[def.foreignKeys]: {
'contains': id
}
}
})
relatedData = _records.length ? _records : undefined
}
}
}
if (relatedData) {
def.setLocalField(record, relatedData)
} else {
}
})
})
}
records.forEach((record) => {
// Track when this record was added
this._added[this.recordId(record)] = timestamp
if (usesRecordClass) {
record._set('$', timestamp)
}
})
return singular ? records[0] : records
},
remove (id, opts) {
const mapper = this.mapper
const record = utils.getSuper(this).prototype.remove.call(this, id, opts)
if (record) {
delete this._added[id]
if (mapper.recordClass) {
record._set('$') // unset
}
}
return record
},
removeAll (query, opts) {
const mapper = this.mapper
const records = utils.getSuper(this).prototype.removeAll.call(this, query, opts)
records.forEach((record) => {
delete this._added[this.recordId(record)]
if (mapper.recordClass) {
record._set('$') // unset
}
})
return records
}
})