1 if (!dojo._hasResource["openils.PermaCrud.Store"]) {
2 dojo._hasResource["openils.PermaCrud.Store"] = true;
3 dojo.provide("openils.PermaCrud.Store");
4 dojo.require("openils.PermaCrud");
6 /* an exception class specific to openils.PermaCrud.Store */
7 function PCSError(message) { this.message = message; }
8 PCSError.prototype.toString = function() {
9 return "openils.PermaCrud.Store: " + this.message;
12 /* PCSQueryCache is a here to prevent openils.PermaCrud.Store from asking
13 * openils.PermaCrud redundant questions within short time frames.
15 function PCSQueryCache() {
18 this._init = function(max_age) {
19 if (typeof (this.max_age = max_age) == "undefined")
20 throw new PCSError("PCSQueryCache requires max_age parameter");
21 this._cached_items = {};
24 this._is_left_anchored = function(key) {
25 return key.slice(-1) == "%";
28 /* Find any reasonably close matches for key */
29 this._similar_key = function(key) {
30 var key_is_left_anchored = this._is_left_anchored(key);
32 for (var candidate in this._cached_items) {
33 if (key == candidate) {
35 } else if (!key_is_left_anchored &&
36 this._is_left_anchored(candidate)) {
37 if (candidate.slice(0, -1) == key)
45 this._get_if_fresh = function(key) {
46 /* XXX This is passive cache aging. Make it active w/ setTimeout? */
47 var age = new Date().getTime() - this._cached_items[key].when;
48 if (age > this.max_age) {
49 delete this._cached_items[key];
52 return this._cached_items[key].data;
56 this.put = function(key, data) {
57 this._cached_items[key] = {
58 "when": new Date().getTime(), "data": data
62 this.get = function(key) {
63 if (similar = this._similar_key(key)) { /* assignment */
64 var results = this._get_if_fresh(similar);
66 console.log("cache hit: " + key);
73 this.clear = function(key) {
77 this.add = function(key, datum) {
78 this._cached_items[key].data.push(datum);
79 this._cached_items[key].when = new Date().getTime();
82 this._init.apply(this, arguments);
86 "openils.PermaCrud.Store", null, {
88 // This is a data store implementing the Read and Identity APIs,
89 // making it possible to lazy-load fieldmapper objects via the
92 // Two "levels" of laziness are possible. You get one
93 // level of laziness by default: no retrieve-all queries are
94 // honored, and fetch() only retrieves objects matching
95 // substantive queries. This is great for autocompleting dijits.
96 // The second level of laziness is invoked by using stubby mode.
97 // In stubby mode, fetch() only retrieves IDs and returns place-
98 // holder objects, while getValue() or anything like it will
99 // actually retrieve the full object. This may be more useful for
100 // grids. In any event, huge datasets don't have to be retrieved
101 // just to provide a widget whereby a user can select a single item.
103 // Later it is hoped that we will also implement the Notification
104 // and Write APIs here, which will enable vastly simpler interfaces
105 // to be developed (and existing interfaces to be vastly simplified)
106 // in Evergreen. Think no more keeping track at the interface layer
107 // of dirty objects, nor manually updating one dijit's store when a
108 // value in another changes.
110 // Note that the methods of this class may throw exceptions in cases
111 // where such behavior is prescribed by the dojo data API from
112 // which said methods originate. These might not be documented in
113 // the method summaries below.
115 // The Thought behind all this came from Mike Rylander, who has a
116 // pretty clear vision of what this needs to be and how it needs
117 // to get there. The actual typing, testing, and gradually dawning
118 // understanding is brought to you by Lebbeous Fogle-Weekley.
120 "constructor": function(/* object */ args) {
122 // Insantiates the store.
124 // Requires the object argument *args*.
126 // An object with these properties:
128 // fmclass: (string required),
129 // fetch_limit: (int default 50),
130 // max_query_cache_age:(int default 10000 ms),
131 // stubby: (bool default false),
132 // honor_retrieve_all: (bool default value of *stubby*),
133 // label_attributes: (optional array of attribute names)
134 // label_separator: (string default " ")
135 // base_filter: (optional object pcrud search filter)
136 // pcrud: (optional openils.PermaCrud object),
137 // authtoken: (optional string authtoken)
140 // The *fmclass* parameter.
141 // This is required, and should be a class hint from the IDL.
142 // In this way you specify the class that the store will deal
145 // The *fetch_limit* parameter.
146 // The maximum number of items the store will fetch at a time.
148 // The *max_query_cache_age* parameter.
149 // An internal cache is used to avoid re-issuing the same query
150 // repeatedly to PermaCrud. This is necessary because some
151 // dijits (dijit.form.FilteringSelect, for example) get pretty
152 // talky with the fetch() method. With this parameter you're
153 // specifying the maximum age of entry in this cache. After
154 // this length of time, a fresh call to fetch(), even with
155 // the same query as it was issued in a previous call, will
156 // result in a call to PermaCrud.
158 // The *stubby* parameter.
159 // In stubby mode, fetch() only retrieves IDs and returns
160 // place-holder objects, while getValue or anything like it
161 // will /then/ actually retrieve the full object.
163 // The *honor_retrive_all* parameter.
164 // This is normally set to whatever the value of *stubby* is,
165 // meaning that queries from dijits of the form
166 // {query: {key: ""}} and {query: {key: "*"}} are ignored by
167 // default in non-stubby mode, and translated to pcrud
168 // search filters of {id: {"!=": null}} in stubby mode (where
169 // id is the primary key for the class in question). Set this
170 // boolean parameter to override the default behavior.
172 // The *label_attributes* parameter.
173 // getLabelAttributes() will figure out what to return based
174 // on 1) fields with a selector attribute for our class in the
175 // IDL and, failing that, 2) the Identity field for our class
176 // _unless_ you want to override that by providing an array
177 // (single element is fine) of field names to use as label
180 // The *label_separator* parameter.
181 // In the event of dealing with a class that has more than one
182 // attribute contributing to the label, this string, which
183 // defaults to " " defines the token that is placed between
184 // the value of each field as the label string is built.
186 // The *base_filter* parameter.
187 // This optional object will be mixed in with any search queries
188 // produced for pcrud, giving the user a way to limit the result
189 // set beyond the query that will be issued by the dijit. For
190 // example, you can provide an autocompleting widget against
191 // the acqpl class, set base_filter to
192 // {"owner": openils.User.user.id()}
193 // and have the dijit query against name, so that as you type
194 // the store issues queries like
195 // {"owner": 1, "name": {"ilike": "new boo%"}}
197 // The *pcrud* paramter.
198 // Optionally pass in your own openils.PermaCrud object, if
199 // you already have one.
201 // The *authtoken* parameter.
202 // Optionally pass in your authtoken string. If you're in
203 // certain parts of the Evergreen environment, we may be able
204 // to get this automagically from openils.User anyway, so that's
205 // why this parameter is optional.
206 if (typeof(this.fmclass = args.fmclass) != "string")
207 throw new PCSError("Must have fmclass");
209 this.pkey = fieldmapper.IDL.fmclasses[this.fmclass].pkey;
210 this.fetch_limit = args.fetch_limit || 50;
211 this.max_query_cache_age = args.max_query_cache_age || 10000; /*ms*/
212 this.stubby = args.stubby || false;
214 if (typeof args.honor_retrieve_all != undefined)
215 this.honor_retrieve_all = args.honor_retrieve_all;
217 this.honor_retrieve_all = args.stubby;
219 this.label_attributes = args.label_attributes || null;
220 this.label_separator = args.label_separator || " ";
222 this.base_filter = args.base_filter || {};
223 this.pcrud = args.pcrud || new openils.PermaCrud(
224 args.authtoken ? {"authtoken": args.authtoken} : null
227 this._stored_items = {};
228 this._query_cache = new PCSQueryCache(this.max_query_cache_age);
231 "_dojo_query_to_pcrud": function(/* request-object */ req) {
233 // Internal method to convery queries from dijits into pcrud
234 // search filters. Messy. Called by fetch().
235 var qkeys = openils.Util.objectProperties(req.query);
236 if (qkeys.length < 1)
237 throw new PCSError("Not enough meat on that query");
239 for (var qkey in req.query) {
240 var value = req.query[qkey];
241 var type = typeof value;
245 (type == "object" && dojo.isArray(value))
248 "Can't deal with query key " + qkey + " (" + type + ")"
252 var pcrud_query = {};
255 for (var i = 0; i < qkeys.length; i++) {
257 var term = req.query[key];
259 /* TODO: break this down into smaller separate methods:
265 if (term == "" || term == "*") {
266 if (qkeys.length != 1) {
267 continue; /* query: {name: "bar", id: "*"}
268 makes no sense; we could just leave
270 } else if (!this.honor_retrieve_all) {
271 return req; /* totally bail */
273 key = this.pkey; /*ignore given key: may not be unique*/
274 pcrud_query[key] = {"!=": null};
275 hashparts[i] = key + ":%";
278 term = term.replace("%", "%%");
279 term = term.replace(/\*$/, "%");
281 if (dojo.indexOf(term, "%") != -1) op = "like";
282 if (req.queryOptions && req.queryOptions.ignoreCase)
286 pcrud_query[key] = {};
287 pcrud_query[key][op] = term;
288 hashparts[i] = key + ":" + op + ":" + term;
290 pcrud_query[key] = term;
291 hashparts[i] = key + ":" + term;
296 var hashkey = hashparts.join(":");
299 opts.offset = req.start || 0;
300 hashkey = "offset:" + opts.offset + ":" + hashkey;
302 opts.limit = (req.count && req.count != Infinity) ?
303 req.count : this.fetch_limit;
304 hashkey = "limit:" + opts.limit + ":" + hashkey;
306 if (dojo.isArray(req.sort)) {
308 opts.order_by[this.fmclass] = dojo.map(
309 req.sort, function(key) {
310 return (key.attribute + " ") + (
311 key.descending ? "DESC" : "ASC"
315 /* XXX not sure whether multiple columns will work as such. */
316 hashkey = "order_by:" + opts.order_by[this.fmclass] + ":" +
320 opts.id_list = this.stubby;
322 return [dojo.mixin(this.base_filter, pcrud_query), opts, hashkey];
325 /* *** Begin dojo.data.api.Read methods *** */
327 "getValue": function(
329 /* string */ attribute,
330 /* anything */ defaultValue) {
332 // Given an *item* and the name of an *attribute* on that item,
333 // return that attribute's value. Load the item first if
334 // it's not actually loaded yet (stubby mode).
335 if (!this.isItem(item))
336 throw new PCSError("getValue(): bad item: " + item);
337 else if (typeof attribute != "string")
338 throw new PCSError("getValue(): bad attribute");
342 if (this.isItemLoaded(item)) {
343 value = item[attribute]();
345 value = this.loadItem({"item": item})[attribute]();
352 /* XXX This method by proscription can't return an array, but what
353 * the heck is it supposed to do if the value of the field
354 * indicated IS an array? */
355 return (typeof value == "undefined") ? defaultValue : value;
358 "getValues": function(/* object */ item, /* string */ attribute) {
360 // Same as getValue(), except the result is always an array
361 // and there is no way to specify a default value.
362 if (!this.isItem(item) || typeof attribute != "string")
363 throw new PCSError("bad arguments");
365 var result = this.getValue(item, attribute, []);
366 return dojo.isArray(result) ? result : [result];
369 "getAttributes": function(/* object */ item) {
371 // Return an array of all of the given *item*'s *attribute*s.
372 // This is done by consulting fieldmapper.
373 if (!this.isItem(item) || typeof attribute != "string")
374 throw new PCSError("getAttributes(): bad arguments");
376 return fieldmapper.IDL.fmclasses[item.classname].fields;
379 "hasAttribute": function(/* object */ item, /* string */ attribute) {
381 // Return true or false based on whether *item* has an
382 // attribute by the name specified in *attribute*.
383 if (!this.isItem(item) || typeof attribute != "string") {
384 throw new PCSError("hasAttribute(): bad arguments");
386 /* tested as autovivification-safe */
388 typeof fieldmapper.IDL.fmclasses[item.classname].
389 fields[attribute] != "undefined"
394 "containsValue": function(
396 /* string */ attribute,
397 /* anything */ value) {
399 // Return true or false based on whether *item* has any value
400 // matching *value* for *attribute*.
401 if (!this.isItem(item) || typeof attribute != "string")
402 throw new PCSError("bad data");
405 dojo.indexOf(this.getValues(item, attribute), value) != -1
409 "isItem": function(/* anything */ something) {
411 // Return true if *something* is an item (loaded or not), else
413 /* XXX Shouldn't this really check to see whether the item came from
414 * our store? Checking type (fieldmapper class) may suffice. */
416 typeof something == "object" && something !== null &&
417 something._isfieldmapper && something.classname == this.fmclass
421 "isItemLoaded": function(/* anything */ something) {
423 // Return true if *something* is an item and is loaded.
424 // In stubby mode, something that is an item but isn't yet
425 // loaded is possible.
426 return this.isItem(something) && something._loaded;
429 "close": function(/* object */ request) {
435 "getLabel": function(/* object */ item) {
437 // Return the name of the attribute that should serve as the
438 // label for objects of the same class as *item*. This is
439 // done by consulting fieldmapper and looking for the field
440 // with "selector" set to true.
444 this.getLabelAttributes(),
445 function(o) { self.getValue(item, o); }
446 ).join(this.label_separator);
449 "getLabelAttributes": function(/* object */ item) {
451 // This is simply a deeper method supporting getLabel().
452 if (dojo.isArray(this.label_attributes)) {
453 return this.label_attributes;
456 var fmclass = fieldmapper.IDL.fmclasses[item];
457 var sels = dojo.filter(
459 function(c) { return Boolean(c.selector); }
461 if (sels.length) return sels;
462 else return [fmclass.pkey];
465 "loadItem": function(/* object */ keywordArgs) {
467 // Fully load the item specified in the *item* property of
468 // *keywordArgs* by retrieving it from PermaCrud.
471 // In non-stubby mode (default) this ultimately just returns the
472 // same object it's given. In stubby mode, the object might
473 // not really be fully loaded, so we go to PermaCrud for it.
475 // This method (part of the Read API) is dependent on
476 // fetchItemByIdentity() (part of the Identity API), so don't
477 // split the two up unless you know what you're doing.
478 if (!this.isItem(keywordArgs.item))
479 throw new PCSError("that's not an item; can't load it");
481 keywordArgs.identity = keywordArgs.item[this.pkey]();
482 return this.fetchItemByIdentity(keywordArgs);
485 "fetch": function(/* request-object */ req) {
487 // Basically, fetch objects matching the *query* property of
488 // the *req* parameter.
491 // In non-stubby mode (default) this means translaating the
492 // *query* in to a pcrud search filter and storing all the
493 // objects that result from that search, up to fetch_limit
494 // (a property of the store itself, set via the constructor).
496 // In stubby mode, this means the same as above except that
497 // we only ask pcrud for an ID list, and what we store are
498 // "fake" objects with only the identifier field set.
500 // In both modes, we also respect the following properties
501 // of the *req* object (all optional):
503 // sort an object that gets translated to order_by
504 // count an int that gets translated to limit
505 // start an int that gets translated to offset
506 // onBegin a callback that takes the number of items
507 // that this call to fetch() will return, but
508 // we always give it -1 (i.e. unknown)
509 // onItem a callback that takes each item as we get it
510 // onComplete a callback that takes the list of items
511 // after they're all fetched
513 // The onError callback is ignored. I've never seen PermaCrud
514 // actually execute its own onerror callback, so this remains
515 // to be figured out.
517 // The Read API also charges this method with adding an abort
518 // callback to the *req* object for the caller's use, but
519 // the one we provide does nothing but issue an alert().
520 var parts = this._dojo_query_to_pcrud(req);
521 var filter = parts[0];
523 var hashkey = parts[2];
525 if (!filter) return req; /* nothing to do */
527 /* set up some closures... */
529 var fetch_results = [];
530 var callback_scope = req.scope || dojo.global;
532 var process_fetch = function(r) {
533 if (r = openils.Util.readResponse(r)) {
536 r = new fieldmapper[self.fmclass]();
542 if (typeof req.onItem == "function")
543 req.onItem.call(callback_scope, r, req);
545 self._stored_items[r[self.pkey]()] = r;
546 fetch_results.push(r);
547 self._query_cache.add(hashkey, r);
550 req.abort = function() {
551 alert("The 'abort' operation is not supported");
554 /* ... and proceed. */
556 if (typeof req.onBegin == "function")
557 req.onBegin.call(callback_scope, -1, req);
559 fetch_results = this._query_cache.get(hashkey);
560 if (!fetch_results.length) {
561 this._query_cache.clear(hashkey);
563 this.fmclass, filter, dojo.mixin(opts, {
565 "timeout": 10, /* important: streaming but sync */
566 "onresponse": process_fetch
571 /* XXX at the moment, I don't believe we need either to call
572 * onItem nor to add to our internal "_stored_items" those items
573 * that we just got from cache. */
575 /* as for onError: I don't believe openils.PermaCrud supports any
576 * onerror-like callback in an actually working way at this time */
578 if (typeof req.onComplete == "function")
579 req.onComplete.call(callback_scope, fetch_results, req);
584 /* *** Begin dojo.data.api.Identity methods *** */
586 "getIdentity": function(/* object */ item) {
588 // Given an *item* return its unique identifier (the value
589 // of its primary key).
590 if (!this.isItem(item)) throw new PCSError("not an item");
591 if (this._stored_items[item[this.pkey]()] == item)
592 return item[this.pkey]();
597 "getIdentityAttributes": function(/* object */ item) {
599 // Given an *item* return the list of the name of the fields
600 // that constitute the item's unique identifier. Since we
601 // deal with fieldmapper objects, that's always a list of one.
605 "fetchItemByIdentity": function(/* object */ keywordArgs) {
607 // Given an *identity* property in the *keywordArgs* object,
608 // retrieve an item, unless we already have the fully loaded
609 // item in the store's internal memory.
612 // Once we've have the item we want one way or another, issue
613 // the *onItem* callback from the *keywordArgs* object. If we
614 // tried to retrieve the item with pcrud but didn't get an item
615 // back, issue the *onError* callback.
616 if (keywordArgs.identity == undefined)
617 return null; // Identity API spec unclear whether error callback
618 // would need to be run, so we won't. Matters
619 // because in some cases pcrud times out when attempting
620 // to retrieve by a null PK value
621 var callback_scope = keywordArgs.scope || dojo.global;
622 var test_item = this._stored_items[keywordArgs.identity];
624 if (test_item && this.isItemLoaded(test_item)) {
626 "fetchItemByIdentity(): already have " +
629 if (typeof keywordArgs.onItem == "function")
630 keywordArgs.onItem.call(callback_scope, test_item);
635 "fetchItemByIdentity(): going to pcrud for " +
640 this.pcrud.retrieve(this.fmclass, keywordArgs.identity);
644 "No item of class " + this.fmclass +
645 " with identity " + keywordArgs.identity +
646 " could be retrieved."
650 this._stored_items[item[this.pkey]()] = item;
652 if (typeof keywordArgs.onItem == "function")
653 keywordArgs.onItem.call(callback_scope, item);
657 if (typeof keywordArgs.onError == "function")
658 keywordArgs.onError.call(callback_scope, E);
665 /* *** This last method is for classes implementing any dojo APIs *** */
667 "getFeatures": function() {
669 "dojo.data.api.Read": true,
670 "dojo.data.api.Identity": true