You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
546 lines
15 KiB
546 lines
15 KiB
define(["../_base/lang", "../Evented", "../_base/declare", "../_base/Deferred", |
|
"../promise/all", "../_base/array", "../_base/connect", "../regexp" |
|
], function(lang, Evented, declare, Deferred, all, array, connect, regexp){ |
|
|
|
// module: |
|
// dojo/data/ObjectStore |
|
|
|
function convertRegex(character){ |
|
return character == '*' ? '.*' : character == '?' ? '.' : character; |
|
} |
|
return declare("dojo.data.ObjectStore", [Evented],{ |
|
// summary: |
|
// A Dojo Data implementation that wraps Dojo object stores for backwards |
|
// compatibility. |
|
|
|
objectStore: null, |
|
constructor: function(options){ |
|
// options: |
|
// The configuration information to pass into the data store. |
|
// |
|
// - options.objectStore: |
|
// |
|
// The object store to use as the source provider for this data store |
|
|
|
this._dirtyObjects = []; |
|
if(options.labelAttribute){ |
|
// accept the old labelAttribute to make it easier to switch from old data stores |
|
options.labelProperty = options.labelAttribute; |
|
} |
|
lang.mixin(this, options); |
|
}, |
|
labelProperty: "label", |
|
|
|
getValue: function(/*Object*/ item, /*String*/property, /*value?*/defaultValue){ |
|
// summary: |
|
// Gets the value of an item's 'property' |
|
// item: |
|
// The item to get the value from |
|
// property: |
|
// property to look up value for |
|
// defaultValue: |
|
// the default value |
|
|
|
return typeof item.get === "function" ? item.get(property) : |
|
property in item ? |
|
item[property] : defaultValue; |
|
}, |
|
getValues: function(item, property){ |
|
// summary: |
|
// Gets the value of an item's 'property' and returns |
|
// it. If this value is an array it is just returned, |
|
// if not, the value is added to an array and that is returned. |
|
// item: Object |
|
// property: String |
|
// property to look up value for |
|
|
|
var val = this.getValue(item,property); |
|
return val instanceof Array ? val : val === undefined ? [] : [val]; |
|
}, |
|
|
|
getAttributes: function(item){ |
|
// summary: |
|
// Gets the available attributes of an item's 'property' and returns |
|
// it as an array. |
|
// item: Object |
|
|
|
var res = []; |
|
for(var i in item){ |
|
if(item.hasOwnProperty(i) && !(i.charAt(0) == '_' && i.charAt(1) == '_')){ |
|
res.push(i); |
|
} |
|
} |
|
return res; |
|
}, |
|
|
|
hasAttribute: function(item,attribute){ |
|
// summary: |
|
// Checks to see if item has attribute |
|
// item: Object |
|
// The item to check |
|
// attribute: String |
|
// The attribute to check |
|
return attribute in item; |
|
}, |
|
|
|
containsValue: function(item, attribute, value){ |
|
// summary: |
|
// Checks to see if 'item' has 'value' at 'attribute' |
|
// item: Object |
|
// The item to check |
|
// attribute: String |
|
// The attribute to check |
|
// value: Anything |
|
// The value to look for |
|
return array.indexOf(this.getValues(item,attribute),value) > -1; |
|
}, |
|
|
|
|
|
isItem: function(item){ |
|
// summary: |
|
// Checks to see if the argument is an item |
|
// item: Object |
|
// The item to check |
|
|
|
// we have no way of determining if it belongs, we just have object returned from |
|
// service queries |
|
return (typeof item == 'object') && item && !(item instanceof Date); |
|
}, |
|
|
|
isItemLoaded: function(item){ |
|
// summary: |
|
// Checks to see if the item is loaded. |
|
// item: Object |
|
// The item to check |
|
|
|
return item && typeof item.load !== "function"; |
|
}, |
|
|
|
loadItem: function(args){ |
|
// summary: |
|
// Loads an item and calls the callback handler. Note, that this will call the callback |
|
// handler even if the item is loaded. Consequently, you can use loadItem to ensure |
|
// that an item is loaded is situations when the item may or may not be loaded yet. |
|
// If you access a value directly through property access, you can use this to load |
|
// a lazy value as well (doesn't need to be an item). |
|
// args: Object |
|
// See dojo/data/api/Read.fetch() |
|
// example: |
|
// | store.loadItem({ |
|
// | item: item, // this item may or may not be loaded |
|
// | onItem: function(item){ |
|
// | // do something with the item |
|
// | } |
|
// | }); |
|
|
|
var item; |
|
if(typeof args.item.load === "function"){ |
|
Deferred.when(args.item.load(), function(result){ |
|
item = result; // in synchronous mode this can allow loadItem to return the value |
|
var func = result instanceof Error ? args.onError : args.onItem; |
|
if(func){ |
|
func.call(args.scope, result); |
|
} |
|
}); |
|
}else if(args.onItem){ |
|
// even if it is already loaded, we will use call the callback, this makes it easier to |
|
// use when it is not known if the item is loaded (you can always safely call loadItem). |
|
args.onItem.call(args.scope, args.item); |
|
} |
|
return item; |
|
}, |
|
close: function(request){ |
|
// summary: |
|
// See dojo/data/api/Read.close() |
|
return request && request.abort && request.abort(); |
|
}, |
|
fetch: function(args){ |
|
// summary: |
|
// See dojo/data/api/Read.fetch() |
|
|
|
args = lang.delegate(args, args && args.queryOptions); |
|
var self = this; |
|
var scope = args.scope || self; |
|
var query = args.query; |
|
if(typeof query == "object"){ // can be null, but that is ignore by for-in |
|
query = lang.delegate(query); // don't modify the original |
|
for(var i in query){ |
|
// find any strings and convert them to regular expressions for wildcard support |
|
var required = query[i]; |
|
if(typeof required == "string"){ |
|
query[i] = RegExp("^" + regexp.escapeString(required, "*?\\").replace(/\\.|\*|\?/g, convertRegex) + "$", args.ignoreCase ? "mi" : "m"); |
|
query[i].toString = (function(original){ |
|
return function(){ |
|
return original; |
|
}; |
|
})(required); |
|
} |
|
} |
|
} |
|
|
|
var results = this.objectStore.query(query, args); |
|
Deferred.when(results.total, function(totalCount){ |
|
Deferred.when(results, function(results){ |
|
if(args.onBegin){ |
|
args.onBegin.call(scope, totalCount || results.length, args); |
|
} |
|
if(args.onItem){ |
|
for(var i=0; i<results.length;i++){ |
|
args.onItem.call(scope, results[i], args); |
|
} |
|
} |
|
if(args.onComplete){ |
|
args.onComplete.call(scope, args.onItem ? null : results, args); |
|
} |
|
return results; |
|
}, errorHandler); |
|
}, errorHandler); |
|
function errorHandler(error){ |
|
if(args.onError){ |
|
args.onError.call(scope, error, args); |
|
} |
|
} |
|
args.abort = function(){ |
|
// abort the request |
|
if(results.cancel){ |
|
results.cancel(); |
|
} |
|
}; |
|
if(results.observe){ |
|
if(this.observing){ |
|
// if we were previously observing, cancel the last time to avoid multiple notifications. Just the best we can do for the impedance mismatch between APIs |
|
this.observing.cancel(); |
|
} |
|
this.observing = results.observe(function(object, removedFrom, insertedInto){ |
|
if(array.indexOf(self._dirtyObjects, object) == -1){ |
|
if(removedFrom == -1){ |
|
self.onNew(object); |
|
} |
|
else if(insertedInto == -1){ |
|
self.onDelete(object); |
|
} |
|
else{ |
|
for(var i in object){ |
|
if(i != self.objectStore.idProperty){ |
|
self.onSet(object, i, null, object[i]); |
|
} |
|
} |
|
} |
|
} |
|
}, true); |
|
} |
|
this.onFetch(results); |
|
args.store = this; |
|
return args; |
|
}, |
|
getFeatures: function(){ |
|
// summary: |
|
// return the store feature set |
|
|
|
return { |
|
"dojo.data.api.Read": !!this.objectStore.get, |
|
"dojo.data.api.Identity": true, |
|
"dojo.data.api.Write": !!this.objectStore.put, |
|
"dojo.data.api.Notification": true |
|
}; |
|
}, |
|
|
|
getLabel: function(/* dojo/data/api/Item */ item){ |
|
// summary: |
|
// See dojo/data/api/Read.getLabel() |
|
if(this.isItem(item)){ |
|
return this.getValue(item,this.labelProperty); //String |
|
} |
|
return undefined; //undefined |
|
}, |
|
|
|
getLabelAttributes: function(/* dojo/data/api/Item */ item){ |
|
// summary: |
|
// See dojo/data/api/Read.getLabelAttributes() |
|
return [this.labelProperty]; //array |
|
}, |
|
|
|
//Identity API Support |
|
|
|
|
|
getIdentity: function(item){ |
|
// summary: |
|
// returns the identity of the given item |
|
// See dojo/data/api/Read.getIdentity() |
|
return this.objectStore.getIdentity ? this.objectStore.getIdentity(item) : item[this.objectStore.idProperty || "id"]; |
|
}, |
|
|
|
getIdentityAttributes: function(item){ |
|
// summary: |
|
// returns the attributes which are used to make up the |
|
// identity of an item. Basically returns this.objectStore.idProperty |
|
// See dojo/data/api/Read.getIdentityAttributes() |
|
|
|
return [this.objectStore.idProperty]; |
|
}, |
|
|
|
fetchItemByIdentity: function(args){ |
|
// summary: |
|
// fetch an item by its identity, by looking in our index of what we have loaded |
|
var item; |
|
Deferred.when(this.objectStore.get(args.identity), |
|
function(result){ |
|
item = result; |
|
args.onItem.call(args.scope, result); |
|
}, |
|
function(error){ |
|
args.onError.call(args.scope, error); |
|
} |
|
); |
|
return item; |
|
}, |
|
|
|
newItem: function(data, parentInfo){ |
|
// summary: |
|
// adds a new item to the store at the specified point. |
|
// Takes two parameters, data, and options. |
|
// data: Object |
|
// The data to be added in as an item. |
|
// data: Object |
|
// See dojo/data/api/Write.newItem() |
|
|
|
if(parentInfo){ |
|
// get the previous value or any empty array |
|
var values = this.getValue(parentInfo.parent,parentInfo.attribute,[]); |
|
// set the new value |
|
values = values.concat([data]); |
|
data.__parent = values; |
|
this.setValue(parentInfo.parent, parentInfo.attribute, values); |
|
} |
|
this._dirtyObjects.push({object:data, save: true}); |
|
this.onNew(data); |
|
return data; |
|
}, |
|
deleteItem: function(item){ |
|
// summary: |
|
// deletes item and any references to that item from the store. |
|
// item: |
|
// item to delete |
|
|
|
// If the desire is to delete only one reference, unsetAttribute or |
|
// setValue is the way to go. |
|
this.changing(item, true); |
|
|
|
this.onDelete(item); |
|
}, |
|
setValue: function(item, attribute, value){ |
|
// summary: |
|
// sets 'attribute' on 'item' to 'value' |
|
// See dojo/data/api/Write.setValue() |
|
|
|
var old = item[attribute]; |
|
this.changing(item); |
|
item[attribute]=value; |
|
this.onSet(item,attribute,old,value); |
|
}, |
|
setValues: function(item, attribute, values){ |
|
// summary: |
|
// sets 'attribute' on 'item' to 'value' value |
|
// must be an array. |
|
// See dojo/data/api/Write.setValues() |
|
|
|
if(!lang.isArray(values)){ |
|
throw new Error("setValues expects to be passed an Array object as its value"); |
|
} |
|
this.setValue(item,attribute,values); |
|
}, |
|
|
|
unsetAttribute: function(item, attribute){ |
|
// summary: |
|
// unsets 'attribute' on 'item' |
|
// See dojo/data/api/Write.unsetAttribute() |
|
|
|
this.changing(item); |
|
var old = item[attribute]; |
|
delete item[attribute]; |
|
this.onSet(item,attribute,old,undefined); |
|
}, |
|
|
|
changing: function(object,_deleting){ |
|
// summary: |
|
// adds an object to the list of dirty objects. This object |
|
// contains a reference to the object itself as well as a |
|
// cloned and trimmed version of old object for use with |
|
// revert. |
|
// object: Object |
|
// Indicates that the given object is changing and should be marked as |
|
// dirty for the next save |
|
// _deleting: [private] Boolean |
|
|
|
object.__isDirty = true; |
|
//if an object is already in the list of dirty objects, don't add it again |
|
//or it will overwrite the premodification data set. |
|
for(var i=0; i<this._dirtyObjects.length; i++){ |
|
var dirty = this._dirtyObjects[i]; |
|
if(object==dirty.object){ |
|
if(_deleting){ |
|
// we are deleting, no object is an indicator of deletiong |
|
dirty.object = false; |
|
if(!this._saveNotNeeded){ |
|
dirty.save = true; |
|
} |
|
} |
|
return; |
|
} |
|
} |
|
var old = object instanceof Array ? [] : {}; |
|
for(i in object){ |
|
if(object.hasOwnProperty(i)){ |
|
old[i] = object[i]; |
|
} |
|
} |
|
this._dirtyObjects.push({object: !_deleting && object, old: old, save: !this._saveNotNeeded}); |
|
}, |
|
|
|
save: function(kwArgs){ |
|
// summary: |
|
// Saves the dirty data using object store provider. See dojo/data/api/Write for API. |
|
// kwArgs: |
|
// - kwArgs.global: |
|
// This will cause the save to commit the dirty data for all |
|
// ObjectStores as a single transaction. |
|
// |
|
// - kwArgs.revertOnError: |
|
// This will cause the changes to be reverted if there is an |
|
// error on the save. By default a revert is executed unless |
|
// a value of false is provide for this parameter. |
|
// |
|
// - kwArgs.onError: |
|
// Called when an error occurs in the commit |
|
// |
|
// - kwArgs.onComplete: |
|
// Called when an the save/commit is completed |
|
|
|
kwArgs = kwArgs || {}; |
|
var result, actions = []; |
|
var savingObjects = []; |
|
var self = this; |
|
var dirtyObjects = this._dirtyObjects; |
|
var left = dirtyObjects.length;// this is how many changes are remaining to be received from the server |
|
try{ |
|
connect.connect(kwArgs,"onError",function(){ |
|
if(kwArgs.revertOnError !== false){ |
|
var postCommitDirtyObjects = dirtyObjects; |
|
dirtyObjects = savingObjects; |
|
self.revert(); // revert if there was an error |
|
self._dirtyObjects = postCommitDirtyObjects; |
|
} |
|
else{ |
|
self._dirtyObjects = dirtyObjects.concat(savingObjects); |
|
} |
|
}); |
|
var transaction; |
|
if(this.objectStore.transaction){ |
|
transaction = this.objectStore.transaction(); |
|
} |
|
for(var i = 0; i < dirtyObjects.length; i++){ |
|
var dirty = dirtyObjects[i]; |
|
var object = dirty.object; |
|
var old = dirty.old; |
|
delete object.__isDirty; |
|
if(object){ |
|
result = this.objectStore.put(object, {overwrite: !!old}); |
|
actions.push(result); |
|
} |
|
else if(typeof old != "undefined"){ |
|
result = this.objectStore.remove(this.getIdentity(old)); |
|
actions.push(result); |
|
} |
|
savingObjects.push(dirty); |
|
dirtyObjects.splice(i--,1); |
|
|
|
} |
|
all(actions).then(function(value){ |
|
if(kwArgs.onComplete){ |
|
kwArgs.onComplete.call(kwArgs.scope, value); |
|
} |
|
}, function(error){ |
|
if(kwArgs.onError) { |
|
kwArgs.onError.call(kwArgs.scope, error); |
|
} |
|
}); |
|
|
|
if(transaction){ |
|
transaction.commit(); |
|
} |
|
|
|
}catch(e){ |
|
kwArgs.onError.call(kwArgs.scope, value); |
|
} |
|
}, |
|
|
|
revert: function(){ |
|
// summary: |
|
// returns any modified data to its original state prior to a save(); |
|
|
|
var dirtyObjects = this._dirtyObjects; |
|
for(var i = dirtyObjects.length; i > 0;){ |
|
i--; |
|
var dirty = dirtyObjects[i]; |
|
var object = dirty.object; |
|
var old = dirty.old; |
|
if(object && old){ |
|
// changed |
|
for(var j in old){ |
|
if(old.hasOwnProperty(j) && object[j] !== old[j]){ |
|
this.onSet(object, j, object[j], old[j]); |
|
object[j] = old[j]; |
|
} |
|
} |
|
for(j in object){ |
|
if(!old.hasOwnProperty(j)){ |
|
this.onSet(object, j, object[j]); |
|
delete object[j]; |
|
} |
|
} |
|
}else if(!old){ |
|
// was an addition, remove it |
|
this.onDelete(object); |
|
}else{ |
|
// was a deletion, we will add it back |
|
this.onNew(old); |
|
} |
|
delete (object || old).__isDirty; |
|
dirtyObjects.splice(i, 1); |
|
} |
|
|
|
}, |
|
isDirty: function(item){ |
|
// summary: |
|
// returns true if the item is marked as dirty or true if there are any dirty items |
|
// item: Object |
|
// The item to check |
|
if(!item){ |
|
return !!this._dirtyObjects.length; |
|
} |
|
return item.__isDirty; |
|
}, |
|
|
|
// Notification Support |
|
|
|
onSet: function(){ |
|
// summary: |
|
// See dojo/data/api/Notification.onSet() |
|
}, |
|
onNew: function(){ |
|
// summary: |
|
// See dojo/data/api/Notification.onNew() |
|
}, |
|
onDelete: function(){ |
|
// summary: |
|
// See dojo/data/api/Notification.onDelete() |
|
}, |
|
// an extra to get result sets |
|
onFetch: function(results){ |
|
// summary: |
|
// Called when a fetch occurs |
|
} |
|
|
|
} |
|
); |
|
});
|
|
|