UNPKG

ares-ide

Version:

A browser-based code editor and UI designer for Enyo 2 projects

548 lines (537 loc) 22.2 kB
(function (enyo) { //*@public /** _enyo.Store_ is a runtime database of data records. The store indexes records internally for faster lookup. While an application may have multiple stores, there is always a top-level store at _enyo.store_ that is used by [enyo.Model](#enyo.Model) and [enyo.Collection](#enyo.Collection). The store serves as a liason between records and the [enyo.Source](#enyo.Source) that dictates how they are retrieved and/or persisted. Every record and every collection has a reference to a store. If none is explicitly provided, it will resolve to _enyo.store_. */ enyo.kind({ name: "enyo.Store", kind: enyo.Object, noDefer: true, /** The hash of named sources that are available for use on this store. The default source is _ajax_, but others may be added by providing _enyo.defaultStoreProperties_ with a _sources_ hash of sources to add. */ sources: {ajax: "enyo.AjaxSource", jsonp: "enyo.JsonpSource"}, /** By default, the store indexes records in several ways, one of which is by the _primaryKey_ value (if it exists). This value is intended to be unique; the store will complain if it finds multiple instances of the same record. If you set this flag to true, the store will no longer complain; however, if a store has duplicate entries with the same _primaryKey_, you will not be able to search for records by _primaryKey_ in _find()_ or _findLocal()_, but will need to search by _euid_ instead. */ ignoreDuplicates: false, //*@protected records: null, collections: null, //*@public /** Creates a new record of a given _kind_ (string or constructor) and returns the newly created instance (with its _store_ property set to this store). Accepts optional attributes (_attrs_) and options (_opts_) that will be passed to the constructor of the [enyo.Model](#enyo.Model) (see [enyo.Model.constructor](#enyo.Model::constructor)). If no _kind_ is specified, _enyo.Model_ will be used by default. */ createRecord: function (kind, attrs, opts) { if (arguments.length < 3) { if (enyo.isObject(kind)) { opts = attrs; attrs = kind; kind = enyo.Model; } } var Kind = (enyo.isString(kind) && enyo.getPath(kind)) || (enyo.isFunction(kind) && kind); // test to see if opts are opts or attrs are opts opts = opts || {}; enyo.mixin(opts, {store: this}); // if we didn't find the constructor we just use default if (!Kind) { Kind = enyo.Model; } return new Kind(attrs, opts); }, /** Retrieves a record (if it exists) by its _euid_. */ getRecord: function (euid) { return this.records.euid[euid]; }, /** Retrieves a collection (if it exists) by its _euid_. */ getCollection: function (euid) { return this.collections[euid]; }, /** Creates a collection of a given _kind_ (string or constructor) and returns the newly created instance (with its _store_ property set to this store). Accepts optional _records_ array and options (_opts_) to be passed to the collection's constructor. If no _kind_ is specified, [enyo.Collection](#enyo.Collection) will be used by default. */ createCollection: function (kind, records, opts) { if (arguments.length < 3) { if (enyo.isArray(kind)) { opts = records; records = kind; kind = enyo.Collection; } } var Kind = (enyo.isString(kind) && enyo.getPath(kind)) || (enyo.isFunction(kind) && kind); // test to see if opts are opts or records are opts opts = opts || {}; enyo.mixin(opts, {store: this}); // if we didn't find the constructor we just use default if (!Kind) { Kind = enyo.Collection; } return new Kind(records, opts); }, /** Adds a record by its _euid_ and, if it has a known value for its _primaryKey_, indexes the record by that value as well for quicker reference later. This method is mostly used internally, as it is called automatically by models as they are created. A record can only exist to one _enyo.Store_ at a time thus this method will first remove it from an existing store for the record if it isn't this store. */ addRecord: function (rec) { var records = this.records, // the named primary key (string) of the kind pkey = rec.primaryKey, // the entries per-instance-primary-key for this kind kinds = records.pk[rec.kindName] || (records.pk[rec.kindName] = {}), // the value (if any) for the primary key of the record id = rec.get(pkey), // the universally unique identifier for this record euid = rec.euid; // a record can only belong to one store at a time so if it already has a store and it // isn't this store we need to remove it from there first if (rec.store && rec.store !== this) { rec.store.removeRecord(rec); } // for sanity and absolute certainty we check to make sure there is no // existing entry for this record by its unique id if (records.euid[euid] && records.euid[euid] !== rec) { // this scenario should never, ever, happen...ever...for 'reakin real throw "enyo.Store.addRecord: duplicate and unmatching euid entries - parallel euid's " + "should not exist"; } else { records.euid[euid] = rec; } // if a primaryKey was resolved to an actual value we add that now too but the same // is true for unique primaryKey values as is euid just it only matters from within the scope // of the kind if (id !== undefined && id !== null) { // here's the sanity check if (kinds[id] && kinds[id] !== rec) { // uh oh we've got a duplicate primaryKey for the record but it is possible that the // primaryKey is...somehow not a unique or useful property and the only unique property // is euid so this flag could be set if (!this.ignoreDuplicates) { throw "enyo.Store.addRecord: duplicate record added to store for kind `" + rec.kindName + "` " + "with primaryKey set to `" + pkey + "` and the same value of `" + id + "` which cannot coexist " + "for the kind without the `ignoreDuplicates` flag of the store set to `true`"; } } else { kinds[id] = rec; } } // now to index the record by its kind name records.kn[rec.kindName] = records.kn[rec.kindName] || (records.kn[rec.kindName] = {}); records.kn[rec.kindName][euid] = rec; if (!rec.store) { rec.store = this; } }, /** Adds a collection to the store. This is typically executed automatically and does not need to be called in application code. Accepts a reference to the collection to be added, and sets the collection's _store_ property to this store. A collection can only exist in a single _enyo.Store_ at a time thus this method will remove it from any existing store prior to adding it. */ addCollection: function (c) { var collections = this.collections, euid = c.euid; if (c.store && c.store !== this) { c.store.removeCollection(c); } c.addListener("destroy", this._collectionDestroyed); collections[euid] = c; if (!c.store) { c.store = this; } }, /** Removes the reference for the given collection if it is found in the store. This is called automatically when a collection is destroyed. */ removeCollection: function (c) { var collections = this.collections, euid = c.euid; delete collections[euid]; c.removeListener("destroy", this._collectionDestroyed); }, /** Removes the reference for the given record if it is found in the store. This is called automatically when a record is destroyed. */ removeRecord: function (rec) { var records = this.records, pkey = rec.primaryKey, euid = rec.euid, id = rec.get(pkey); delete records.euid[euid]; delete records.kn[rec.kindName][euid]; delete records.pk[rec.kindName][id]; }, /** Adds sources to this store. Requires a hash with _key/value_ pairs, in which a key is a source's name and a value is that source's constructor, an instance, or the path to either expressed as a string. */ addSources: function (props) { var dd = this.sources; for (var k in props) { dd[k] = props[k]; } this._initSources(); }, /** Removes the source with the passed-in name from this store. */ removeSource: function (name) { delete this.sources[name]; }, /** Accepts an array of source names for removal from the store. */ removeSources: function (sources) { var dd = this.sources; for (var i=0, k; (k=sources[i]); ++i) { delete dd[k]; } }, /** Queries a source for a record (or records), with the ability to use various strategies to compile the results in the current store. This is an asynchronous method that requires two parameters--the kind of record to use (in the form of a constructor or string), and an options hash that includes a _success_ method, an optional _fail_ method, a _source_ designating the source to use (or else the record kind's default will be used), a _strategy_ (explained below), and a hash of _attributes_ to use in the query. How these attributes are used in the query depends on the source being used. The _success_ method expects to receive the original options hash passed into _find()_, followed by the result set (returned by the _strategy_, as explained below). There is a special use for this method if an _euid_ or _primaryKey_ value is provided (the _euid_ directly on the options hash; the _primaryKey_ in the _attributes_ hash of the options). In this case, the method will attempt to find the record locally first; if it is found, the _success_ method will be called and the source will not be queried. If the record cannot be found locally, the method will proceed normally. Regardless of whether the source is used, whenever the _euid_ or _primaryKey_ value is provided, the result will be either a single record or _undefined_; it will not be an array. For queries against runtime records only (i.e., records in the store), see _findLocal()_. When results are retrieved from the requested source, they will be handled according to the requested _strategy_ (the default is _merge_). Strategies may easily be extended by creating a method on the store of the form _&lt;name&gt;Strategy_, and then setting the _name_ as the _strategy_ option passed to this method. The strategy resolvers receive two parameters--the current array of records for the kind in the original request, and the incoming results from the source query. These methods are executed under the context of the store. There are two available strategies: _replace_ and _merge_. * When the _replace_ strategy is used, all known records are thrown away (though not destroyed) and replaced by the new results. * When using the _merge_ strategy (the default), any incoming records with the same _primaryKey_ as records already in the store are updated with the values retrieved, and new records are simply added to the store. */ find: function (kind, opts) { // in cases where no kind is provided we assume enyo.Model which is consistent with // other similar api's if (arguments.length == 1) { opts = kind; kind = enyo.Model; } // first find the kind, get access to its prototype and the // options so we can bind our own success and fail methods var c = enyo.isString(kind)? enyo.constructorForKind(kind): kind, p = c.prototype, o = opts, a = o.attributes; // now we need to figure out what strategy, source, pk... var dd = this.sources, rr = this.records, pk = p.primaryKey, d = (o.source && ((enyo.isString(o.source) && dd[o.source]) || o.source)) || dd[p.defaultSource], r = (o.euid && rr.euid[o.euid]) || (a && a[pk] && rr.pk[p.kindName][a[pk]]); // if we already found the record then we know that it was one of the cases where searching // locally first worked so we return this value directly to the original success method if (r) { return o.success(o, r); } // now we have to ensure we have a valid source if (!d) { return this.warn("could not find source `" + (o.source || p.defaultSource) + "`"); } // otherwise we need to clone the options so we can now add our own success methods o = enyo.clone(o); // bind our methods o.success = this.bindSafely("didFind", opts); o.fail = this.bindSafely("didFail", "find", opts); // set the strategy, note that we're setting this on the options being passed to the // success method not our clone opts.strategy = opts.strategy || "merge"; // and fire off the request assuming the source will be able to handle the request d.find(c, o); }, /** Queries the runtime database (in the store); will not query a source even if one is provided. This is a synchronous method and will return an array of records, or an empty array if no matching records are found. As with _find()_, this method behaves somewhat differently when an _euid_ or _primaryKey_ value is provided (the _euid_ directly on the options hash; the _primaryKey_ in the _attributes_ hash of the options). In this case, a specific record is sought, with the return value being not an array, but rather a single record or _undefined_. This method accepts three parameters, the kind as a constructor or string, the options to match against, and an optional _filter_ method. Unlike _find()_, there are no _success_ or _fail_ methods. Also, note that all keys specified in the options will be used as criteria to match against. If you provide an _euid_ key, a key matching the _primaryKey_ of the model kind, or a _kindName_ property, the method will not query for any other values or scan the entire dataset. Using the _kindName_ property will return all records registered in this store for that _kindName_. The filtering process is handled by the store's _filter_ method. Overload this method or provide an optional third parameter that may be a function or string name of a method on the store. This filter method will receive the options to match against and the record as the second parameter, and should return either _true_ or _false_ to indicate whether it should be included in the result set. */ findLocal: function (kind, opts, filter) { if (arguments.length < 3 && enyo.isObject(kind)) { if (enyo.isFunction (opts)) { filter = opts; } opts = kind; kind = enyo.Model; } // we need to find the constructor (for the prototype) of the requested // record type so we know what kind of primaryKey we might be looking for var proto = (typeof kind == "string"? enyo.constructorForKind(kind): kind).prototype, records = this.records, pkey = proto? proto.primaryKey: "", id = opts[pkey], base; // fast path search for single entry by euid, quickest way to find a record if (opts.euid) { return records.euid[opts.euid]; } // if there is a provided primary key value we can use that too if (id !== undefined && id !== null) { base = records.pk[proto.kindName]; // we ensure an explicit undefined not a 'false' value return in cases // where it could not be determined return (base && base[id]) || undefined; } // if a kindName property exists on opts we return an array of all the records // for that kind if (opts.kindName) { base = records.kn[opts.kindName]; return (base && enyo.values(base)) || []; } // if we've gotten here lets check and see if we have a filter we need to apply // to find results filter = (filter && ((typeof filter == "string" && this[filter]) || filter)) || this.filter; filter = this.bindSafely(filter, opts); return enyo.filter((enyo.values(records.kn[proto.kindName]) || []), filter, this); }, /** Overload this method to handle special cases. The default filtering behavior simply matches a record according to the options provided as attributes. This method is used internally by other methods of the store. */ filter: function (opts, rec) { for (var k in opts) { if (rec.get(k) !== opts[k]) { return false; } } return true; }, /** Responds to _find_ requests asynchronously and executes the correct strategy for the results before responding to user callbacks. */ didFind: function () { // TODO: this.log(arguments); }, //*@public /** When the _fetch()_ method is executed on a record and is successful, this method will be called before any _success_ method supplied as an option to the record itself. Overload this method to handle other scenarios. */ didFetch: function (rec, opts, res) { if (opts) { if (opts.success) { opts.success(res); } } }, /** When the _commit()_ method is executed on a record and is successful, this method will be called before any _success_ method supplied as an option to the record itself. Overload this method to handle other scenarios. */ didCommit: function (rec, opts, res) { if (opts) { if (opts.success) { opts.success(res); } } }, /** When the _destroy()_ method is executed on a record and is successful, this method will be called before any _success_ method supplied as an option to the record itself. Overload this method to handle other scenarios. */ didDestroy: function (rec, opts, res) { if (opts) { if (opts.success) { opts.success(res); } } }, /** This method is executed when one of the primary actions has failed. It has the name of the action (one of _"fetch"_, _"commit"_, or _"destroy"_), a reference to the record the action failed on, and the options originally passed to the store for the action. Overload this method to handle other possible failure cases gracefully. By default, it will look for a _fail_ method in the options and (if one is found) execute it. */ didFail: function (action, rec, opts, res) { if (opts) { if (opts.fail) { return opts.fail(res); } } }, //*@protected /** Internal method called to find the requested source and execute the correct method. It also hooks the store's own response mechanisms via the options hash. */ fetchRecord: function (rec, opts) { var ss = this.sources, o = opts? enyo.clone(opts): {}, s = ss[o.source || rec.defaultSource]; if (!s) { throw "enyo.Store: Could not find source '" + (o.source || rec.defaultSource) + "'"; } o.success = this.bindSafely("didFetch", rec, opts); o.fail = this.bindSafely("didFail", "fetch", rec, opts); s.fetch(rec, o); }, /** Internal method called to find the requested source and execute the correct method. It also hooks the store's own response mechanisms via the options hash. */ commitRecord: function (rec, opts) { var ss = this.sources, o = opts? enyo.clone(opts): {}, s = ss[o.source || rec.defaultSource]; if (!s) { throw "enyo.Store: Could not find source '" + (o.source || rec.defaultSource) + "'"; } o.success = this.bindSafely("didCommit", rec, opts); o.fail = this.bindSafely("didFail", "commit", rec, opts); s.commit(rec, o); }, /** Internal method called to find the requested source and execute the correct method. It also hooks the store's own response mechanisms via the options hash. */ destroyRecord: function (rec, opts) { var ss = this.sources, o = opts? enyo.clone(opts): {}, s = ss[o.source || rec.defaultSource]; if (!s) { throw "enyo.Store: Could not find source '" + (o.source || rec.defaultSource) + "'"; } o.success = this.bindSafely("didDestroy", rec, opts); o.fail = this.bindSafely("didFail", "destroy", rec, opts); s.destroy(rec, o); }, destroyRecordLocal: function (rec, opts) { this.didDestroy(rec, opts); }, _initRecords: function () { var r = this.records, pp = ["euid", "pk", "kn"]; for (var i=0, k; (k=pp[i]); ++i) { r[k] = r[k] || {}; } }, _initSources: function () { var dd = this.sources, Kind; for (var k in dd) { if ((Kind = dd[k]) && enyo.isString(Kind)) { Kind = enyo.getPath(Kind); } if (Kind) { if ("function" == typeof Kind && Kind.prototype) { dd[k] = new Kind({store: this}); } else { dd[k] = Kind; Kind.store = this; } } else if (!Kind && enyo.isString(dd[k])) { this.warn("could not find source -> `" + dd[k] + "`"); } } }, //* this cannot be handled in the didDestroy method because that happens before //* the record has a chance to do its own notifications, this must happen last _recordDestroyed: function (rec) { this.removeRecord(rec); }, _collectionDestroyed: function (col) { this.removeCollection(col); }, _recordKeyChanged: function (rec, prev) { // ultimately we need to remove only the entry for the record by its kindName's // unique primaryKey value if (prev) { delete this.records.pk[rec.kindName][prev]; } this.addRecord(rec); }, constructor: enyo.inherit(function (sup) { return function (props) { var r = sup.apply(this, arguments); this.sources = this.sources || {}; this.records = this.records || {}; this.collections = this.collections || {}; this._initRecords(); this._initSources(); this._recordDestroyed = this.bindSafely("_recordDestroyed"); this._collectionDestroyed = this.bindSafely("_collectionDestroyed"); return r; }; }) }); //*@protected enyo.Store.concat = function (ctor, props) { if (props.sources) { var p = ctor.prototype || ctor; p.sources = (p.sources? enyo.mixin(enyo.clone(p.sources), props.sources): props.sources); delete props.sources; } }; //*@public /** There must always be an _enyo.store_ instance. If the default is not what you need, simply create a new instance and assign it to this variable, or use it to create your collections and models and they will not use this instance. */ enyo.store = new enyo.Store(); })(enyo);