js/datas/data_collection.js
// NPM IMPORTS
import assert from 'assert'
// COMMON IMPORTS
import T from '../utils/types'
import Loggable from '../base/loggable'
/**
* Contextual constant for this file logs.
* @private
*/
let context = 'common/data/data_collection'
/**
* DataCollection class.
*
* @author Luc BORIES
* @license Apache-2.0
*
* @example
* API:
* ->constructor(arg_cache_manager,arg_data_adapter,arg_model_schema)
*
* ->get_name():string - get tenant to use inside this datastore.
* ->get_cache_manager():CacheManager - get cache manager instance.
* ->get_adapter():DataAdapter - get data adapter instance.
* ->get_model():DataModel - get collection model instance.
*
* ->validate_record(arg_record):Promise(boolean) - test if given datas are valid for collection model.
* ->new_record(arg_record_datas, arg_record_id) - create a new record instance.
* ->create_record(arg_record):Promise(DataRecord) - create an existing unsaved data record.
* ->delete_record(arg_record):Promise(boolean) - delete an existing data record.
* ->update_record(arg_record):Promise(DataRecord) - update an existing data record.
* ->reload_record(arg_record):Promise(DataRecord) - reload an existing data record.
* ->has_record(arg_record_id):Promise(boolean) - test if a data record exists with an id.
*
* ->find_one_record(arg_record_id):Promise(DataRecord) - find an existing data record with an id.
* ->find_records(arg_query):Promise(DataRecordArray) - find existing data records with a query.
* ->find_all_records():Promise(DataRecordArray) - find all xisting data records.
*
* PRIVATE:
* ->_emit(arg_event, arg_datas=undefined):nothing
* ->_trigger(arg_event, arg_datas):nothing
* ->_has_cached_record_by_id(arg_id):Promise(boolean)
* ->_get_cached_record_by_id(arg_id):Promise(DataRecord)
* ->_set_cached_record_by_id(arg_record):Promise(boolean)
* ->_remove_cached_record_by_id(arg_id):Promise(boolean)
*
*
* USAGE ON BROWSER:
* // ds is a DataStore instance
* var cars = ds.get_collection('cars')
* var car_12 = cars.find_one_record('12')
*
*
*/
export default class DataCollection extends Loggable
{
/**
* DataCollection class is responsible to manage one model records from one adapter:
* all records operations, cached records, model logic (field value validation, triggers).
* DataCollection instances are managed by a DataStore instance.
*
* @param {CacheManager} arg_cache_manager - cache manager instance.
* @param {DataAdapter} arg_data_adapter - collection data adapter.
* @param {array} arg_model_schema - topology model schema.
*
* @returns {nothing}
*/
constructor(arg_cache_manager, arg_data_adapter, arg_model_schema)
{
assert( T.isObject(arg_cache_manager) && arg_cache_manager.is_cache_manager, context + ':constructor:bad cache manager object')
assert( T.isObject(arg_data_adapter) && arg_data_adapter.is_data_adapter, context + ':constructor:bad data adapter object')
assert( T.isObject(arg_model_schema) && arg_model_schema.is_topology_model, context + ':constructor:bad model schema object')
const model_plural_name = arg_model_schema.get_plural_name()
assert( T.isString(model_plural_name) && model_plural_name.length > 0, context + ':constructor:bad collection name string')
super(context)
/**
* Class type flag.
* @type {boolean}
*/
this.is_data_collection = true
/**
* CacheManager instance.
* @type {CacheManager}
*/
this._cache_manager = arg_cache_manager
/**
* Datas adapter instance.
* @type {DataAdapter}
*/
this._adapter = arg_data_adapter
/**
* Topology model schema instance.
* @type {array}
*/
this._schema = arg_model_schema
/**
* Datas collection prefix.
* @type {string}
*/
this._cache_prefix = model_plural_name
/**
* Datas collection name.
* @type {string}
*/
this._name = model_plural_name
}
/**
* Emit on event.
* @private
*
* @param {string} arg_event - event name.
* @param {any} arg_datas - event datas (optional, default:undefined).
*
* @returns {nothing}
*/
_emit(arg_event, arg_datas=undefined) // TODO
{
this.debug(context + ':emit:' + arg_event)
// if ( this._schema.has_rules_agenda() )
// {
// this._schema.push_on_rule_agenda(arg_event, arg_datas)
// }
}
/**
* Call event triggers.
* @private
*
* @param {string} arg_event - event name.
* @param {any} arg_datas - event datas (optional, default:undefined).
*
* @returns {nothing}
*/
_trigger(arg_event, arg_datas=undefined) // TODO
{
this.debug(context + ':trigger:' + arg_event)
// this._schema._trigger(arg_event, arg_datas)
this._emit(arg_event, arg_datas)
// TODO: UPDATE METRICS
}
/**
* Test if a record is cached.
* @private
*
* @param {string} arg_id - record id.
*
* @returns {Promise} - Promise of boolean value: found (true) or not found (false).
*/
_has_cached_record_by_id(arg_id)
{
const key = this._cache_prefix + ':' + arg_id
return this._cache_manager.has(key)
}
/**
* Test if a record array is cached.
* @private
*
* @param {DataQuery} arg_query - data query.
*
* @returns {Promise} - Promise of boolean value: found (true) or not found (false).
*/
_has_cached_record_by_query(arg_query)
{
const key = this._cache_prefix + ':query:' + arg_query.hash()
return this._cache_manager.has(key)
}
/**
* Get a cached record.
* @private
*
* @param {string} arg_id - record id.
*
* @returns {Promise} - Promise of a DataRecord instance.
*/
_get_cached_record_by_id(arg_id)
{
const key = this._cache_prefix + ':' + arg_id
return this._cache_manager.get(key, undefined)
}
/**
* Get a cached record array.
* @private
*
* @param {DataQuery} arg_query - data query.
*
* @returns {Promise} - Promise of a DataRecordArray instance.
*/
_get_cached_record_by_query(arg_query)
{
const key = this._cache_prefix + ':query:' + arg_query.hash()
return this._cache_manager.get(key, undefined)
}
/**
* Add a record to cache.
* @private
*
* @param {DataRecord} arg_record - record
*
* @returns {Promise} - Promise of boolean value: success (true) or failure (false).
*/
_set_cached_record_by_id(arg_record)
{
assert( T.isObject(arg_record) && arg_record.is_data_record, context + ':_set_cached_record_by_id:bad data record object')
const key = this._cache_prefix + ':' + arg_record.get_id()
return this._cache_manager.set(key, arg_record, this._schema.get_ttl())
}
/**
* Add a record array to cache.
* @private
*
* @param {DataQuery} arg_query - data query.
* @param {DataRecordArray} arg_record_array - record array.
*
* @returns {Promise} - Promise of boolean value: success (true) or failure (false).
*/
_set_cached_record_by_query(arg_query, arg_record_array)
{
assert( T.isObject(arg_query) && arg_query.is_data_query, context + ':_set_cached_record_by_query:bad data query object')
assert( T.isObject(arg_record_array) && arg_record_array.is_data_record_array, context + ':_set_cached_record_by_query:bad data record object')
const promises = []
// CACHE RECORD ARRAY
const key = this._cache_prefix + ':query:' + arg_query.hash()
promises.push( this._cache_manager.set(key, arg_record_array, this._schema.get_ttl()) )
// CACHE ALL ARRAY RECORDS
arg_record_array._records.forEach(
(record)=>{
promises.push( this._set_cached_record_by_id(record) )
}
)
return Promise.all(promises)
}
/**
* Remove a cached record.
* @private
*
* @param {string} arg_id - record id.
*
* @returns {Promise} - Promise of boolean value: success (true) or failure (false).
*/
_remove_cached_record_by_id(arg_id)
{
const key = this._cache_prefix + ':' + arg_id
return this._cache_manager.remove(key)
}
/**
* Get collection name.
*
* @returns {string}
*/
get_name()
{
return this._name
}
/**
* Get cache manager.
*
* @returns {DataAdapter}
*/
get_cache_manager()
{
assert( T.isObject(this._cache_manager) && this._cache_manager.is_cache_manager, context + ':get_cache_manager:bad cache manager object')
return this._cache_manager
}
/**
* Get data adapter.
*
* @returns {DataAdapter}
*/
get_adapter()
{
assert( T.isObject(this._adapter) && this._adapter.is_data_adapter, context + ':get_adapter:bad data adapter object')
return this._adapter
}
/**
* Get data model.
*
* @returns {DataModel}
*/
get_model()
{
assert( T.isObject(this._schema) && this._schema.is_topology_model, context + ':get_model:bad data model object')
return this._schema
}
/**
* Get data model.
*
* @returns {DataModel}
*/
get_schema()
{
assert( T.isObject(this._schema) && this._schema.is_topology_model, context + ':get_model:bad data model object')
return this._schema
}
/**
* Validate data record values.
* 1-Call 'before_validate' triggers.
* 2-Check all fields values.
* 3-Call 'after_validate_ok' triggers on success.
* Call 'after_validate_ko' triggers on failure.
* 4-Return success (true) or failure (false)
*
* @param {DataRecord} arg_record - data record instance.
*
* @returns {Promise} - Promise of a boolean.
*/
validate_record(arg_record) // TODO
{
assert( T.isObject(arg_record) && arg_record.is_data_record, context + ':validate_record:bad data record object')
this._trigger('before_validate', { record:arg_record })
const result = this._schema.validate(arg_record.get_attributes_object())
this._trigger(result.is_valid ? 'after_validate_ok' : 'after_validate_ko', { record:arg_record })
return Promise.resolve(result)
}
/**
* Create a new data record instance, not saved.
*
* @param {object} arg_record_datas - new record attributes.
* @param {string} arg_record_id - new record unique id (optional).
*
* @returns {Promise} - Promise(DataRecord)
*/
new_record(arg_record_datas, arg_record_id)
{
// const is_cached_promise = this._has_cached_record_by_id(arg_record_id)
// if (is_cached)
// {
// console.log('collection:is cached')
// return this._get_cached_record_by_id(arg_record_id)
// }
return this._adapter.new_record(this._schema.get_name(), arg_record_datas, arg_record_id)
}
/**
* Create a data collection record.
* 1-Call 'before_create' triggers.
* 2-Check existing record id within cached records.
* 3-Validate record datas: reject on failure
* 4-Create a DataRecord instance with adapter.new_record.
* 5-Add record to cache.
* 6-Call record.save()
* 7-Call 'after_create' triggers.
*
* @param {DataRecord} arg_record - data record instance.
*
* @returns {Promise} - Promise of a DataRecord object.
*/
create_record(arg_record)
{
assert( T.isObject(arg_record) && arg_record.is_data_record, context + ':create_record:bad data record object')
this._trigger('before_create', { has_error:false, error_msg:undefined, record:arg_record})
return this._has_cached_record_by_id(arg_record.get_id())
.then(
(is_cached)=>{
if (is_cached)
{
this._trigger('after_create', { has_error:true, error_msg:'already_exists', record:arg_record })
return Promise.reject('already_exists')
}
}
)
.then(
()=>{
const is_valid = this.validate_record(arg_record)
if ( ! is_valid)
{
this._trigger('after_create', { has_error:true, error_msg:'not_valid', record:arg_record })
return Promise.reject('not_valid')
}
}
)
.then(
()=>{
return this._adapter.create_record(this._schema.get_name(), arg_record.get_attributes_object())
}
)
.then(
(record)=>{
this._trigger('after_create', { has_error:false, error_msg:undefined, record:record})
return record
}
)
.catch(
(e)=>{
this._trigger('after_create', { has_error:true, error_msg:e, record:arg_record})
console.error(context + ':create_record:', e)
return undefined
}
)
}
/**
* Delete a data collection record.
* 1-Call 'before_delete' triggers.
* 2-Remove cached record.
* 3-Call record.delete()
* 4-Call 'after_delete' triggers.
*
* @param {DataRecord} arg_record - data record instance.
*
* @returns {Promise} - Promise of boolean success.
*/
delete_record(arg_record)
{
assert( T.isObject(arg_record) && arg_record.is_data_record, context + ':delete_record:bad data record object')
this._trigger('before_delete', { has_error:false, error_msg:undefined, record:arg_record})
return this._remove_cached_record_by_id(arg_record.get_id())
.then(
()=>{
return this._adapter.delete_record(this._schema.get_name(), arg_record.get_id())
.then( (record)=> { return record.set_removed() } )
}
).then(
()=>{
this._trigger('after_delete', { has_error:false, error_msg:undefined, record:arg_record})
return true
}
).catch(
(e)=>{
this._trigger('after_delete', { has_error:true, error_msg:e, record:arg_record})
return false
}
)
}
/**
* Update a data collection record.
* 1-Call 'before_update' triggers.
* 2-Update cached record.
* 3-Call record.update()
* 4-Call 'after_update' triggers.
*
* @param {DataRecord} arg_record - data record instance.
*
* @returns {Promise} - Promise of boolean success.
*/
update_record(arg_record)
{
assert( T.isObject(arg_record) && arg_record.is_data_record, context + ':update_record:bad data record object')
this._trigger('before_update', { has_error:false, error_msg:undefined, record:arg_record})
return this._adapter.update_record(this._schema.get_name(), arg_record)
.then(
(record)=>{
console.log(context + ':update_record:record', record)
return this._set_cached_record_by_id(arg_record.get_id(), record)
}
)
.then(
()=>{
this._trigger('after_update', { has_error:false, error_msg:undefined, record:arg_record})
console.log(context + ':update_record:true')
return true
}
)
.catch(
(e)=>{
this._trigger('after_update', { has_error:true, error_msg:e, record:arg_record})
return false
}
)
}
/**
* Update a data collection record.
* 1-Search record into cache
* 2-Search record into adapter
*
* @param {string} arg_record_id - data record id.
*
* @returns {Promise} - Promise of boolean found:true, not found:false.
*/
has_record(arg_record_id)
{
assert( T.isString(arg_record_id) && arg_record_id.length > 0, context + ':has_record:bad record id string')
return this._has_cached_record_by_id(arg_record_id)
.then(
(found)=>{
if (found)
{
return true
}
return this._adapter.has_record(this._schema.get_name(), arg_record_id)
}
).catch(
(e)=>{
this._trigger('has_record', { has_error:true, error_msg:e, id:arg_record_id} )
return false
}
)
}
/**
* Find an existing data record with an id.
* 1-Search record into cache
* 2-Search record into adapter
* 3-Save record into cache
*
* @param {string} arg_record_id - data record id.
*
* @returns {Promise} - Promise of DataRecord.
*/
find_one_record(arg_record_id)
{
assert( T.isString(arg_record_id) && arg_record_id.length > 0, context + ':find_one_record:bad record id string')
return this._get_cached_record_by_id(arg_record_id)
.then(
(record)=>{
if ( T.isObject(record) && record.is_data_record )
{
this._trigger('find_one_record', { has_error:false, error_msg:undefined, id:arg_record_id, from:'cache'})
return record
}
return this._adapter.find_one_record(this._schema.get_name(), arg_record_id)
.then(
(attributes)=>{
// console.log(context + ':find_one_record:adapter:attributes', attributes)
return this.new_record(attributes, arg_record_id)
}
)
.then(
(record)=>{
if ( T.isObject(record) && record.is_data_record )
{
this._trigger('find_one_record', { has_error:false, error_msg:undefined, id:arg_record_id, record:record, from:'adapter'})
// console.log(context + ':find_one_record:adapter:good record', record)
return record
}
console.error(context + ':find_one_record:adapter:bad record', record)
this._trigger('find_one_record', { has_error:false, error_msg:undefined, id:arg_record_id, from:'notfound'})
return undefined
}
)
}
)
.catch(
(e)=>{
this._trigger('find_one_record', { has_error:true, error_msg:e, id:arg_record_id} )
console.error(context + ':find_one_record:error', e)
return undefined
}
)
}
/**
* Find existing data records with a query.
* 1-Search records into cache
* 2-Search records into adapter
* 3-Save records into cache
*
* @param {DataQuery} arg_query - data query.
*
* @returns {Promise} - Promise of DataRecordArray.
*/
find_records(arg_query)
{
assert( T.isObject(arg_query) && arg_query.is_data_query, context + ':find_records:bad query object')
return this._get_cached_record_by_query(arg_query)
.then(
(record_array)=>{
if ( T.isObject(record_array) && record_array.is_data_record_array )
{
this._trigger('find_records', { has_error:false, error_msg:undefined, query:arg_query, from:'cache'})
return record_array
}
return this._adapter.find_records(this._schema.get_name(), arg_query)
.then(
(attributes_array)=>{
if ( T.isArray(attributes_array) )
{
return this.new_record_array(attributes_array)
}
this._trigger('find_records', { has_error:true, error_msg:'bad attributes array', query:arg_query, from:'notfound'})
return undefined
}
)
.then(
(record_array)=>{
if ( T.isObject(record_array) && record_array.is_data_record_array )
{
return this._set_cached_record_by_query(arg_query, record_array).then(
()=>{
this._trigger('find_records', { has_error:false, error_msg:undefined, query:arg_query, records:record_array, from:'adapter'})
return record_array
}
)
}
this._trigger('find_records', { has_error:false, error_msg:'bad DataRecordArray', query:arg_query, from:'notfound'})
return undefined
}
)
}
).catch(
(e)=>{
this._trigger('find_one_record', { has_error:true, error_msg:e, query:arg_query} )
return undefined
}
)
}
/**
* Find all existing data records from adapter.
*
* @returns {Promise} - Promise of DataRecordArray.
*/
find_all_records() // TODO use a query and cache: calling find_records(query) with query.select_all()
{
return this._adapter.find_all_records(this._schema.get_name())
.then(
(record_array)=>{
if ( T.isObject(record_array) && record_array.is_data_record_array )
{
this._trigger('find_all_records', { has_error:false, error_msg:undefined, from:'adapter'})
return record_array
}
this._trigger('find_all_records', { has_error:true, error_msg:'bad DataRecordArray'} )
return undefined
}
)
.catch(
(e)=>{
this._trigger('find_all_records', { has_error:true, error_msg:e } )
return undefined
}
)
}
}