Add etag support.
[xtuple] / enyo-client / database / source / xm / javascript / item_site.sql
1 /* Delete previously misnamed record */
2 delete from xt.js where js_context='xtuple' and js_type = 'item_site';
3
4 select xt.install_js('XM','ItemSite','xtuple', $$
5   /* Copyright (c) 1999-2014 by OpenMFG LLC, d/b/a xTuple.
6      See www.xm.ple.com/CPAL for the full text of the software license. */
7
8 (function () {
9
10   if (!XM.ItemSite) { XM.ItemSite = {}; }
11
12   XM.ItemSite.isDispatchable = true;
13
14   /**
15     Return the current cost for a particular item site.
16   */
17   XM.ItemSite.cost = function (itemsiteId) {
18     if (!XT.Data.checkPrivilege('ViewCosts')) { return null; }
19     return plv8.execute('select itemcost(itemsite_id) as cost from itemsite where obj_uuid = $1;', [itemsiteId])[0].cost;
20   };
21
22   /** @private */
23   var _fetch = function (recordType, backingType, query, backingTypeJoinColumn, idColumn) {
24     query = query || {};
25     backingTypeJoinColumn = backingTypeJoinColumn || 'itemsite_item_id';
26     idColumn = idColumn || 'itemsite_id';
27
28     var data = Object.create(XT.Data),
29       nameSpace = recordType.beforeDot(),
30       type = recordType.afterDot(),
31       tableNamespace = backingType.beforeDot(),
32       table = backingType.afterDot(),
33       orderBy = query.orderBy,
34       orm = data.fetchOrm(nameSpace, type),
35       pkey = XT.Orm.primaryKey(orm),
36       nkey = XT.Orm.naturalKey(orm),
37       keyColumn = XT.Orm.primaryKey(orm, true),
38       customerId = null,
39       accountId = -1,
40       shiptoId,
41       effectiveDate = new Date(),
42       vendorId = null,
43       limit = query.rowLimit ? 'limit ' + Number(query.rowLimit) : '',
44       offset = query.rowOffset ? 'offset ' + Number(query.rowOffset) : '',
45       clause,
46       ret = {
47         nameSpace: nameSpace,
48         type: type
49       },
50       itemJoinMatches,
51       itemJoinTable,
52       keySearch = false,
53       extra = "",
54       qry,
55       counter = 1,
56       ids = [],
57       idParams = [],
58       sqlCount,
59       sql1 = 'select pt1.%3$I as id ' +
60              'from ( ' +
61              'select t1.* as id ' +
62              'from %1$I.%2$I t1 {joins} ' +
63              'where {conditions} {extra}',
64       sql2 = 'select * from %1$I.%2$I where id in ({ids}) {orderBy}';
65
66     /* Handle special parameters */
67     if (query.parameters) {
68       query.parameters = query.parameters.filter(function (param) {
69         var result = false;
70
71         /* Over-ride usual search behavior */
72         if (param.keySearch) {
73           keySearch = param.value;
74           sql1 += ' and t1.%4$I in (select item_id from item where item_number ~^ ${p1} or item_upccode ~^ ${p1}) ' +
75             'union ' +
76             'select t1.* ' +
77             'from %1$I.%2$I t1 {joins} ' +
78             ' join itemalias on t1.%4$I=itemalias_item_id ' +
79             '   and itemalias_crmacct_id is null ' +
80             'where {conditions} {extra} ' +
81             ' and (itemalias_number ~^ ${p1}) ' +
82             'union ' +
83             'select t1.* ' +
84             'from %1$I.%2$I t1 {joins} ' +
85             ' join itemalias on t1.%4$I=itemalias_item_id ' +
86             '   and itemalias_crmacct_id={accountId} ' +
87             'where {conditions} {extra} ' +
88             ' and (itemalias_number ~^ ${p1}) ';
89           return false;
90         }
91
92         switch (param.attribute)
93         {
94         case "customer":
95           customerNumber = param.value;
96           customerId = data.getId(data.fetchOrm('XM', 'CustomerProspectRelation'), param.value);
97           accountId = data.getId(data.fetchOrm('XM', 'AccountRelation'), param.value);
98           break;
99         case "shipto":
100           shiptoId = data.getId(data.fetchOrm('XM', 'CustomerShipto'), param.value);
101           break;
102         case "effectiveDate":
103           effectiveDate = param.value;
104           break;
105         case "vendor":
106           vendorId = data.getId(data.fetchOrm('XM', 'VendorRelation'), param.value);
107           break;
108         default:
109           result = true;
110         }
111         return result;
112       });
113     }
114
115     clause = data.buildClause(nameSpace, type, query.parameters, orderBy);
116
117     /* Check if public.item is already joined through clause.joins. */
118     if (clause.joins && clause.joins.length) {
119       itemJoinMatches = clause.joins.match(/(.item )(jt\d+)/g);
120
121       if (itemJoinMatches && itemJoinMatches.length) {
122         itemJoinTable = itemJoinMatches[0].match(/(jt\d+)/g);
123       }
124     }
125
126     if (!itemJoinTable) {
127       /* public.item is not already joined. Set the default name. */
128       itemJoinTable = 'sidejoin';
129     }
130
131     /* If customer passed, restrict results to item sites allowed to be sold to that customer */
132     if (customerId) {
133       extra += XT.format(' and %1$I.item_id in (' +
134              'select item_id from item where item_sold and not item_exclusive ' +
135              'union ' +
136              'select item_id from xt.custitem where cust_id=${p2} ' +
137              '  and ${p4}::date between effective and (expires - 1) ', [itemJoinTable]);
138
139       if (shiptoId) {
140         extra += 'union ' +
141                'select item_id from xt.shiptoitem where shipto_id=${p3}::integer ' +
142                '  and ${p4}::date between effective and (expires - 1) ';
143       }
144
145       extra += ") ";
146
147       if (!clause.joins) {
148         clause.joins = '';
149       }
150
151       /* public.item is not already joined. Add it here. */
152       if (itemJoinTable === 'sidejoin') {
153         clause.joins = clause.joins + XT.format(' left join item %1$I on t1.%2$I = %1$I.item_id ', [itemJoinTable, backingTypeJoinColumn]);
154       }
155     }
156
157     /* If vendor passed, and vendor can only supply against defined item sources, then restrict results */
158     if (vendorId) {
159       extra +=  XT.format(' and %1$I.item_id in (' +
160               '  select itemsrc_item_id ' +
161               '  from itemsrc ' +
162               '  where itemsrc_active ' +
163               '    and itemsrc_vend_id=%2$I)', [itemJoinTable, vendorId]);
164
165       if (!clause.joins) {
166         clause.joins = '';
167       }
168
169       /* public.item is not already joined. Add it here. */
170       if (itemJoinTable === 'sidejoin') {
171         clause.joins = clause.joins + XT.format(' left join item %1$I on t1.%2$I = %1$I.item_id ', [itemJoinTable, backingTypeJoinColumn]);
172       }
173     }
174
175     if (query.count) {
176       /* Just get the count of rows that match the conditions */
177       sqlCount = 'select count(distinct t1.%3$I) as count from %1$I.%2$I t1 {joins} where {conditions} {extra};';
178       sqlCount = XT.format(sqlCount, [tableNamespace.decamelize(), table.decamelize(), idColumn, backingTypeJoinColumn]);
179       sqlCount = sqlCount.replace(/{conditions}/g, clause.conditions)
180                          .replace(/{extra}/g, extra)
181                          .replace('{joins}', clause.joins)
182                          .replace(/{p2}/g, clause.parameters.length + 1)
183                          .replace(/{p3}/g, clause.parameters.length + 2)
184                          .replace(/{p4}/g, clause.parameters.length + 3);
185
186       if (customerId) {
187         clause.parameters = clause.parameters.concat([customerId, shiptoId, effectiveDate]);
188       }
189
190       if (DEBUG) {
191         XT.debug('ItemSiteListItem sqlCount = ', sqlCount);
192         XT.debug('ItemSiteListItem values = ', clause.parameters);
193       }
194
195       ret.data = plv8.execute(sqlCount, clause.parameters);
196
197       return ret;
198     }
199
200     sql1 = XT.format(
201       sql1 += ') pt1 group by pt1.%3$I{groupBy} {orderBy} %5$s %6$s;',
202       [tableNamespace, table, idColumn, backingTypeJoinColumn, limit, offset]
203     );
204
205     /* Because we query views of views, you can get inconsistent results */
206     /* when doing limit and offest queries without an order by. Add a default. */
207     if (limit && offset && (!orderBy || !orderBy.length) && !clause.orderByColumns) {
208       /* We only want this on sql1, not sql2's clause.orderBy. */
209       clause.orderByColumns = XT.format('order by t1.%1$I', [idColumn]);
210     }
211
212     /* Change table reference in group by and order by to pt1. */
213     if (clause.groupByColumns && clause.groupByColumns.length) {
214       clause.groupByColumns = clause.groupByColumns.replace(/t1./g, 'pt1.');
215     }
216     if (clause.orderByColumns && clause.orderByColumns.length) {
217       clause.orderByColumns = clause.orderByColumns.replace(/t1./g, 'pt1.');
218     }
219
220     /* Query the model */
221     sql1 = sql1.replace(/{conditions}/g, clause.conditions)
222              .replace(/{extra}/g, extra)
223              .replace(/{joins}/g, clause.joins)
224              .replace(/{groupBy}/g, clause.groupByColumns)
225              .replace('{orderBy}', clause.orderByColumns)
226              .replace('{accountId}', accountId)
227              .replace(/{p1}/g, clause.parameters.length + 1)
228              .replace(/{p2}/g, clause.parameters.length + (keySearch ? 2 : 1))
229              .replace(/{p3}/g, clause.parameters.length + (keySearch ? 3 : 2))
230              .replace(/{p4}/g, clause.parameters.length + (keySearch ? 4 : 3));
231
232     if (keySearch) {
233       clause.parameters.push(keySearch);
234     }
235     if (customerId) {
236       clause.parameters = clause.parameters.concat([customerId, shiptoId, effectiveDate]);
237     }
238     if (DEBUG) {
239       XT.debug('ItemSiteListItem sql1 = ', sql1.slice(0,500));
240       XT.debug(sql1.slice(500, 1000));
241       XT.debug(sql1.slice(1000, 1500));
242       XT.debug(sql1.slice(1500, 2000));
243       XT.debug(sql1.slice(2000, 2500));
244       XT.debug('ItemSiteListItem parameters = ', clause.parameters);
245     }
246     qry = plv8.execute(sql1, clause.parameters);
247
248     if (!qry.length) {
249       ret.data = [];
250       return ret;
251     }
252
253     qry.forEach(function (row) {
254       ids.push(row.id);
255       idParams.push("$" + counter);
256       counter++;
257     });
258
259     if (orm.lockable) {
260       sql_etags = "select ver_etag as etag, ver_record_id as id " +
261                   "from xt.ver " +
262                   "where ver_table_oid = ( " +
263                     "select pg_class.oid::integer as oid " +
264                     "from pg_class join pg_namespace on relnamespace = pg_namespace.oid " +
265                     /* Note: using $L for quoted literal e.g. 'contact', not an identifier. */
266                     "where nspname = %1$L and relname = %2$L " +
267                   ") " +
268                   "and ver_record_id in ({ids})";
269       sql_etags = XT.format(sql_etags, [tableNamespace, table]);
270       sql_etags = sql_etags.replace('{ids}', idParams.join());
271
272       if (DEBUG) {
273         XT.debug('fetch sql_etags = ', sql_etags);
274         XT.debug('fetch etags_values = ', JSON.stringify(ids));
275       }
276       etags = plv8.execute(sql_etags, ids) || {};
277       ret.etags = {};
278     }
279
280     sql2 = XT.format(sql2, [nameSpace.decamelize(), type.decamelize()]);
281     sql2 = sql2.replace(/{orderBy}/g, clause.orderBy)
282                .replace('{ids}', idParams.join());
283
284     if (DEBUG) {
285       XT.debug('fetch sql2 = ', sql2);
286       XT.debug('fetch values = ', JSON.stringify(ids));
287     }
288
289     ret.data = plv8.execute(sql2, ids) || [];
290
291     for (var i = 0; i < ret.data.length; i++) {
292       if (etags) {
293         /* Add etags to result in pkey->etag format. */
294         for (var j = 0; j < etags.length; j++) {
295           if (etags[j].id === ret.data[i][pkey]) {
296             ret.etags[ret.data[i][nkey]] = etags[j].etag;
297           }
298         }
299       }
300     }
301
302     data.sanitize(nameSpace, type, ret.data);
303
304     return ret;
305   };
306
307   if (!XM.ItemSiteListItem) { XM.ItemSiteListItem = {}; }
308
309   XM.ItemSiteListItem.isDispatchable = true;
310
311   /**
312     Returns item site list items using usual query means with additional special support for:
313       * Attributes `customer`,`shipto`, and `effectiveDate` for exclusive item rules.
314       * Attribute `vendor` to filter on only items with associated item sources.
315       * Cross check on `alias` and `barcode` attributes for item numbers.
316
317     @param {String} Record type. Must have `itemsite` or related view as its orm source table.
318     @param {Object} Additional query filter (Optional)
319     @returns {Array}
320   */
321   XM.ItemSiteListItem.fetch = function (query) {
322     var result = _fetch("XM.ItemSiteListItem", "public.itemsite", query);
323     return result.data;
324   };
325
326   /**
327    Wrapper for XM.ItemSiteListItem.fetch with support for REST query formatting.
328    Sample usage:
329     select xt.post('{
330       "nameSpace":"XM",
331       "type":"ItemSiteListItem",
332       "dispatch":{
333         "functionName":"restFetch",
334         "parameters":[
335           {
336             "query":[
337               {"customer":{"EQUALS":"TTOYS"}},
338               {"shipto":{"EQUALS":"1d103cb0-dac6-11e3-9c1a-0800200c9a66"}},
339               {"effectiveDate":{"EQUALS":"2014-05-01"}}
340             ]
341           }
342         ]
343       },
344       "username":"admin",
345       "encryptionKey":"hm6gnf3xsov9rudi"
346     }');
347
348    @param {Object} options: query
349    @returns Object
350   */
351   XM.ItemSiteListItem.restFetch = function (options) {
352     options = options || {};
353
354     var items = {},
355       query = {},
356       result = {};
357
358     if (options) {
359       /* Convert from rest_query to XM.Model.query structure. */
360       query = XM.Model.restQueryFormat(options);
361
362       /* Perform the query. */
363       // TODO - move this to xdruple extension.
364       //return _fetch("XM.ItemSiteListItem", "public.itemsite", query);
365       return _fetch("XM.XdrupleCommerceProduct", "xdruple.xd_commerce_product", query, 'product_id', 'id');
366     } else {
367       throw new handleError("Bad Request", 400);
368     }
369   };
370   XM.ItemSiteListItem.restFetch.description = "Returns ItemSiteListItems with additional special support for exclusive item rules, to filter on only items with associated item sources and Cross check on `alias` and `barcode` attributes for item numbers.";
371   XM.ItemSiteListItem.restFetch.request = {
372     "$ref": "ItemSiteListItemQuery"
373   };
374   XM.ItemSiteListItem.restFetch.parameterOrder = ["options"];
375   // For JSON-Schema deff, see:
376   // https://github.com/fge/json-schema-validator/issues/46#issuecomment-14681103
377   XM.ItemSiteListItem.restFetch.schema = {
378     ItemSiteListItemQuery: {
379       properties: {
380         attributes: {
381           title: "ItemSiteListItem Service request attributes",
382           description: "An array of attributes needed to perform a ItemSiteListItem query.",
383           type: "array",
384           items: [
385             {
386               title: "Options",
387               type: "object",
388               "$ref": "ItemSiteListItemOptions"
389             }
390           ],
391           "minItems": 1,
392           "maxItems": 1,
393           required: true
394         }
395       }
396     },
397     ItemSiteListItemOptions: {
398       properties: {
399         query: {
400           title: "query",
401           description: "The query to perform.",
402           type: "array",
403           items: [
404             {
405               title: "column",
406               type: "object"
407             }
408           ],
409           "minItems": 1
410         },
411         orderby: {
412           title: "Order By",
413           description: "The query order by.",
414           type: "array",
415           items: [
416             {
417               title: "column",
418               type: "object"
419             }
420           ]
421         },
422         rowlimit: {
423           title: "Row Limit",
424           description: "The query for paged results.",
425           type: "integer"
426         },
427         maxresults: {
428           title: "Max Results",
429           description: "The query limit for total results.",
430           type: "integer"
431         },
432         pagetoken: {
433           title: "Page Token",
434           description: "The query offset page token.",
435           type: "integer"
436         },
437         count: {
438           title: "Count",
439           description: "Set to true to return only the count of results for this query.",
440           type: "boolean"
441         }
442       }
443     }
444   };
445
446   if (!XM.ItemSiteRelation) { XM.ItemSiteRelation = {}; }
447
448   XM.ItemSiteRelation.isDispatchable = true;
449
450   /**
451     Returns item site relatinos using usual query means with additional special support for:
452       * Attributes `customer`,`shipto`, and `effectiveDate` for exclusive item rules.
453       * Attribute `vendor` to filter on only items with associated item sources.
454       * Cross check on `alias` and `barcode` attributes for item numbers.
455
456     @param {String} Record type. Must have `itemsite` or related view as its orm source table.
457     @param {Object} Additional query filter (Optional)
458     @returns {Array}
459   */
460   XM.ItemSiteRelation.fetch = function (query) {
461     var result = _fetch("XM.ItemSiteRelation", "xt.itemsiteinfo", query);
462     return result.data;
463   };
464
465 }());
466
467 $$ );