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() { 405 // if (!entity.__destroying) { 406 if (onFailure) { 407 onFailure(); 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 }); 466 if (!asynchronous) { 467 return entity; 468 } 469 }, 470 471 findAll: function(options) { 472 options = options || {}; 473 var asynchronous = options.asynchronous == undefined || options.asynchronous; 474 475 if (options.onSuccess == undefined) { 476 asynchronous = false; 477 options.onSuccess = Prototype.emptyFunction; 478 } 479 480 var idField = Object.keys(options.parameters).detect(function(parameter) { 481 var val = options.parameters[parameter]; 482 return val && val.temporary && val.temporary(); 483 }); 484 485 var urlToUse = options.url || this.url(); 486 var parameters = new rio.Parameters(options.parameters || {}, options.nonAjaxParameters); 487 // Assume that there are no entities yet (since a param is unreified) 488 // New entities will be added to the collectionEntity as they are created or broadcast 489 // If this poses a problem, we can always schedule a find for after reification and then reconcile the CE's 490 if (idField) { 491 var collectionEntity = rio.CollectionEntity.create({ 492 model: model, 493 values: [], 494 condition: parameters.conditionFunction() 495 }); 496 this.putCollectionEntity(urlToUse + "#" + Object.toJSON(parameters.ajaxParameters()), collectionEntity); 497 498 Object.values(model._identityCache).each(function(entity) { 499 collectionEntity.add(entity); 500 }); 501 502 options.onSuccess(collectionEntity); 503 return collectionEntity; 504 } 505 506 507 if (this._collectionEntities[urlToUse + "#" + Object.toJSON(parameters.ajaxParameters())]) { 508 var found = this._collectionEntities[urlToUse + "#" + Object.toJSON(parameters.ajaxParameters())]; 509 found.prepare(); 510 options.onSuccess(found); 511 if (!asynchronous) { 512 return found; 513 } else { 514 return; 515 } 516 } 517 518 var results; 519 rio.Model._findAllRequests.push( 520 new Ajax.Request(urlToUse, { 521 asynchronous: asynchronous, 522 method: 'get', 523 evalJSON: true, 524 parameters: {conditions: Object.toJSON(parameters.ajaxParameters())}, 525 onSuccess: function(response) { 526 results = this._collectionEntityFromJson(response.responseJSON, parameters, urlToUse); 527 results.prepare(); 528 options.onSuccess(results); 529 }.bind(this) 530 }) 531 ); 532 if (!asynchronous) { 533 return results; 534 } 535 }, 536 537 _hasManyAssociations: {}, 538 hasMany: function(hasManyName) { 539 var options = {}; 540 if (!Object.isString(hasManyName)) { 541 options = hasManyName[1] || {}; 542 hasManyName = hasManyName[0]; 543 } 544 options.className = hasManyName.singularize().classize(); 545 options.foreignKey = this.NAME.toLowerCase() + "Id"; 546 options.parameters = options.parameters || {}; 547 548 this._hasManyAssociations[hasManyName] = options; 549 550 this.attrAccessor(hasManyName); 551 this.clientOnlyAttr(hasManyName); 552 553 var getName = ("get-" + hasManyName).camelize(); 554 555 this.prototype[getName] = this.prototype[getName].wrap(function(proceed) { 556 if (this["_" + hasManyName] == undefined) { 557 var parameters = Object.clone(options.parameters); 558 parameters[options.foreignKey] = this.getId(); 559 rio.models[options.className].findAll({ 560 asynchronous: false, 561 parameters: parameters, 562 onSuccess: function(entities) { 563 this["_" + hasManyName] = entities; 564 }.bind(this) 565 }); 566 } 567 return proceed.apply(this, $A(arguments).slice(1)); 568 }); 569 }, 570 571 belongsTo: function(args) { 572 var options = {}; 573 var associationName; 574 if (!Object.isString(args)) { 575 associationName = args[0]; 576 options = args[1] || {}; 577 } else { 578 associationName = args; 579 } 580 this.attrAccessor(associationName); 581 this.clientOnlyAttr(associationName); 582 583 var className = options.className || associationName.classize(); 584 var foreignKey = options.foreignKey || associationName + "Id"; 585 var getName = ("get-" + associationName).camelize(); 586 587 this.prototype[getName] = this.prototype[getName].wrap(function(proceed) { 588 if (this["_" + associationName] == undefined) { 589 var setAssociation = function() { 590 var associationId = this["_" + foreignKey]; 591 var foundValue = rio.models[className].find(associationId, { 592 asynchronous: false 593 }); 594 this[("set-" + associationName).camelize()](foundValue); 595 }.bind(this); 596 setAssociation(); 597 this[foreignKey].bind(setAssociation, true); 598 } 599 return proceed.apply(this, $A(arguments).slice(1)); 600 }); 601 }, 602 603 _parametersFromJsonParameters: function(params) { 604 return new rio.Parameters( 605 Object.keys(params).inject({}, function(acc, p) { 606 acc[p.camelize()] = params[p]; 607 return acc; 608 }) 609 ); 610 }, 611 612 _processIncludedCollectionEntities: function(json) { 613 for (var i=json.length; i--;) { 614 var include = json[i]; 615 params = this._parametersFromJsonParameters(include.parameters); 616 var url = rio.models[include.className].url(); 617 if (this._collectionEntities[url + "#" + Object.toJSON(params.ajaxParameters())] == undefined) { 618 rio.models[include.className]._collectionEntityFromJson(include.json, params, url); 619 } 620 } 621 }, 622 623 _filterAndProcessJsonWhileAccumulatingCollectionEntities: function(inJson) { 624 if (inJson._set) { 625 var ceFunction = function() { 626 model._processIncludedCollectionEntities(inJson._set.include); 627 }; 628 return [rio.Model.filterJson(inJson._set.self), ceFunction]; 629 } else { 630 return [rio.Model.filterJson(inJson), Prototype.emptyFunction]; 631 } 632 }, 633 634 _filterAndProcessJson: function(inJson) { 635 var results = model._filterAndProcessJsonWhileAccumulatingCollectionEntities(inJson); 636 results[1](); 637 return results[0]; 638 }, 639 640 _collectionEntityFromJson: function(json, parameters, url) { 641 // In case multiple identical queries were made simultaneously 642 if (this._collectionEntities[url + "#" + Object.toJSON(parameters.ajaxParameters())]) { 643 return this._collectionEntities[url + "#" + Object.toJSON(parameters.ajaxParameters())]; 644 } 645 646 var results = json.map(function(result) { 647 // This may cause bugs. It should map the results of _filterAndProcessJsonWhileAccumulatingCollectionEntities 648 // and execute them after them map to prevent reification collisions 649 var modelJson = model._filterAndProcessJson(result); 650 var fromCache = this.getFromCache(model.id(modelJson.id)); 651 if (fromCache) { 652 return fromCache; 653 } else { 654 var lazyNew = function() { 655 var fromCache = this.getFromCache(model.id(modelJson.id)); 656 if (fromCache) { return fromCache; } 657 return new model(modelJson); 658 }.bind(this); 659 lazyNew.__lazyNew = true; 660 return lazyNew; 661 } 662 }.bind(this)); 663 664 var collectionEntity = rio.CollectionEntity.create({ 665 model: model, 666 values: results, 667 condition: parameters.conditionFunction() 668 }); 669 collectionEntity.prepare = function() { 670 collectionEntity.prepare = Prototype.emptyFunction; 671 // prevent the initialization from double adding the entities 672 var oldAdd = this.add; 673 try { 674 var i; 675 this.add = Prototype.emptyFunction; 676 var timesToLoop = this.length; 677 var toRemove = []; 678 for (i=0; i<timesToLoop; i++) { 679 if (this[i].__lazyNew) { 680 var modelInstance = this[i](); 681 if (!this.include(modelInstance)) { 682 this[i] = modelInstance; 683 } else { 684 toRemove.push(i); 685 } 686 } 687 } 688 for (i=toRemove.length; i--;) { 689 this.splice(toRemove[i], 1); 690 } 691 } finally { 692 this.add = oldAdd; 693 } 694 }; 695 (function() { 696 // Ultimately we shouldn't need to do this 697 // This is required if when parsing eager loaded json collections 698 // contain individual entities that need to be added to other collection entities 699 // and the entity representing the eager collection is never loaded, hence prepared. 700 collectionEntity.prepare(); 701 }.defer()); 702 this.putCollectionEntity(url + "#" + Object.toJSON(parameters.ajaxParameters()), collectionEntity); 703 704 return collectionEntity; 705 }, 706 707 _clientOnlyAttrs: [], 708 709 clientOnlyAttr: function(attrName) { 710 this._clientOnlyAttrs.push(attrName); 711 }, 712 713 _identityCache: {}, 714 715 getFromCache: function(id) { 716 if (id == undefined) { return; } 717 return this._identityCache[id.cacheKey ? id.cacheKey() : id] || this._identityCache[model.id(id) && model.id(id).cacheKey()]; 718 }, 719 720 putInCache: function(id, value) { 721 var cacheKey = id.cacheKey ? id.cacheKey() : id; 722 if (this._identityCache[cacheKey] == value) { 723 return; 724 } 725 this._identityCache[cacheKey] = value; 726 this.addToCollectionEntities(value); 727 }, 728 729 removeFromCache: function(id) { 730 var cacheKey = id.cacheKey ? id.cacheKey() : id; 731 delete this._identityCache[cacheKey]; 732 }, 733 734 _collectionEntities: {}, 735 putCollectionEntity: function(key, value) { 736 this._collectionEntities[key] = value; 737 }, 738 739 addToCollectionEntities: function(value) { 740 var ces = this._collectionEntities; 741 for (var key in ces) { 742 ces[key].add(value); 743 } 744 }, 745 746 updateInCollectionEntites: function(value) { 747 var ces = this._collectionEntities; 748 for (var key in ces) { 749 ces[key].update(value); 750 } 751 }, 752 753 removeFromCollectionEntities: function(value) { 754 Object.values(this._collectionEntities).each(function(collectionEntity) { 755 collectionEntity.remove(value); 756 }); 757 } 758 759 }); 760 }, 761 762 hasChannel: function() { 763 return false; 764 }, 765 766 channel: function() { 767 model.addMethods({ 768 broadcast: function() { 769 var args = $A(arguments); 770 var methodName = args.shift(); 771 var body = { 772 id: this.getId().toString(), 773 method: methodName, 774 args: args 775 }; 776 777 new Ajax.Request("/push/broadcast", { 778 asynchronous: true, 779 method: "get", 780 evalJSON: false, 781 evalJS: false, 782 parameters: { 783 channel: this.channelName(), 784 message: Object.toJSON(body) 785 } 786 }); 787 }, 788 789 channelName: function() { 790 return model + "." + this.getId(); 791 } 792 }); 793 794 Object.extend(model, { 795 hasChannel: function() { return true; }, 796 797 receiveBroadcast: function(options) { 798 var instance = model.getFromCache(model.id(options.id)); 799 if (instance) { 800 instance[options.method].apply(instance, options.args); 801 } 802 } 803 }); 804 } 805 }); 806 807 if (args.length > 0 && args.last() != undefined && !args.last().ATTR) { 808 var initializers = args.last(); 809 if (Object.isString(initializers.resource)) { 810 model.resource(initializers.resource); 811 } 812 if (initializers.channel) { 813 model.channel(); 814 } 815 816 if (initializers.hasMany) { 817 initializers.hasMany.each(function(h) { 818 model.hasMany(h); 819 }); 820 } 821 822 if (initializers.belongsTo) { 823 initializers.belongsTo.each(function(h) { 824 model.belongsTo(h); 825 }); 826 } 827 828 (initializers.clientOnlyAttrs || []).each(function(clientOnlyAttr) { 829 model.clientOnlyAttr(clientOnlyAttr); 830 }); 831 832 if (initializers.undoEnabled) { 833 model.undoEnabled = true; 834 } 835 836 rio.Model.extend(model, initializers.methods || {}); 837 } 838 839 return model; 840 }, 841 842 _findAllRequests: [], 843 afterActiveQueries: function(fcn) { 844 var count = 0; 845 this._findAllRequests.each(function(far) { 846 if (far._complete) { return; } 847 count++; 848 far.options.onComplete = (far.options.onComplete || Prototype.emptyFunction).wrap(function(proceed) { 849 var ret = proceed(arguments); 850 count--; 851 if (count == 0) { fcn(); } 852 return ret; 853 }); 854 }); 855 if (count == 0) { fcn(); } 856 }, 857 858 filterJson: function(json) { 859 if (json.attributes) { return json.attributes; } 860 return (rio.environment.includeRootInJson) ? Object.values(json)[0] : json; 861 }, 862 863 remoteCreate: function(options) { 864 if (options.transactionKey == rio.environment.transactionKey) { return; } 865 try { 866 var results = rio.Model.doRemoteCreate(options); 867 if (results.undoFunction) { 868 rio.Undo.registerUndo(results.undoFunction); 869 } 870 } catch(e) { 871 rio.error(e, "Remote create error!"); 872 } 873 }, 874 875 remoteUpdate: function(options) { 876 if (options.transactionKey == rio.environment.transactionKey) { return; } 877 try { 878 var results = rio.Model.doRemoteUpdate(options); 879 if (results.undoFunction) { 880 rio.Undo.registerUndo(results.undoFunction); 881 } 882 } catch(e) { 883 rio.error(e, "Remote update error!"); 884 } 885 }, 886 887 remoteDestroy: function(options) { 888 if (options.transactionKey == rio.environment.transactionKey) { return; } 889 try { 890 var results = rio.Model.doRemoteDestroy(options); 891 if (results.undoFunction) { 892 rio.Undo.registerUndo(results.undoFunction); 893 } 894 } catch(e) { 895 rio.error(e, "Remote destroy error!"); 896 } 897 }, 898 899 doRemoteCreate: function(options) { 900 var model = rio.models[options.name]; 901 var fromCache = model.getFromCache(model.id(options.id)); 902 if (!fromCache) { 903 fromCache = new model(model._filterAndProcessJson(options.json)); 904 } else { 905 return {}; 906 } 907 908 return { 909 undoFunction: model.undoEnabled ? function() { 910 model.getFromCache(fromCache.getId()).destroy(); 911 } : null 912 }; 913 }, 914 915 doRemoteUpdate: function(options) { 916 var model = rio.models[options.name]; 917 var instance = model.getFromCache(model.id(options.id)); 918 if (!instance) { 919 return {}; 920 } 921 922 var json = model._filterAndProcessJson(options.json); 923 924 var results = {}; 925 926 if (model.undoEnabled) { 927 var oldState = instance.attributeState(); 928 var processUndo = function() { 929 model.getFromCache(instance.getId()).updateAttributes(oldState); 930 }; 931 results.undoFunction = processUndo; 932 } 933 934 var attributes = Object.keys(json).without("id").inject({}, function(acc, k) { 935 acc[k.camelize()] = json[k]; 936 return acc; 937 }); 938 instance.updateAttributes(attributes, { skipSave: true }); 939 rio.models[options.name].updateInCollectionEntites(instance); 940 941 instance._lastSavedState = instance.attributeState(); 942 943 return results; 944 }, 945 946 doRemoteDestroy: function(options) { 947 var model = rio.models[options.name]; 948 var fromCache = model.getFromCache(model.id(options.id)); 949 var results = {}; 950 951 if (fromCache) { 952 if (model.undoEnabled) { 953 var oldState = fromCache.attributeState(); 954 var processUndo = function() { 955 var instance = new model(Object.extend({id: fromCache.getId()}, oldState)); 956 instance._lastSavedState = undefined; 957 instance.save(); 958 }; 959 results.undoFunction = processUndo; 960 } 961 962 fromCache.removeFromCaches(); 963 964 fromCache.fire("destroy"); 965 } 966 967 return results; 968 }, 969 970 remoteTransaction: function(transactionData) { 971 if (transactionData.transactionKey == rio.environment.transactionKey) { return; } 972 try { 973 // TODO: Two potential bugs here 974 // 975 // 1) Need to execute this in a rio.Attr.transaction 976 // 2) Need to get back the collection entity process functions from _filterAndProcessJsonWhileAccumulatingCollectionEntities 977 // and run them at the end. Otherwise we might have reification collisions. 978 var resultSets = transactionData.transaction.map(function(t) { 979 switch(t.action) { 980 case "create": 981 return rio.Model.doRemoteCreate(t); 982 case "update": 983 return rio.Model.doRemoteUpdate(t); 984 case "destroy": 985 return rio.Model.doRemoteDestroy(t); 986 } 987 }); 988 989 var undos = resultSets.map(function(r) { return r.undoFunction; }).compact(); 990 991 if (!undos.empty()) { 992 rio.Undo.registerUndo(function() { 993 undos.each(function(undo) { 994 undo(); 995 }); 996 }); 997 } 998 } catch(e) { 999 rio.error(e, "Remote transaction error!"); 1000 } 1001 }, 1002 1003 extend: function(model, extension) { 1004 1005 extension.__initialize = extension.initialize || Prototype.emptyFunction; 1006 extension.initialize = function(options) { 1007 this.__model = model; 1008 1009 if (options.id == undefined && model.url) { 1010 options.id = model.id(); 1011 this._id = options.id; 1012 } 1013 1014 if (options.id && options.id.constructor != rio.Id && model.id) { 1015 options.id = model.id(options.id, true); 1016 this.setId(options.id); 1017 } 1018 1019 (this.__initialize.bind(this))(options); 1020 1021 // Add the hasManyAssocitation#create methods 1022 Object.keys(model._hasManyAssociations).each(function(hasManyName) { 1023 var options = model._hasManyAssociations[hasManyName]; 1024 this[hasManyName].create = function(createOptions) { 1025 // In the case that the entity is still new, we should preload the association 1026 // to make sure no entries are missed 1027 // TODO: before we can wrap in isNew, we need to make the spec fixture proxy include created entities 1028 // if (this.isNew()) { 1029 this[("get-" + hasManyName).camelize()](); 1030 // } 1031 var parameters = Object.clone(options.parameters); 1032 parameters[options.foreignKey] = this.getId(); 1033 return rio.models[options.className].create(Object.extend(createOptions || {}, parameters)); 1034 }.bind(this); 1035 }.bind(this)); 1036 1037 if (model.hasChannel()) { 1038 if (rio.push) { 1039 var addChannel = function() { 1040 rio.push.addChannel(this.channelName()); 1041 }.bind(this); 1042 if (options.id && options.id.temporary()) { 1043 options.id.doAfterReification(addChannel); 1044 } else { 1045 addChannel(); 1046 } 1047 } else { 1048 rio.warn("Attempted to add a channel without an available push server"); 1049 } 1050 } 1051 1052 options = options || {}; 1053 if (options.id && model.url) { 1054 model.putInCache(options.id, this); 1055 } 1056 if (options.id && options.id.temporary && !options.id.temporary()) { 1057 this._lastSavedState = this.attributeState(); 1058 } 1059 }; 1060 1061 rio.Attr.extend(model, extension); 1062 }, 1063 1064 toString: function() { 1065 return "Model"; 1066 } 1067 };