dhxmvp
Version:
A complete boilerplate for building online, offline and syncable MVP Single Page Applications using DHTMLX.
921 lines (869 loc) • 47.4 kB
JavaScript
/*jshint loopfunc: true */
/**
* model.database
* model.versions
* model.schema
* model.data
*/
$dhx.ui.mvp.model.engine.declare({
"backboneIDB": (function() {
'strict';
var model = (function() {
var _private = {
/**
* [database indexeddb-backbonejs-adapter database object]
* @type {Object}
*/
database: {
/**
* [id name of the IndexedDB database]
* @type {String}
*/
id: "dhxMVP__99999",
description: "dhxMVP demo database",
nolog: true,
/**
* [migrations Stores a set of metadata object.
* Every object defines a database version.
* This array will be filled on model.schema.start() ]
* @type {Array}
*/
migrations: []
},
/**
* [versions metadata is a set of objects that defines all versions of the database.
* This metadata will be used as configuration to set up indexedDB stores,
* setup grids and forms on UI and validate input data]
* @type {Object}
*/
versions: {
/**
* This is the number of the version. Shall to be a integer number,
* but encoded as JSON string.
* This number is used to set up a indexedDB version number
*/
"1": $dhx.ui.mvp.model.collection.get()
},
start: function(c) {
console.log('model.start() triggered');
if (c.onSuccess) c.onSuccess();
}
},
_public = {
database: _private.database,
versions: _private.versions,
start: _private.start
};
return _public;
}());
model.subscriber =function(event, message) {
var now = (new Date()).getTime() * 10000;
// if message was sent by other client
if( message.client_id != $dhx.ui.session.client_id() )
{
if( model.schema.io[ message.collection ] )
{
if( model.schema.io[ message.collection ][ message.method ] )
{
console.log('backboneIDB Received Message: ', message);
$dhx.jDBdStorage.storeObject('$dhx.mvp.ui.last_sync_time', $dhx.crypt.base64_encode(now));
message.model.notify = false;
model.schema.io[ message.collection ][ message.method ]({
record: message.model,
onSuccess: function() {
console.info('success '+message.method+'ing record: ', (message.model.id || message.model) );
},
onFail: function() {
console.warn('discarding record: ', message.model);
//console.warn('props: ', Object.keys(message.model).length);
}
});
return;
}
}
console.warn('backboneIDB Received Unknow Message: ', message);
}
};
model.data = (function() {
var _collections = {},
_private = {},
API = {
_collections: _collections,
setIoAPI : function(collectionName, modelName){
model.schema.io[ collectionName ] = (function(collectionName, modelName) {
var api = {
model: modelName,
collection: collectionName,
create: function(c) {
c.model = this.model;
c.collection = this.collection;
model.schema._io._create(c);
},
update: function(c) {
c.model = this.model;
c.collection = this.collection;
model.schema._io._update(c);
},
destroy: function(c) {
c.model = this.model;
c.collection = this.collection;
model.schema._io._destroy(c);
},
read: function(c) {
c.model = this.model;
c.collection = this.collection;
model.schema._io._read(c);
},
readAll: function(c) {
c.model = this.model;
c.collection = this.collection;
model.schema._io._readAll(c);
}
};
return api;
})(collectionName, modelName);
},
_start: function() {
console.info(' starting model.data ');
// iterate over collection's metadata and create private namespace
/**
* iterate over collection's metadata and create public namespace
* The namespace 'model.data' will store all backbone models and collections
* The namespace 'model.schema.io' will store all CRUD operations for the model
*/
for (var name in _collections) {
// create backbone model
//console.log( Backbone.ajaxSync );
this.setIoAPI(name, _collections[name].item);
_private[_collections[name].item] = Backbone.Model.extend({
database: model.database,
storeName: name,
nolog: true,
defaults: _collections[name].defaults,
//test : 1, // goes to model's __proto__
url: function() {
// Important! It's got to know where to send its REST calls.
// In this case, POST to '/donuts' and PUT to '/donuts/:id'
return this.id ? '/' + _collections[name].item + '/' + this.id : '/' + _collections[name].item;
},
ajaxSync: function() {},
incrementVersion: function() {
var version = this.get('__v');
console.log('version: ', version);
version = version + 1;
this.set({
__v: version
});
console.log('this.get("__v"): ', this.get('__v'));
},
initialize: function() {
var reject = ['_id', 'id', '__v'];
if (this.get('id')) {
//console.log("MODEL:: model.data." + model.storeName + "." + model.get('id') + " initialized");
} else {
//console.log("MODEL:: new model created in model.data." + model.storeName + " ");
}
//for( var attribute in this.attributes )
//{
// if( ! reject.contains( attribute ) )
// {
this.on("change", this.onChange);
// }
//}
this.on('sync', this.onSync);
this.on('error', this.onError);
this.on('destroy', this.onDestroy);
},
onSync: function(model, errors, options) {
// called on every indexedDB operation
},
onError: function(model, errors, options) {
// your code
},
onDestroy: function(model, errors, options) {
// your code
},
onChange: function(model, errors, options) {
//console.log(arguments);
//console.log('--MODEL CHANGED--');
},
});
// create backbone collection
_private[name] = Backbone.Collection.extend({
model: _private[_collections[name].item],
database: model.database,
nolog: true,
storeName: name,
url: name + ".json",
//parse: function(data) {
// return data[name];
//}
});
// create model
API[_collections[name].item] = (function(name) {
return function(s) {
//console.log(name);
//console.log(_collections[name].item);
return new _private[_collections[name].item](s);
};
})(name);
// Create collection
API[name] = (function(name) {
//console.log( name );
return function() {
//console.log(name);
//console.log(_collections[name].item);
return new _private[name]();
};
})(name);
}
}
};
return API;
}());
model.schema = (function() {
var _private = {
setup: function(c) {
var self = _private,
version = null,
version_schema,
start_data = {};
//var complete = function(){
for (version in model.versions) {
/**
* Lets use our metadata to setup backbone models
*/
// START set up backbone models
var store_names = Object.keys(model.versions[version]);
store_names.forEach(function(name) {
//console.log('store name: ', name);
var storeObj = model.versions[version][name],
collection_defaults = {};
// for every attribute/column of the storage 'defaults' metadata
for (var attributeName in storeObj.defaults) {
// attribute/column configuration metadata
var attributeObj = storeObj.defaults[attributeName];
// set defaults for collection's attributte
collection_defaults[attributeName] = attributeObj.default;
}
// set up metadata for backbone models
model.data._collections[name] = {
item: storeObj.item,
defaults: collection_defaults
};
});
// END set up backbone models
// Define backbone indexedDB adapter configuration
version_schema = {
version: parseInt(version),
migrate: function(transaction, next) {
var stores = [],
store_names = Object.keys(model.versions[version]),
total_store_to_create = store_names.length,
total_store_created = 0;
console.log('store_names: ', store_names);
store_names.forEach(function(name) {
// store configuration metadata
var storeObj = model.versions[version][name];
// create store reference as null
stores[name] = null;
// if store does not exist, let's create it
if (!transaction.db.objectStoreNames.contains(name)) {
stores[name] = transaction.db.createObjectStore(name);
}
// reference to created/existing store
stores[name] = transaction.objectStore(name);
// for every attribute/column of the storage 'defaults' metadata
for (var attributeName in storeObj.defaults) {
// attribute/column configuration metadata
var attributeObj = storeObj.defaults[attributeName];
// create an index on store
stores[name].createIndex(attributeName, attributeName, {
unique: attributeObj.unique || false
});
}
transaction.addEventListener('complete', function(event) {
total_store_created += 1;
console.log(total_store_created, total_store_to_create);
if (total_store_created == total_store_to_create) {
//if( c.onSetup ) c.onSetup();
console.log(model.data._collections);
}
});
transaction.onsuccess = function(event) {
console.log(event);
};
transaction.addEventListener('success', function(event) {
console.log(event);
});
}); // end for each
next();
}
};
// push the version_schema as an item into the database migrations array
// This version_schema will be used to set up a IndexedDB when we create
// the first Backbone model or colletion inside our application
model.database.migrations.push(version_schema);
} // end for( version in model.versions )
console.info('model.schema setup is done!');
if (c.onSetup) c.onSetup();
//self.add_initial_data();
//};
//complete();
},
_get_collection_meta: function(version, store_name) {
var self = _private,
store_names = Object.keys(model.versions[version]),
storeObj = false;
store_names.forEach(function(name) {
if (name == store_name) {
//console.log('store name: ', name);
storeObj = model.versions[version][name];
}
});
return storeObj;
},
_get_collection_item: function(version, store_name) {
var self = _private,
store_names = Object.keys(model.versions[version]),
item_name = false;
store_names.forEach(function(name) {
if (name == store_name) {
//
item_name = model.versions[version][name].item;
}
});
return item_name;
},
_set_start_data: function(data) {
var self = _private;
if (!$dhx.isObject(data)) {
alert('The start data does not have a valid format.\nApplication will not start.');
return;
}
self.start_data = data;
// self._get_start_data()
},
_get_start_data: function() {
var self = _private;
return self.start_data || {};
},
// == PubNub Syncing
_setLocalSyncTime:function(now, end){
if (parseInt(end) > 0) {
last_sync_time = $dhx.jDBdStorage.get('$dhx.mvp.ui.last_sync_time');
if (last_sync_time) {
last_sync_time = $dhx.crypt.base64_decode(last_sync_time);
if ($dhx.isNumber(parseInt(last_sync_time))) {
if (parseInt(last_sync_time) < parseInt(end)) {
console.log('updating last_sync_time as ', now);
$dhx.jDBdStorage.storeObject('$dhx.mvp.ui.last_sync_time', $dhx.crypt.base64_encode(now));
}
}
} else {
console.log('saving last_sync_time as ', now);
$dhx.jDBdStorage.storeObject('$dhx.mvp.ui.last_sync_time', $dhx.crypt.base64_encode(now));
}
}
},
msg_iterator_queue: function( m ){
_private._received_messages_queue.push( m );
},
_received_messages_queue: [],
_collections_to_sync_queue: [],
_collections_to_sync: 0,
_collections_synced: 0,
_getAllMessagesFromPubNub: function(timetoken, channel, fn) {
var self = _private,
payload = {
start: timetoken,
channel: channel,
callback: function(payload) {
var msgs = payload[0],
start = payload[1],
end = payload[2],
last_sync_time,
now = (new Date()).getTime() * 10000;
_private._setLocalSyncTime(now, end);
_.forEach(msgs, _private.msg_iterator_queue);
//console.log(msgs.length, msgs);
//console.log('------------');
if (msgs.length === 100) {
_private._getAllMessagesFromPubNub(start, channel, fn);
} else {
if(fn) fn();
}
}
};
pubnub.history( payload );
payload = null;
self = null;
},
_getLastMessagesFromPubNub: function(timetoken, channel, fn) {
var self = _private,
payload = {
end: timetoken,
channel: channel,
callback: function(payload) {
var msgs = payload[0],
start = payload[1],
end = payload[2],
last_sync_time,
now = (new Date()).getTime() * 10000;
_private._setLocalSyncTime(now, end);
_.forEach(msgs, _private.msg_iterator_queue);
//console.log(msgs.length, msgs);
//console.log('------------');
if (msgs.length === 100) {
_private._getLastMessagesFromPubNub(end, channel, fn);
} else {
if(fn) fn();
}
}
};
pubnub.history( payload );
payload = null;
self = null;
},
_process_collections_to_sync: function(c){
var collection = null,
last_sync_time = $dhx.jDBdStorage.get('$dhx.mvp.ui.last_sync_time');
if( last_sync_time )
{
// fetch all messages since last synced time
last_sync_time = parseInt( $dhx.crypt.base64_decode( last_sync_time ) );
}
else
{
// fetch all messages since the begining
last_sync_time = 0;
}
//console.log('==============> ', last_sync_time);
if( _private._collections_to_sync_queue.length > 0 )
{
collection = _private._collections_to_sync_queue.splice(0, 1);
if( last_sync_time === 0 )
{
_private._getAllMessagesFromPubNub( last_sync_time, collection, function()
{
_private._collections_synced += 1;
if (_private._collections_synced == _private._collections_to_sync) {
_private._process_received_messages( c );
return;
}
_private._process_collections_to_sync( c );
});
}
else
{
_private._getLastMessagesFromPubNub( last_sync_time, collection, function()
{
_private._collections_synced += 1;
if (_private._collections_synced == _private._collections_to_sync) {
_private._process_received_messages( c );
return;
}
_private._process_collections_to_sync( c );
});
}
}
else
{
if(c.onSuccess) c.onSuccess();
}
},
_process_received_messages: function( c ){
var initial_data = {},
set_collections_iterator = function( collection ){
initial_data[ collection ] = [];
},
read_received_messages_queue_iterator = function( message ){
//console.log( message );
if( message.method == 'create' )
{
initial_data[ message.collection ].push( message.model );
}
};
// set data collections as empty array
_.forEach(Object.keys(_private.io), set_collections_iterator);
// set collection's documents
_.forEach( _private._received_messages_queue, read_received_messages_queue_iterator );
_private._set_start_data(initial_data);
_private.add_initial_data({
onSuccess: function() {
if( c.onSuccess ) c.onSuccess();
}
});
},
sync: function( c ){
_private._collections_to_sync = Object.keys(_private.io).length;
for(var collection in _private.io)
{
_private._collections_to_sync_queue.push(collection);
}
_private._process_collections_to_sync( c );
},
// == PubNub Syncing
add_initial_data: function(c) {
var self = _private,
tables = Object.keys(self._get_start_data()),
total_tables = tables.length,
total_tables_added = 0,
models = [],
collections = [],
c_names = [],
m_names = [],
item = null,
add_all = function(done, c_names, m_names) {
//console.log( c_names );
//console.log( m_names );
var model_name = m_names.shift(),
collection_name = c_names.shift();
if (c_names.length > 0 && m_names.length > 0) {
//console.log('XXXXXXXXXX> ' + model_name, collection_name);
self.insert_records_into_table(models[model_name], collection_name, function(model, storeName) {
add_all(done, c_names, m_names);
});
} else if (c_names.length === 0 && m_names.length === 0) {
//console.log('XXXXXXXXXX> ' + model_name, collection_name);
if (typeof model_name == 'undefined' || typeof collection_name == 'undefined') {
if (done) done();
} else {
self.insert_records_into_table(models[model_name], collection_name, function(model, storeName) {
if (done) done();
});
}
} else {
if (done) done();
}
};
//console.log( tables );
tables.forEach(function(collection, index) {
item = model.schema._get_collection_item(1, collection);
models[item] = Backbone.Model.extend({
database: model.database,
storeName: collection
});
collections[collection] = Backbone.Collection.extend({
database: model.database,
storeName: collection,
model: models[item]
});
c_names.push(collection);
m_names.push(item);
});
add_all(function() {
if (c.onSuccess) c.onSuccess();
}, c_names, m_names);
},
insert_records_into_table: function(model, storeName, done) {
var self = _private,
record = self._get_start_data()[storeName].shift();
if (record) {
record.notify = false;
_private.io[storeName].create({
record: record,
onSuccess: function() {
//console.info('success inserting record: ', record);
self.insert_records_into_table(model, storeName, done);
},
onFail: function() {
//console.warn('discarding record: ', record);
//console.warn('props: ', Object.keys(record).length);
}
});
} else {
//console.log( 'done inserting records into: ', storeName );
//console.warn( ' ---------------- ' );
done(model, storeName);
}
},
start: function(c) {
var self = _private,
version = null,
version_schema;
c.onSetup = function() {
//try {
model.data._start();
/**
* indexedDB plugin for Backbone requires a initial db operation ( may be fetch ) to really create the indexedDB database
*/
model.data[Object.keys(model.data._collections)[0]]().fetch({
success: function(collection, response, options) {
console.info('The collection ' + Object.keys(model.data._collections)[0] + ' was fetched and the indexedDB is done!');
if (c.onSuccess) c.onSuccess();
//console.log( collection.models );
},
error: function(collection, response, options) {
console.log(collection);
}
});
//} catch (e) {
// console.log(e.stack);
//}
};
self.setup(c);
},
_io: {
_create: function(c) {
var jDocument = (typeof c.record === 'string' ? JSON.parse(c.record) : c.record),
m = new model.data[c.model](),
fDocument = new $dhx.ui.mvp.model.helpers.schema.record({
model: c.model,
record: jDocument
}),
save_model = function(fDocument) {
//console.warn('saving fDocument ', fDocument);
m.save(fDocument, {
success: function() {
//console.log('----------END-----------', fDocument);
if (c.onSuccess) c.onSuccess( fDocument );
if( jDocument.notify === false )
{
return;
}
$dhx.ui.Mediator.notify('changeModel', {
'collection': c.collection, // mandatory in all data events
'method': 'create', // create, update, delete,
'model': fDocument,
options: null,
client_id: $dhx.ui.session.client_id()
});
},
error: function(o, error) {
dhtmlx.alert({
text: 'Error creating '+c.model+' model data locally',
type: "alert-error"
});
if (c.onFail) c.onFail();
}
});
},
mm;
fDocument.__v = +(fDocument.__v || 0);
// if fDocument.id, then it is a update operation
if (fDocument.id) {
mm = new model.data[c.model]({
id: fDocument.id
});
mm.fetch({
success: function(mmm) {
var error = false,
emsg;
if (fDocument.__v > mmm.get('__v')) {
save_model(fDocument);
} else {
emsg = '_io._create() -> Record rejected. The version of the record is not greater than local record version.'+ fDocument;
error = {
error: emsg
};
console.warn(emsg);
if (c.onSuccess) c.onSuccess( fDocument, error);
if (c.onFail) c.onFail();
}
},
error: function(o, error) {
if (error == "Not Found") {
console.info('Document ' + fDocument.id + ' not found. I will create then.', fDocument);
save_model(fDocument);
}
}
});
}
// else it is a create operation
else {
save_model(fDocument);
}
},
_update: function(c) {
var jDocument = (typeof c.record === 'string' ? JSON.parse(c.record) : c.record),
m = new model.data[c.model]({
id: jDocument.id
}),
error = false,
emsg,
fDocument = new $dhx.ui.mvp.model.helpers.schema.record({
model: c.model,
record: jDocument
}),
actual_version = 0,
save_model = function(fDocument) {
m.save(fDocument, {
success: function() {
if (c.onSuccess) c.onSuccess( fDocument );
if( jDocument.notify === false )
{
return;
}
fDocument.id = jDocument.id;
$dhx.ui.Mediator.notify('changeModel', {
'collection': c.collection, // mandatory in all data events
'method': 'update', // create, update, delete,
'model': fDocument,
options: null,
client_id: $dhx.ui.session.client_id()
});
},
error: function(o, error) {
dhtmlx.alert({
text: 'Error updating '+c.model+' model data locally',
type: "alert-error"
});
if (c.onFail) c.onFail();
}
});
};
m.fetch({
success: function(model)
{
// delete document ids
delete fDocument.id;
delete fDocument._id;
// set document version
if (fDocument.__v) {
if (typeof fDocument.__v !== 'number') {
throw 'fDocument version(__v) shall to be a number';
}
if (fDocument.__v < 0) {
throw 'fDocument version(__v) shall to greater than zero';
}
if (fDocument.__v > model.get('__v')) {
save_model(fDocument);
} else {
emsg = '_io._update() -> Record rejected. The version of the record is not greater than local record version.'+ fDocument;
error = {
error: emsg
};
console.warn(emsg);
if (c.onSuccess) c.onSuccess( fDocument, error );
if (c.onFail) c.onFail();
}
} else {
actual_version = +(model.get('__v') || 0);
actual_version += 1;
fDocument.__v = actual_version;
save_model(fDocument);
}
},
error: function(o, error) {
if (error == "Not Found") {
emsg = ''+c.model+' '+jDocument.id+' not found';
error = {
error: emsg
};
if (c.onSuccess) c.onSuccess( fDocument, error );
dhtmlx.alert({
text: emsg,
type: "alert-error"
});
}
}
});
},
_destroy: function(c) {
var jDocument = (typeof c.record === 'string' ? JSON.parse(c.record) : c.record),
m = new model.data[c.model]({
id: jDocument.id
}),
destroy_model = function() {
m.destroy({
success: function() {
if (c.onSuccess) c.onSuccess( jDocument );
if( jDocument.notify === false )
{
return;
}
$dhx.ui.Mediator.notify('changeModel', {
'collection': c.collection, // mandatory in all data events
'method': 'destroy', // create, update, delete,
'model': jDocument,
options: null,
client_id: $dhx.ui.session.client_id()
});
},
error: function(o, error) {
dhtmlx.alert({
text: 'Error deleting '+c.model+' model data locally',
type: "alert-error"
});
if (c.onFail) c.onFail();
}
});
};
m.fetch({
success: function(model) {
destroy_model();
},
error: function(o, error) {
if (error == "Not Found") {
dhtmlx.alert({
text: ''+c.model+' '+jDocument.id+' not found',
type: "alert-error"
});
if (c.onFail) c.onFail();
}
}
});
},
_read: function(c) {
var jDocument = (typeof c.record === 'string' ? JSON.parse(c.record) : c.record),
m = new model.data[c.model]({
id: jDocument.id
});
m.fetch({
success: function(model) {
jDocument = model.toJSON();
if (c.onSuccess) c.onSuccess( model, jDocument );
},
error: function(o, error) {
if (error == "Not Found") {
dhtmlx.alert({
text: ''+c.model+' '+jDocument.id+' not found',
type: "alert-error"
});
if (c.onFail) c.onFail(o, error);
}
}
});
},
_readAll: function(c) {
var idb_payload = {
success: function( collection, response, options ) {
if (c.onSuccess) c.onSuccess( collection, response, options );
},
error: function(o, error) {
if (error == "Not Found") {
dhtmlx.alert({
text: ''+c.collection+' not found',
type: "alert-error"
});
if (c.onFail) c.onFail(o, error);
}
}
};
if( c.sort )
{
idb_payload.sort = c.sort;
}
model.data[c.collection]().fetch(idb_payload);
}
},
// IO API
io: { /* built in on the fly */ }
},
API = {
start: _private.start,
_get_collection_meta: _private._get_collection_meta,
_get_collection_item: _private._get_collection_item,
_set_start_data: _private._set_start_data,
_get_start_data: _private._get_start_data,
add_initial_data: _private.add_initial_data,
sync: _private.sync,
io: _private.io,
_io: _private._io
};
return API;
}());
return model;
}())
});