1 /** 2 @class 3 @extends rio.Attr 4 5 Model is used to create model classes in a rio application. It provides ActiveResource style synchronization with 6 a rest-based resource and automated client-server bindings with a push server. 7 8 @author Jason Tillery 9 @copyright 2008-2009 Thinklink LLC 10 */ 11 rio.Model = { 12 create: function() { 13 var args = $A(arguments); 14 if (args.length > 0 && args.last() != undefined && !args.last().ATTR) { 15 args[args.size() - 1] = Object.extend({ noExtend: true }, args.last()); 16 } 17 var model = rio.Attr.create.apply(this, args); 18 if (model._fields.id == undefined || model.prototype.setId == undefined) { 19 model.attrAccessor("id"); 20 } 21 model.attrEvent("destroy"); 22 Object.extend(model, { 23 resource: function(url) { 24 model.addMethods({ 25 isNew: function() { 26 return this.getId() == undefined || (this.getId().temporary != undefined && this.getId().temporary()); 27 }, 28 29 url: function() { 30 return model.url() + "/" + this.getId(); 31 }, 32 33 save: function(options) { 34 if (this.__destroying) { return; } 35 36 if (this.valid && !this.valid()) { return; } 37 // var idField = Object.keys(model._fields).detect(function(field) { 38 // var val = this["_" + field]; 39 // return field != "id" && val && val.temporary && val.temporary(); 40 // }.bind(this)); 41 // if (idField) { 42 // this["_" + idField].doAfterReification(this.save.curry(options).bind(this)); 43 // return; 44 // } 45 46 // if (this._creating) { 47 // if (!this._pendingUpdates) { this._pendingUpdates = []; } 48 // this._pendingUpdates.push(this.save.curry(options).bind(this)); 49 // return; 50 // } 51 var firstTimeCreating = !this._creating; 52 this._creating = true; 53 if(this.isNew() && this.beforeCreate && firstTimeCreating){ this.beforeCreate(); } 54 55 model.addToTransaction(this, options); 56 }, 57 58 destroy: function(options) { 59 if(this.__destroying) {return;} 60 61 if (this.beforeDestroy) { this.beforeDestroy(); } 62 63 this.__destroying = true; 64 65 model.addToTransaction(this, Object.extend({destroy: true}, options)); 66 this.removeFromCaches(); 67 68 this.fire("destroy"); 69 }, 70 71 parameters: function() { 72 var modelKey = model.url().match(/^\/?(.*)$/)[1].singularize(); 73 var parameters = {}; 74 75 var persistentFieldNames = model.persistentFieldNames(); 76 for (var i=persistentFieldNames.length; i--;) { 77 var name = persistentFieldNames[i]; 78 var currentState = this["_" + name]; 79 var lastState = this._lastSavedState ? this._lastSavedState[name] : undefined; 80 81 if (currentState != lastState) { 82 parameters[modelKey + "[" + name.underscore() + "]"] = currentState; 83 } 84 } 85 return parameters; 86 }, 87 88 afterUpdateField: function() { 89 model.updateInCollectionEntites(this); 90 }, 91 92 attributeState: function() { 93 var state = {}; 94 95 var persistentFieldNames = model.persistentFieldNames(); 96 for (var i=persistentFieldNames.length; i--;) { 97 var f = persistentFieldNames[i]; 98 state[f] = this[("get-" + f).camelize()](); 99 } 100 return state; 101 }, 102 103 attributeStateChange: function() { 104 var state = {}; 105 106 var persistentFieldNames = model.persistentFieldNames(); 107 for (var i=persistentFieldNames.length; i--;) { 108 var f = persistentFieldNames[i]; 109 var currentState = this["_" + f]; 110 var lastState = this._lastSavedState ? this._lastSavedState[f] : undefined; 111 if (currentState != lastState) { 112 state[f] = currentState; 113 } 114 } 115 116 return state; 117 }, 118 119 removeFromCaches: function() { 120 model.removeFromCollectionEntities(this); 121 model.removeFromCache(this.getId()); 122 }, 123 124 toString: function() { 125 return "[rio.models.*]"; 126 } 127 }); 128 129 Object.extend(model, { 130 undoEnabled: false, 131 132 url: function() { 133 return url; 134 }, 135 136 _idCache: {}, 137 id: function(val, createIfNotFound) { 138 if (val) { 139 if (this._idCache[val]) { return this._idCache[val]; } 140 if (createIfNotFound) { 141 var id = new rio.Id(val); 142 this._idCache[val] = id; 143 return id; 144 } 145 } else { 146 return new rio.Id(); 147 } 148 }, 149 150 reifyId: function(id, val) { 151 id.reify(val); 152 if (this._idCache[val]) { 153 rio.warn("id collision while reifying - " + model + "#" + id); 154 } 155 this._idCache[val] = id; 156 }, 157 158 persistentFieldNames: function() { 159 return Object.keys(model._fields).reject(function(f) { 160 return model._clientOnlyAttrs.include(f) || model.prototype[("set-" + f).camelize()] == undefined || f == "id"; 161 }); 162 }, 163 164 _transaction: [], 165 addToTransaction: function(entity, options) { 166 options = options || {}; 167 var existingTransaction = this._transaction.detect(function(t) { return t.entity == entity; }); 168 if (existingTransaction) { 169 if (options.destroy) { 170 if (existingTransaction.entity.isNew()) { 171 this._transaction.splice(this._transaction.indexOf(existingTransaction), 1); 172 // don't need to do this, now that we immediately remove on destroy 173 // model.removeFromCollectionEntities(existingTransaction.entity); 174 } else { 175 existingTransaction.options = options; 176 } 177 } else { 178 existingTransaction.attributeState = entity.attributeStateChange(); 179 existingTransaction.parameters = entity.parameters(); 180 } 181 // TODO: chain onSuccess/onFailure functions 182 } else { 183 this._transaction.push({ 184 entity: entity, 185 options: options, 186 attributeState: entity.attributeStateChange(), 187 parameters: entity.parameters() 188 }); 189 } 190 if (!this._transactionQueued) { 191 this._transactionQueued = true; 192 this.prepareTransaction(); 193 } 194 }, 195 196 /* Simply defers execution. Can override this for testing. */ 197 prepareTransaction: function() { 198 this.executeTransaction.bind(this, rio.Undo.isProcessingUndo(), rio.Undo.isProcessingRedo()).defer(); 199 }, 200 201 __transactionInProgress: false, 202 __queuedTransactions: [], 203 executeTransaction: function(undoTransaction, redoTransaction) { 204 this._transactionQueued = false; 205 206 var transaction = this._transaction.clone(); 207 this._transaction.clear(); 208 209 // Transactions can be queued here, so lets make sure that all entities _lastSavedState's are 210 // correct before letting subsequent transaction queue up. 211 for (var i=0, len=transaction.length; i<len; i++) { 212 var t = transaction[i]; 213 t.oldLastSavedState = t.entity._lastSavedState; 214 t.entity._lastSavedState = t.options.destroy ? undefined : t.entity.attributeState(); 215 } 216 217 transaction.undoTransaction = undoTransaction; 218 transaction.redoTransaction = redoTransaction; 219 if (this.__transactionInProgress) { 220 this.__queuedTransactions.push(transaction); 221 return; 222 } else { 223 this._doExecuteTransaction(transaction); 224 } 225 }, 226 227 _doExecuteTransaction: function(transaction) { 228 if (transaction.empty()) { return; } 229 230 if (model.undoEnabled) { 231 var undos = transaction.map(function(t) { 232 var undo = { id: t.entity.getId(), state: t.oldLastSavedState, destroy: t.options.destroy }; 233 // t.entity._lastSavedState = t.options.destroy ? undefined : t.entity.attributeState(); 234 return undo; 235 }); 236 237 var processUndos = function() { 238 undos.reverse().each(function(u) { 239 if (u.state) { 240 if (u.destroy) { 241 var instance = new model(Object.extend({ id: u.id }, u.state)); 242 instance._lastSavedState = undefined; 243 instance.save(); 244 } else { 245 model.getFromCache(u.id).updateAttributes(u.state); 246 } 247 } else { 248 model.getFromCache(u.id).destroy(); 249 } 250 }); 251 }; 252 253 if (transaction.undoTransaction) { 254 rio.Undo.registerRedo(processUndos); 255 } else { 256 rio.Undo.registerUndo(processUndos, transaction.redoTransaction); 257 } 258 } 259 260 // // Always update the last saved states 261 // for (var i=0, len=transaction.length; i<len; i++) { 262 // var t = transaction[i]; 263 // 264 // // LOOKS LIKE A BUG 265 // // TODO: SHOULD PROBABLY BE USING THE t.attributeState instead of t.entity.attributeState() 266 // t.entity._lastSavedState = t.options.destroy ? undefined : t.entity.attributeState(); 267 // } 268 269 var createSuccess = function(entity, onSuccess, json) { 270 entity._creating = false; 271 var results = model._filterAndProcessJsonWhileAccumulatingCollectionEntities(json); 272 var idString = results[0].id; 273 if (entity.getId() && entity.getId().temporary && entity.getId().temporary()) { 274 var id = parseInt(idString, 10); 275 model.reifyId(entity.getId(), id); 276 } else { 277 // NOTE: An entity will not be updated with any attributes but the ID on create success 278 entity._id = model.id(parseInt(idString, 10), true); 279 model.putInCache(entity._id, entity); 280 } 281 onSuccess(entity); 282 if (entity.afterCreate) { entity.afterCreate(); } 283 // if (entity._pendingUpdates) { 284 // entity._pendingUpdates.each(function(u) { u(); }); 285 // } 286 return results[1]; 287 }; 288 289 var updateSuccess = function(entity, onSuccess) { 290 onSuccess(entity); 291 }; 292 293 var destroySuccess = function(entity, onSuccess) { 294 // don't need to do this, now that we immediately remove on destroy 295 // model.removeFromCollectionEntities(entity); 296 (onSuccess || Prototype.emptyFunction)(); 297 }; 298 299 var url; 300 var method; 301 var parameters; 302 var onSuccess; 303 var onFailure; 304 var onConnectionFailure; 305 306 if (transaction.length > 1) { 307 url = model.url(); 308 method = "post"; 309 310 parameters = {}; 311 transaction.each(function(t) { 312 var id = t.entity.getId(); 313 if (t.options.destroy) { 314 parameters["transaction[" + id + "]"] = "delete"; 315 } else { 316 for (var f in t.attributeState) { 317 var val = t.attributeState[f]; 318 if (val && val.toString) { val = val.toString(); } 319 parameters["transaction[" + id + "][" + f.underscore() + "]"] = val; 320 } 321 } 322 }); 323 324 onSuccess = function(response) { 325 var afterFunctions = transaction.map(function(t) { 326 var entity = t.entity; 327 var options = t.options; 328 329 options = options || {}; 330 options.onSuccess = options.onSuccess || Prototype.emptyFunction; 331 332 var json = response.responseJSON.transaction[t.entity.getId()]; 333 return ( 334 options.destroy ? destroySuccess : (entity.isNew() ? createSuccess : updateSuccess) 335 )(entity, options.onSuccess, json); 336 }); 337 338 afterFunctions.compact().each(function(af) { 339 af(); 340 }); 341 }.bind(this); 342 343 onFailure = function() { 344 var handled = true; 345 transaction.each(function(t) { 346 if (t.options.onFailure) { 347 t.options.onFailure(); 348 } else { 349 handled = false; 350 } 351 }); 352 if (!handled) { 353 rio.Application.fail("Failed creating, updating, or destroying.", model + "\n" + Object.toJSON(parameters)); 354 } 355 }.bind(this); 356 357 onConnectionFailure = function() { 358 transaction.each(function(t) { 359 if (t.options.onConnectionFailure) { 360 t.options.onConnectionFailure(); 361 } 362 }); 363 }; 364 } else { 365 var entity = transaction.first().entity; 366 var options = transaction.first().options; 367 368 options = options || {}; 369 options.onSuccess = options.onSuccess || Prototype.emptyFunction; 370 371 parameters = options.parameters || transaction.first().parameters; 372 url = entity.isNew() ? model.url() : entity.url(); 373 method = options.destroy ? "delete" : (entity.isNew() ? "post" : "put"); 374 onSuccess = function(response) { 375 var after = ( 376 options.destroy ? destroySuccess : (entity.isNew() ? createSuccess : updateSuccess) 377 )(entity, options.onSuccess, response.responseJSON); 378 if (after) { after(); } 379 }; 380 onFailure = options.onFailure; 381 onConnectionFailure = options.onConnectionFailure; 382 } 383 384 this.__transactionInProgress = true; 385 new Ajax.Request(url, { 386 asynchronous: true, 387 method: method, 388 evalJSON: true, 389 parameters: $H(parameters).merge({ 390 'transaction_key': rio.environment.transactionKey, 391 'authenticity_token': rio.Application.getToken() 392 }).toObject(), 393 onSuccess: function(response) { 394 if (response.status == 0) { 395 if (onConnectionFailure) { 396 onConnectionFailure(); 397 } else { 398 rio.Application.fail("Connection Failure", model + ":\n" + Object.toJSON(parameters)); 399 } 400 } else { 401 onSuccess(response); 402 } 403 }, 404 onFailure: function(response) { 405 // if (!entity.__destroying) { 406 if (onFailure) { 407 onFailure(response); 408 } else { 409 rio.Application.fail("Failed creating, updating, or destroying.", model + ":\n" + Object.toJSON(parameters)); 410 } 411 // } 412 }.bind(this), 413 onComplete: function(response) { 414 this.__transactionInProgress = false; 415 if (!this.__queuedTransactions.empty()) { 416 this._doExecuteTransaction(this.__queuedTransactions.shift()); 417 } 418 }.bind(this) 419 }); 420 421 transaction.each(function(t) { 422 if (t.entity.isNew()) { 423 t.entity._creating = true; 424 } 425 if (t.options.destroy) { 426 t.entity.__destroying = true; 427 } 428 }); 429 }, 430 431 create: function(options) { 432 var obj = new model(options); 433 obj.save(options); 434 return obj; 435 }, 436 437 find: function(id, options) { 438 if (id == undefined) { return; } 439 var rioId = id.constructor == rio.Id ? id : model.id(id); 440 441 options = options || {}; 442 var asynchronous = options.asynchronous == undefined || options.asynchronous; 443 444 if (options.onSuccess == undefined) { 445 asynchronous = false; 446 options.onSuccess = Prototype.emptyFunction; 447 } 448 449 var existing = this.getFromCache(rioId); 450 if (existing) { 451 options.onSuccess(existing); 452 return existing; 453 } 454 455 var entity; 456 457 new Ajax.Request(this.url() + "/" + id, { 458 asynchronous: asynchronous, 459 method: 'get', 460 evalJSON: true, 461 onSuccess: function(response) { 462 entity = new model(model._filterAndProcessJson(response.responseJSON)); 463 options.onSuccess(entity); 464 }, 465 onFailure: options.onFailure || Prototype.emptyFunction 466 }); 467 if (!asynchronous) { 468 return entity; 469 } 470 }, 471 472 findAll: function(options) { 473 options = options || {}; 474 var asynchronous = options.asynchronous == undefined || options.asynchronous; 475 476 if (options.onSuccess == undefined) { 477 asynchronous = false; 478 options.onSuccess = Prototype.emptyFunction; 479 } 480 481 var idField = Object.keys(options.parameters).detect(function(parameter) { 482 var val = options.parameters[parameter]; 483 return val && val.temporary && val.temporary(); 484 }); 485 486 var urlToUse = options.url || this.url(); 487 var parameters = new rio.Parameters(options.parameters || {}, options.nonAjaxParameters); 488 // Assume that there are no entities yet (since a param is unreified) 489 // New entities will be added to the collectionEntity as they are created or broadcast 490 // If this poses a problem, we can always schedule a find for after reification and then reconcile the CE's 491 if (idField) { 492 var collectionEntity = rio.CollectionEntity.create({ 493 model: model, 494 values: [], 495 condition: parameters.conditionFunction() 496 }); 497 this.putCollectionEntity(urlToUse + "#" + Object.toJSON(parameters.ajaxParameters()), collectionEntity); 498 499 Object.values(model._identityCache).each(function(entity) { 500 collectionEntity.add(entity); 501 }); 502 503 options.onSuccess(collectionEntity); 504 return collectionEntity; 505 } 506 507 508 if (this._collectionEntities[urlToUse + "#" + Object.toJSON(parameters.ajaxParameters())]) { 509 var found = this._collectionEntities[urlToUse + "#" + Object.toJSON(parameters.ajaxParameters())]; 510 found.prepare(); 511 options.onSuccess(found); 512 if (!asynchronous) { 513 return found; 514 } else { 515 return; 516 } 517 } 518 519 var results; 520 rio.Model._findAllRequests.push( 521 new Ajax.Request(urlToUse, { 522 asynchronous: asynchronous, 523 method: 'get', 524 evalJSON: true, 525 parameters: {conditions: Object.toJSON(parameters.ajaxParameters())}, 526 onSuccess: function(response) { 527 results = this._collectionEntityFromJson(response.responseJSON, parameters, urlToUse); 528 results.prepare(); 529 options.onSuccess(results); 530 }.bind(this) 531 }) 532 ); 533 if (!asynchronous) { 534 return results; 535 } 536 }, 537 538 _hasManyAssociations: {}, 539 hasMany: function(hasManyName) { 540 var options = {}; 541 if (!Object.isString(hasManyName)) { 542 options = hasManyName[1] || {}; 543 hasManyName = hasManyName[0]; 544 } 545 options.className = hasManyName.singularize().classize(); 546 options.foreignKey = this.NAME.toLowerCase() + "Id"; 547 options.parameters = options.parameters || {}; 548 549 this._hasManyAssociations[hasManyName] = options; 550 551 this.attrAccessor(hasManyName); 552 this.clientOnlyAttr(hasManyName); 553 554 var getName = ("get-" + hasManyName).camelize(); 555 556 this.prototype[getName] = this.prototype[getName].wrap(function(proceed) { 557 if (this["_" + hasManyName] == undefined) { 558 var parameters = Object.clone(options.parameters); 559 parameters[options.foreignKey] = this.getId(); 560 rio.models[options.className].findAll({ 561 asynchronous: false, 562 parameters: parameters, 563 onSuccess: function(entities) { 564 this["_" + hasManyName] = entities; 565 }.bind(this) 566 }); 567 } 568 return proceed.apply(this, $A(arguments).slice(1)); 569 }); 570 }, 571 572 _belongsToAssociations: {}, 573 belongsTo: function(args) { 574 var options = {}; 575 var associationName; 576 if (!Object.isString(args)) { 577 associationName = args[0]; 578 options = args[1] || {}; 579 } else { 580 associationName = args; 581 } 582 options.className = options.className || associationName.classize(); 583 options.foreignKey = options.foreignKey || associationName + "Id"; 584 585 this._belongsToAssociations[associationName] = options; 586 587 this.attrAccessor(associationName); 588 this.clientOnlyAttr(associationName); 589 590 var getName = ("get-" + associationName).camelize(); 591 592 this.prototype[getName] = this.prototype[getName].wrap(function(proceed) { 593 if (this["_" + associationName] == undefined) { 594 var setAssociation = function() { 595 var associationId = this["_" + options.foreignKey]; 596 var foundValue = rio.models[options.className].find(associationId, { 597 asynchronous: false 598 }); 599 this[("set-" + associationName).camelize()](foundValue); 600 }.bind(this); 601 setAssociation(); 602 this[options.foreignKey].bind(setAssociation, true); 603 } 604 return proceed.apply(this, $A(arguments).slice(1)); 605 }); 606 }, 607 608 _parametersFromJsonParameters: function(params) { 609 return new rio.Parameters( 610 Object.keys(params).inject({}, function(acc, p) { 611 acc[p.camelize()] = params[p]; 612 return acc; 613 }) 614 ); 615 }, 616 617 _processIncludedCollectionEntities: function(json) { 618 for (var i=json.length; i--;) { 619 var include = json[i]; 620 params = this._parametersFromJsonParameters(include.parameters); 621 var url = rio.models[include.className].url(); 622 if (this._collectionEntities[url + "#" + Object.toJSON(params.ajaxParameters())] == undefined) { 623 rio.models[include.className]._collectionEntityFromJson(include.json, params, url); 624 } 625 } 626 }, 627 628 _filterAndProcessJsonWhileAccumulatingCollectionEntities: function(inJson) { 629 if (inJson._set) { 630 var ceFunction = function() { 631 model._processIncludedCollectionEntities(inJson._set.include); 632 }; 633 return [rio.Model.filterJson(inJson._set.self), ceFunction]; 634 } else { 635 return [rio.Model.filterJson(inJson), Prototype.emptyFunction]; 636 } 637 }, 638 639 _filterAndProcessJson: function(inJson) { 640 var results = model._filterAndProcessJsonWhileAccumulatingCollectionEntities(inJson); 641 results[1](); 642 return results[0]; 643 }, 644 645 _collectionEntityFromJson: function(json, parameters, url) { 646 // In case multiple identical queries were made simultaneously 647 if (this._collectionEntities[url + "#" + Object.toJSON(parameters.ajaxParameters())]) { 648 return this._collectionEntities[url + "#" + Object.toJSON(parameters.ajaxParameters())]; 649 } 650 651 var results = json.map(function(result) { 652 // This may cause bugs. It should map the results of _filterAndProcessJsonWhileAccumulatingCollectionEntities 653 // and execute them after them map to prevent reification collisions 654 var modelJson = model._filterAndProcessJson(result); 655 var fromCache = this.getFromCache(model.id(modelJson.id)); 656 if (fromCache) { 657 return fromCache; 658 } else { 659 var lazyNew = function() { 660 var fromCache = this.getFromCache(model.id(modelJson.id)); 661 if (fromCache) { return fromCache; } 662 return new model(modelJson); 663 }.bind(this); 664 lazyNew.__lazyNew = true; 665 return lazyNew; 666 } 667 }.bind(this)); 668 669 var collectionEntity = rio.CollectionEntity.create({ 670 model: model, 671 values: results, 672 condition: parameters.conditionFunction() 673 }); 674 collectionEntity.prepare = function() { 675 collectionEntity.prepare = Prototype.emptyFunction; 676 // prevent the initialization from double adding the entities 677 var oldAdd = this.add; 678 try { 679 var i; 680 this.add = Prototype.emptyFunction; 681 var timesToLoop = this.length; 682 var toRemove = []; 683 for (i=0; i<timesToLoop; i++) { 684 if (this[i].__lazyNew) { 685 var modelInstance = this[i](); 686 if (!this.include(modelInstance)) { 687 this[i] = modelInstance; 688 } else { 689 toRemove.push(i); 690 } 691 } 692 } 693 for (i=toRemove.length; i--;) { 694 this.splice(toRemove[i], 1); 695 } 696 } finally { 697 this.add = oldAdd; 698 } 699 }; 700 (function() { 701 // Ultimately we shouldn't need to do this 702 // This is required if when parsing eager loaded json collections 703 // contain individual entities that need to be added to other collection entities 704 // and the entity representing the eager collection is never loaded, hence prepared. 705 collectionEntity.prepare(); 706 }.defer()); 707 this.putCollectionEntity(url + "#" + Object.toJSON(parameters.ajaxParameters()), collectionEntity); 708 709 return collectionEntity; 710 }, 711 712 _clientOnlyAttrs: [], 713 714 clientOnlyAttr: function(attrName) { 715 this._clientOnlyAttrs.push(attrName); 716 }, 717 718 _identityCache: {}, 719 720 getFromCache: function(id) { 721 if (id == undefined) { return; } 722 return this._identityCache[id.cacheKey ? id.cacheKey() : id] || this._identityCache[model.id(id) && model.id(id).cacheKey()]; 723 }, 724 725 putInCache: function(id, value) { 726 var cacheKey = id.cacheKey ? id.cacheKey() : id; 727 if (this._identityCache[cacheKey] == value) { 728 return; 729 } 730 this._identityCache[cacheKey] = value; 731 this.addToCollectionEntities(value); 732 }, 733 734 removeFromCache: function(id) { 735 var cacheKey = id.cacheKey ? id.cacheKey() : id; 736 delete this._identityCache[cacheKey]; 737 }, 738 739 _collectionEntities: {}, 740 putCollectionEntity: function(key, value) { 741 this._collectionEntities[key] = value; 742 }, 743 744 addToCollectionEntities: function(value) { 745 var ces = this._collectionEntities; 746 for (var key in ces) { 747 ces[key].add(value); 748 } 749 }, 750 751 updateInCollectionEntites: function(value) { 752 var ces = this._collectionEntities; 753 for (var key in ces) { 754 ces[key].update(value); 755 } 756 }, 757 758 removeFromCollectionEntities: function(value) { 759 Object.values(this._collectionEntities).each(function(collectionEntity) { 760 collectionEntity.remove(value); 761 }); 762 } 763 764 }); 765 }, 766 767 hasChannel: function() { 768 return false; 769 }, 770 771 channel: function() { 772 model.addMethods({ 773 broadcast: function() { 774 var args = $A(arguments); 775 var methodName = args.shift(); 776 var body = { 777 id: this.getId().toString(), 778 method: methodName, 779 args: args 780 }; 781 782 new Ajax.Request("/push/broadcast", { 783 asynchronous: true, 784 method: "get", 785 evalJSON: false, 786 evalJS: false, 787 parameters: { 788 channel: this.channelName(), 789 message: Object.toJSON(body) 790 } 791 }); 792 }, 793 794 channelName: function() { 795 return model + "." + this.getId(); 796 } 797 }); 798 799 Object.extend(model, { 800 hasChannel: function() { return true; }, 801 802 receiveBroadcast: function(options) { 803 var instance = model.getFromCache(model.id(options.id)); 804 if (instance) { 805 instance[options.method].apply(instance, options.args); 806 } 807 } 808 }); 809 } 810 }); 811 812 if (args.length > 0 && args.last() != undefined && !args.last().ATTR) { 813 var initializers = args.last(); 814 if (Object.isString(initializers.resource)) { 815 model.resource(initializers.resource); 816 } 817 if (initializers.channel) { 818 model.channel(); 819 } 820 821 if (initializers.hasMany) { 822 initializers.hasMany.each(function(h) { 823 model.hasMany(h); 824 }); 825 } 826 827 if (initializers.belongsTo) { 828 initializers.belongsTo.each(function(h) { 829 model.belongsTo(h); 830 }); 831 } 832 833 (initializers.clientOnlyAttrs || []).each(function(clientOnlyAttr) { 834 model.clientOnlyAttr(clientOnlyAttr); 835 }); 836 837 if (initializers.undoEnabled) { 838 model.undoEnabled = true; 839 } 840 841 rio.Model.extend(model, initializers.methods || {}); 842 } 843 844 return model; 845 }, 846 847 _findAllRequests: [], 848 afterActiveQueries: function(fcn) { 849 var count = 0; 850 this._findAllRequests.each(function(far) { 851 if (far._complete) { return; } 852 count++; 853 far.options.onComplete = (far.options.onComplete || Prototype.emptyFunction).wrap(function(proceed) { 854 var ret = proceed(arguments); 855 count--; 856 if (count == 0) { fcn(); } 857 return ret; 858 }); 859 }); 860 if (count == 0) { fcn(); } 861 }, 862 863 filterJson: function(json) { 864 if (json.attributes) { return json.attributes; } 865 return (rio.environment.includeRootInJson) ? Object.values(json)[0] : json; 866 }, 867 868 remoteCreate: function(options) { 869 if (options.transactionKey == rio.environment.transactionKey) { return; } 870 try { 871 var results = rio.Model.doRemoteCreate(options); 872 if (results.undoFunction) { 873 rio.Undo.registerUndo(results.undoFunction); 874 } 875 } catch(e) { 876 rio.error(e, "Remote create error!"); 877 } 878 }, 879 880 remoteUpdate: function(options) { 881 if (options.transactionKey == rio.environment.transactionKey) { return; } 882 try { 883 var results = rio.Model.doRemoteUpdate(options); 884 if (results.undoFunction) { 885 rio.Undo.registerUndo(results.undoFunction); 886 } 887 } catch(e) { 888 rio.error(e, "Remote update error!"); 889 } 890 }, 891 892 remoteDestroy: function(options) { 893 if (options.transactionKey == rio.environment.transactionKey) { return; } 894 try { 895 var results = rio.Model.doRemoteDestroy(options); 896 if (results.undoFunction) { 897 rio.Undo.registerUndo(results.undoFunction); 898 } 899 } catch(e) { 900 rio.error(e, "Remote destroy error!"); 901 } 902 }, 903 904 doRemoteCreate: function(options) { 905 var model = rio.models[options.name]; 906 var fromCache = model.getFromCache(model.id(options.id)); 907 if (!fromCache) { 908 fromCache = new model(model._filterAndProcessJson(options.json)); 909 } else { 910 return {}; 911 } 912 913 return { 914 undoFunction: model.undoEnabled ? function() { 915 model.getFromCache(fromCache.getId()).destroy(); 916 } : null 917 }; 918 }, 919 920 doRemoteUpdate: function(options) { 921 var model = rio.models[options.name]; 922 var instance = model.getFromCache(model.id(options.id)); 923 if (!instance) { 924 return {}; 925 } 926 927 var json = model._filterAndProcessJson(options.json); 928 929 var results = {}; 930 931 if (model.undoEnabled) { 932 var oldState = instance.attributeState(); 933 var processUndo = function() { 934 model.getFromCache(instance.getId()).updateAttributes(oldState); 935 }; 936 results.undoFunction = processUndo; 937 } 938 939 var attributes = Object.keys(json).without("id").inject({}, function(acc, k) { 940 acc[k.camelize()] = json[k]; 941 return acc; 942 }); 943 instance.updateAttributes(attributes, { skipSave: true }); 944 rio.models[options.name].updateInCollectionEntites(instance); 945 946 instance._lastSavedState = instance.attributeState(); 947 948 return results; 949 }, 950 951 doRemoteDestroy: function(options) { 952 var model = rio.models[options.name]; 953 var fromCache = model.getFromCache(model.id(options.id)); 954 var results = {}; 955 956 if (fromCache) { 957 if (model.undoEnabled) { 958 var oldState = fromCache.attributeState(); 959 var processUndo = function() { 960 var instance = new model(Object.extend({id: fromCache.getId()}, oldState)); 961 instance._lastSavedState = undefined; 962 instance.save(); 963 }; 964 results.undoFunction = processUndo; 965 } 966 967 fromCache.removeFromCaches(); 968 969 fromCache.fire("destroy"); 970 } 971 972 return results; 973 }, 974 975 remoteTransaction: function(transactionData) { 976 if (transactionData.transactionKey == rio.environment.transactionKey) { return; } 977 try { 978 // TODO: Two potential bugs here 979 // 980 // 1) Need to execute this in a rio.Attr.transaction 981 // 2) Need to get back the collection entity process functions from _filterAndProcessJsonWhileAccumulatingCollectionEntities 982 // and run them at the end. Otherwise we might have reification collisions. 983 var resultSets = transactionData.transaction.map(function(t) { 984 switch(t.action) { 985 case "create": 986 return rio.Model.doRemoteCreate(t); 987 case "update": 988 return rio.Model.doRemoteUpdate(t); 989 case "destroy": 990 return rio.Model.doRemoteDestroy(t); 991 } 992 }); 993 994 var undos = resultSets.map(function(r) { return r.undoFunction; }).compact(); 995 996 if (!undos.empty()) { 997 rio.Undo.registerUndo(function() { 998 undos.each(function(undo) { 999 undo(); 1000 }); 1001 }); 1002 } 1003 } catch(e) { 1004 rio.error(e, "Remote transaction error!"); 1005 } 1006 }, 1007 1008 extend: function(model, extension) { 1009 1010 extension.__initialize = extension.initialize || Prototype.emptyFunction; 1011 extension.initialize = function(options) { 1012 this.__model = model; 1013 1014 if (options.id == undefined && model.url) { 1015 options.id = model.id(); 1016 this._id = options.id; 1017 } 1018 1019 if (options.id && options.id.constructor != rio.Id && model.id) { 1020 options.id = model.id(options.id, true); 1021 this.setId(options.id); 1022 } 1023 1024 // Set assiated id's from belongsTo associations 1025 Object.keys(model._belongsToAssociations).each(function(belongsToName) { 1026 var associationValue = options[belongsToName]; 1027 if (associationValue) { 1028 var belongsToOptions = model._belongsToAssociations[belongsToName]; 1029 this["_" + belongsToOptions.foreignKey] = associationValue.getId(); 1030 } 1031 }.bind(this)); 1032 1033 (this.__initialize.bind(this))(options); 1034 1035 // Add the hasManyAssocitation#create methods 1036 Object.keys(model._hasManyAssociations).each(function(hasManyName) { 1037 var options = model._hasManyAssociations[hasManyName]; 1038 this[hasManyName].create = function(createOptions) { 1039 // In the case that the entity is still new, we should preload the association 1040 // to make sure no entries are missed 1041 // TODO: before we can wrap in isNew, we need to make the spec fixture proxy include created entities 1042 // if (this.isNew()) { 1043 this[("get-" + hasManyName).camelize()](); 1044 // } 1045 var parameters = Object.clone(options.parameters); 1046 parameters[options.foreignKey] = this.getId(); 1047 return rio.models[options.className].create(Object.extend(createOptions || {}, parameters)); 1048 }.bind(this); 1049 }.bind(this)); 1050 1051 if (model.hasChannel()) { 1052 if (rio.push) { 1053 var addChannel = function() { 1054 rio.push.addChannel(this.channelName()); 1055 }.bind(this); 1056 if (options.id && options.id.temporary()) { 1057 options.id.doAfterReification(addChannel); 1058 } else { 1059 addChannel(); 1060 } 1061 } else { 1062 rio.warn("Attempted to add a channel without an available push server"); 1063 } 1064 } 1065 1066 options = options || {}; 1067 if (options.id && model.url) { 1068 model.putInCache(options.id, this); 1069 } 1070 if (options.id && options.id.temporary && !options.id.temporary()) { 1071 this._lastSavedState = this.attributeState(); 1072 } 1073 }; 1074 1075 rio.Attr.extend(model, extension); 1076 }, 1077 1078 toString: function() { 1079 return "Model"; 1080 } 1081 };