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