Merge remote-tracking branch 'XTUPLE/4_5_x' into 4_5_x
[xtuple] / lib / orm / source / xt / javascript / data.sql
1 select xt.install_js('XT','Data','xtuple', $$
2
3 (function () {
4
5   /**
6    * @class
7    *
8    * The XT.Data class includes all functions necessary to process data source requests against the database.
9    * It should be instantiated as an object against which its funtion calls are made. This class enforces privilege
10    * control and as such is not and should not be dispatchable.
11    */
12
13   XT.Data = {
14
15     ARRAY_TYPE: 'A',
16     COMPOSITE_TYPE: 'C',
17     DATE_TYPE: 'D',
18     STRING_TYPE: 'S',
19
20     CREATED_STATE: 'create',
21     READ_STATE: 'read',
22     UPDATED_STATE: 'update',
23     DELETED_STATE: 'delete',
24
25     /**
26      * Build a SQL `where` clause based on privileges for name space and type,
27      * and conditions and parameters passed.
28      *
29      * @seealso fetch
30      *
31      * @param {String} Name space
32      * @param {String} Type
33      * @param {Array} Parameters - optional
34      * @returns {Object}
35      */
36     buildClause: function (nameSpace, type, parameters, orderBy) {
37       parameters = parameters || [];
38
39       var that = this,
40         arrayIdentifiers = [],
41         arrayParams,
42         charSql,
43         childOrm,
44         clauses = [],
45         count = 1,
46         fromKeyProp,
47         groupByColumnParams = [],
48         identifiers = [],
49         joinIdentifiers = [],
50         orderByList = [],
51         orderByColumnList = [],
52         isArray = false,
53         op,
54         orClause,
55         orderByIdentifiers = [],
56         orderByColumnIdentifiers = [],
57         orderByParams = [],
58         orderByColumnParams = [],
59         joins = [],
60         orm = this.fetchOrm(nameSpace, type),
61         param,
62         params = [],
63         parts,
64         pcount,
65         pertinentExtension,
66         pgType,
67         prevOrm,
68         privileges = orm.privileges,
69         prop,
70         sourceTableAlias,
71         ret = {};
72
73       ret.conditions = "";
74       ret.parameters = [];
75
76       /* Handle privileges. */
77       if (orm.isNestedOnly) { plv8.elog(ERROR, 'Access Denied'); }
78       if (privileges &&
79           (!privileges.all ||
80             (privileges.all &&
81               (!this.checkPrivilege(privileges.all.read) &&
82               !this.checkPrivilege(privileges.all.update)))
83           ) &&
84           privileges.personal &&
85           (this.checkPrivilege(privileges.personal.read) ||
86             this.checkPrivilege(privileges.personal.update))
87         ) {
88
89         parameters.push({
90           attribute: privileges.personal.properties,
91           isLower: true,
92           isUsernamePrivFilter: true,
93           value: XT.username
94         });
95       }
96
97       /* Support the short cut wherein the client asks for a filter on a toOne with a
98         string. Technically they should use "theAttr.theAttrNaturalKey", but if they
99         don't, massage the inputs as if they did */
100       parameters.map(function (parameter) {
101         var attributeIsString = typeof parameter.attribute === 'string';
102           attributes = attributeIsString ? [parameter.attribute] : parameter.attribute;
103
104         attributes.map(function (attribute) {
105           var prop = XT.Orm.getProperty(orm, attribute),
106             propName = prop.name,
107             childOrm,
108             naturalKey,
109             index;
110
111           if ((prop.toOne || prop.toMany) && attribute.indexOf('.') < 0) {
112             /* Someone is querying on a toOne without using a path */
113             /* TODO: even if there's a path x.y, it's possible that it's still not
114               correct because the correct path maybe is x.y.naturalKeyOfY */
115             if (prop.toOne && prop.toOne.type) {
116               childOrm = that.fetchOrm(nameSpace, prop.toOne.type);
117             } else if (prop.toMany && prop.toMany.type) {
118               childOrm = that.fetchOrm(nameSpace, prop.toMany.type);
119             } else {
120               plv8.elog(ERROR, "toOne or toMany property is missing it's 'type': " + prop.name);
121             }
122             naturalKey = XT.Orm.naturalKey(childOrm);
123             if (attributeIsString) {
124               /* add the natural key to the end of the requested attribute */
125               parameter.attribute = attribute + "." + naturalKey;
126             } else {
127               /* swap out the attribute in the array for the one with the prepended natural key */
128               index = parameter.attribute.indexOf(attribute);
129               parameter.attribute.splice(index, 1);
130               parameter.attribute.splice(index, 0, attribute + "."  + naturalKey);
131             }
132           }
133         });
134       });
135
136       /* Handle parameters. */
137       if (parameters.length) {
138         for (var i = 0; i < parameters.length; i++) {
139           orClause = [];
140           param = parameters[i];
141           op = param.operator || '=';
142           switch (op) {
143           case '=':
144           case '>':
145           case '<':
146           case '>=':
147           case '<=':
148           case '!=':
149             break;
150           case 'BEGINS_WITH':
151             op = '~^';
152             break;
153           case 'ENDS_WITH':
154             op = '~?';
155             break;
156           case 'MATCHES':
157             op = '~*';
158             break;
159           case 'ANY':
160             op = '<@';
161             for (var c = 0; c < param.value.length; c++) {
162               ret.parameters.push(param.value[c]);
163               param.value[c] = '$' + count;
164               count++;
165             }
166             break;
167           case 'NOT ANY':
168             op = '!<@';
169             for (var c = 0; c < param.value.length; c++) {
170               ret.parameters.push(param.value[c]);
171               param.value[c] = '$' + count;
172               count++;
173             }
174             break;
175           default:
176             plv8.elog(ERROR, 'Invalid operator: ' + op);
177           }
178
179           /* Handle characteristics. This is very specific to xTuple,
180              and highly dependant on certain table structures and naming conventions,
181              but otherwise way too much work to refactor in an abstract manner right now. */
182           if (param.isCharacteristic) {
183             /* Handle array. */
184             if (op === '<@') {
185               param.value = ' ARRAY[' + param.value.join(',') + ']';
186             }
187
188             /* Booleans are stored as strings. */
189             if (param.value === true) {
190               param.value = 't';
191             } else if (param.value === false) {
192               param.value = 'f';
193             }
194
195             /* Yeah, it depends on a property called 'characteristics'... */
196             prop = XT.Orm.getProperty(orm, 'characteristics');
197
198             /* Build the characteristics query clause. */
199             identifiers.push(XT.Orm.primaryKey(orm, true));
200             identifiers.push(prop.toMany.inverse);
201             identifiers.push(orm.nameSpace.toLowerCase());
202             identifiers.push(prop.toMany.type.decamelize());
203             identifiers.push(param.attribute);
204             identifiers.push(param.value);
205
206             charSql = '%' + (identifiers.length - 5) + '$I in (' +
207                       '  select %' + (identifiers.length - 4) + '$I '+
208                       '  from %' + (identifiers.length - 3) + '$I.%' + (identifiers.length - 2) + '$I ' +
209                       '    join char on (char_name = characteristic)' +
210                       '  where 1=1 ' +
211                       /* Note: Not using $i for these. L = literal here. These is not identifiers. */
212                       '    and char_name = %' + (identifiers.length - 1) + '$L ' +
213                       '    and value ' + op + ' %' + (identifiers.length) + '$L ' +
214                       ')';
215
216             clauses.push(charSql);
217
218           /* Array comparisons handle another way. e.g. %1$I !<@ ARRAY[$1,$2] */
219           } else if (op === '<@' || op === '!<@') {
220             /* Handle paths if applicable. */
221             if (param.attribute.indexOf('.') > -1) {
222               parts = param.attribute.split('.');
223               childOrm = this.fetchOrm(nameSpace, type);
224               params.push("");
225               pcount = params.length - 1;
226
227               for (var n = 0; n < parts.length; n++) {
228                 /* Validate attribute. */
229                 prop = XT.Orm.getProperty(childOrm, parts[n]);
230                 if (!prop) {
231                   plv8.elog(ERROR, 'Attribute not found in object map: ' + parts[n]);
232                 }
233
234                 /* Build path. */
235                 if (n === parts.length - 1) {
236                   identifiers.push("jt" + (joins.length - 1));
237                   identifiers.push(prop.attr.column);
238                   pgType = this.getPgTypeFromOrmType(
239                     this.getNamespaceFromNamespacedTable(childOrm.table),
240                     this.getTableFromNamespacedTable(childOrm.table),
241                     prop.attr.column
242                   );
243                   pgType = pgType ? "::" + pgType + "[]" : '';
244                   params[pcount] += "%" + (identifiers.length - 1) + "$I.%" + identifiers.length + "$I";
245                   params[pcount] += ' ' + op + ' ARRAY[' + param.value.join(',') + ']' + pgType;
246                 } else {
247                   childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
248                   sourceTableAlias = n === 0 ? "t1" : "jt" + (joins.length - 1);
249                   joinIdentifiers.push(
250                     this.getNamespaceFromNamespacedTable(childOrm.table),
251                     this.getTableFromNamespacedTable(childOrm.table),
252                     sourceTableAlias, prop.toOne.column,
253                     XT.Orm.primaryKey(childOrm, true));
254                   joins.push("left join %" + (joinIdentifiers.length - 4) + "$I.%" + (joinIdentifiers.length - 3)
255                     + "$I jt" + joins.length + " on %"
256                     + (joinIdentifiers.length - 2) + "$I.%"
257                     + (joinIdentifiers.length - 1) + "$I = jt" + joins.length + ".%" + joinIdentifiers.length + "$I");
258                 }
259               }
260             } else {
261               prop = XT.Orm.getProperty(orm, param.attribute);
262               pertinentExtension = XT.Orm.getProperty(orm, param.attribute, true);
263               if(pertinentExtension.isChild || pertinentExtension.isExtension) {
264                 /* We'll need to join this orm extension */
265                 fromKeyProp = XT.Orm.getProperty(orm, pertinentExtension.relations[0].inverse);
266                 joinIdentifiers.push(
267                   this.getNamespaceFromNamespacedTable(pertinentExtension.table),
268                   this.getTableFromNamespacedTable(pertinentExtension.table),
269                   fromKeyProp.attr.column,
270                   pertinentExtension.relations[0].column);
271                 joins.push("left join %" + (joinIdentifiers.length - 3) + "$I.%" + (joinIdentifiers.length - 2)
272                   + "$I jt" + joins.length + " on t1.%"
273                   + (joinIdentifiers.length - 1) + "$I = jt" + joins.length + ".%" + joinIdentifiers.length + "$I");
274               }
275               if (!prop) {
276                 plv8.elog(ERROR, 'Attribute not found in object map: ' + param.attribute);
277               }
278
279               identifiers.push(pertinentExtension.isChild || pertinentExtension.isExtension ?
280                 "jt" + (joins.length - 1) :
281                 "t1");
282               identifiers.push(prop.attr.column);
283               pgType = this.getPgTypeFromOrmType(
284                 this.getNamespaceFromNamespacedTable(orm.table),
285                 this.getTableFromNamespacedTable(orm.table),
286                 prop.attr.column
287               );
288               pgType = pgType ? "::" + pgType + "[]" : '';
289               params.push("%" + (identifiers.length - 1) + "$I.%" + identifiers.length + "$I " + op + ' ARRAY[' + param.value.join(',') + ']' + pgType);
290               pcount = params.length - 1;
291             }
292             clauses.push(params[pcount]);
293
294           /* Everything else handle another. */
295           } else {
296             if (XT.typeOf(param.attribute) !== 'array') {
297               param.attribute = [param.attribute];
298             }
299
300             for (var c = 0; c < param.attribute.length; c++) {
301               /* Handle paths if applicable. */
302               if (param.attribute[c].indexOf('.') > -1) {
303                 parts = param.attribute[c].split('.');
304                 childOrm = this.fetchOrm(nameSpace, type);
305                 params.push("");
306                 pcount = params.length - 1;
307                 isArray = false;
308
309                 /* Check if last part is an Array. */
310                 for (var m = 0; m < parts.length; m++) {
311                   /* Validate attribute. */
312                   prop = XT.Orm.getProperty(childOrm, parts[m]);
313                   if (!prop) {
314                     plv8.elog(ERROR, 'Attribute not found in object map: ' + parts[m]);
315                   }
316
317                   if (m < parts.length - 1) {
318                     if (prop.toOne && prop.toOne.type) {
319                       childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
320                     } else if (prop.toMany && prop.toMany.type) {
321                       childOrm = this.fetchOrm(nameSpace, prop.toMany.type);
322                     } else {
323                       plv8.elog(ERROR, "toOne or toMany property is missing it's 'type': " + prop.name);
324                     }
325                   } else if (prop.attr && prop.attr.type === 'Array') {
326                     /* The last property in the path is an array. */
327                     isArray = true;
328                     params[pcount] = '$' + count;
329                   }
330                 }
331
332                 /* Reset the childOrm to parent. */
333                 childOrm = this.fetchOrm(nameSpace, type);
334
335                 for (var n = 0; n < parts.length; n++) {
336                   /* Validate attribute. */
337                   prop = XT.Orm.getProperty(childOrm, parts[n]);
338                   if (!prop) {
339                     plv8.elog(ERROR, 'Attribute not found in object map: ' + parts[n]);
340                   }
341
342                   /* Do a persional privs array search e.g. 'admin' = ANY (usernames_array). */
343                   if (param.isUsernamePrivFilter && isArray) {
344                     identifiers.push(prop.attr.column);
345                     arrayIdentifiers.push(identifiers.length);
346
347                     if (n < parts.length - 1) {
348                       childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
349                     }
350                   } else {
351                     pertinentExtension = XT.Orm.getProperty(childOrm, parts[n], true);
352                     var isExtension = pertinentExtension.isChild || pertinentExtension.isExtension;
353                     if(isExtension) {
354                       /* We'll need to join this orm extension */
355                       fromKeyProp = XT.Orm.getProperty(orm, pertinentExtension.relations[0].inverse);
356                       joinIdentifiers.push(
357                         this.getNamespaceFromNamespacedTable(pertinentExtension.table),
358                         this.getTableFromNamespacedTable(pertinentExtension.table),
359                         fromKeyProp.attr.column,
360                         pertinentExtension.relations[0].column);
361                       joins.push("left join %" + (joinIdentifiers.length - 3) + "$I.%" + (joinIdentifiers.length - 2)
362                         + "$I jt" + joins.length + " on t1.%"
363                         + (joinIdentifiers.length - 1) + "$I = jt" + joins.length + ".%" + joinIdentifiers.length + "$I");
364                     }
365                     /* Build path, e.g. table_name.column_name */
366                     if (n === parts.length - 1) {
367                       identifiers.push("jt" + (joins.length - 1));
368                       identifiers.push(prop.attr.column);
369                       params[pcount] += "%" + (identifiers.length - 1) + "$I.%" + identifiers.length + "$I";
370                       if (param.isLower) {
371                         params[pcount] = "lower(" + params[pcount] + ")";
372                       }
373                     } else {
374                       sourceTableAlias = n === 0 && !isExtension ? "t1" : "jt" + (joins.length - 1);
375                       if (prop.toOne && prop.toOne.type) {
376                         childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
377                         joinIdentifiers.push(
378                           this.getNamespaceFromNamespacedTable(childOrm.table),
379                           this.getTableFromNamespacedTable(childOrm.table),
380                           sourceTableAlias, prop.toOne.column,
381                           XT.Orm.primaryKey(childOrm, true)
382                         );
383                       } else if (prop.toMany && prop.toMany.type) {
384                         childOrm = this.fetchOrm(nameSpace, prop.toMany.type);
385                         joinIdentifiers.push(
386                           this.getNamespaceFromNamespacedTable(childOrm.table),
387                           this.getTableFromNamespacedTable(childOrm.table),
388                           sourceTableAlias, prop.toMany.column,
389                           XT.Orm.primaryKey(childOrm, true)
390                         );
391                       }
392                       joins.push("left join %" + (joinIdentifiers.length - 4) + "$I.%" + (joinIdentifiers.length - 3)
393                         + "$I jt" + joins.length + " on %"
394                         + (joinIdentifiers.length - 2) + "$I.%"
395                         + (joinIdentifiers.length - 1) + "$I = jt" + joins.length + ".%" + joinIdentifiers.length + "$I");
396                     }
397                   }
398                 }
399               } else {
400                 /* Validate attribute. */
401                 prop = XT.Orm.getProperty(orm, param.attribute[c]);
402                 pertinentExtension = XT.Orm.getProperty(orm, param.attribute[c], true);
403                 if(pertinentExtension.isChild || pertinentExtension.isExtension) {
404                   /* We'll need to join this orm extension */
405                   fromKeyProp = XT.Orm.getProperty(orm, pertinentExtension.relations[0].inverse);
406                   joinIdentifiers.push(
407                     this.getNamespaceFromNamespacedTable(pertinentExtension.table),
408                     this.getTableFromNamespacedTable(pertinentExtension.table),
409                     fromKeyProp.attr.column,
410                     pertinentExtension.relations[0].column);
411                   joins.push("left join %" + (joinIdentifiers.length - 3) + "$I.%" + (joinIdentifiers.length - 2)
412                     + "$I jt" + joins.length + " on t1.%"
413                     + (joinIdentifiers.length - 1) + "$I = jt" + joins.length + ".%" + joinIdentifiers.length + "$I");
414                 }
415                 if (!prop) {
416                   plv8.elog(ERROR, 'Attribute not found in object map: ' + param.attribute[c]);
417                 }
418
419                 identifiers.push(pertinentExtension.isChild || pertinentExtension.isExtension ?
420                   "jt" + (joins.length - 1) :
421                   "t1");
422                 identifiers.push(prop.attr.column);
423
424                 /* Do a persional privs array search e.g. 'admin' = ANY (usernames_array). */
425                 if (param.isUsernamePrivFilter && ((prop.toMany && !prop.isNested) ||
426                   (prop.attr && prop.attr.type === 'Array'))) {
427
428                   params.push('$' + count);
429                   pcount = params.length - 1;
430                   arrayIdentifiers.push(identifiers.length);
431                 } else {
432                   params.push("%" + (identifiers.length - 1) + "$I.%" + identifiers.length + "$I");
433                   pcount = params.length - 1;
434                 }
435               }
436
437               /* Add persional privs array search. */
438               if (param.isUsernamePrivFilter && ((prop.toMany && !prop.isNested)
439                 || (prop.attr && prop.attr.type === 'Array') || isArray)) {
440
441                 /* XXX: this bit of code has not been touched by the optimization refactor */
442                 /* e.g. 'admin' = ANY (usernames_array) */
443                 arrayParams = "";
444                 params[pcount] += ' ' + op + ' ANY (';
445
446                 /* Build path. e.g. ((%1$I).%2$I).%3$I */
447                 for (var f =0; f < arrayIdentifiers.length; f++) {
448                   arrayParams += '%' + arrayIdentifiers[f] + '$I';
449                   if (f < arrayIdentifiers.length - 1) {
450                     arrayParams = "(" + arrayParams + ").";
451                   }
452                 }
453                 params[pcount] += arrayParams + ')';
454
455               /* Add optional is null clause. */
456               } else if (parameters[i].includeNull) {
457                 /* e.g. %1$I = $1 or %1$I is null */
458                 params[pcount] = params[pcount] + " " + op + ' $' + count + ' or ' + params[pcount] + ' is null';
459               } else {
460                 /* e.g. %1$I = $1 */
461                 params[pcount] += " " + op + ' $' + count;
462               }
463
464               orClause.push(params[pcount]);
465             }
466
467             /* If more than one clause we'll get: (%1$I = $1 or %1$I = $2 or %1$I = $3) */
468             clauses.push('(' + orClause.join(' or ') + ')');
469             count++;
470             ret.parameters.push(param.value);
471           }
472         }
473       }
474
475       ret.conditions = (clauses.length ? '(' + XT.format(clauses.join(' and '), identifiers) + ')' : ret.conditions) || true;
476
477       /* Massage orderBy with quoted identifiers. */
478       /* We need to support the xm case for sql2 and the xt/public (column) optimized case for sql1 */
479       /* In practice we build the two lists independently of one another */
480       if (orderBy) {
481         for (var i = 0; i < orderBy.length; i++) {
482           /* Handle path case. */
483           if (orderBy[i].attribute.indexOf('.') > -1) {
484             parts = orderBy[i].attribute.split('.');
485             prevOrm = orm;
486             orderByParams.push("");
487             orderByColumnParams.push("");
488             groupByColumnParams.push("");
489             pcount = orderByParams.length - 1;
490
491             for (var n = 0; n < parts.length; n++) {
492               prop = XT.Orm.getProperty(orm, parts[n]);
493               if (!prop) {
494                 plv8.elog(ERROR, 'Attribute not found in map: ' + parts[n]);
495               }
496               orderByIdentifiers.push(parts[n]);
497               orderByParams[pcount] += "%" + orderByIdentifiers.length + "$I";
498
499               if (n === parts.length - 1) {
500                 orderByColumnIdentifiers.push("jt" + (joins.length - 1));
501                 orderByColumnIdentifiers.push(prop.attr.column);
502                 orderByColumnParams[pcount] += "%" + (orderByColumnIdentifiers.length - 1) + "$I.%" + orderByColumnIdentifiers.length + "$I"
503                 groupByColumnParams[pcount] += "%" + (orderByColumnIdentifiers.length - 1) + "$I.%" + orderByColumnIdentifiers.length + "$I"
504               } else {
505                 orderByParams[pcount] = "(" + orderByParams[pcount] + ").";
506                 orm = this.fetchOrm(nameSpace, prop.toOne.type);
507                 sourceTableAlias = n === 0 ? "t1" : "jt" + (joins.length - 1);
508                 joinIdentifiers.push(
509                   this.getNamespaceFromNamespacedTable(orm.table),
510                   this.getTableFromNamespacedTable(orm.table),
511                   sourceTableAlias, prop.toOne.column,
512                   XT.Orm.primaryKey(orm, true));
513                 joins.push("left join %" + (joinIdentifiers.length - 4) + "$I.%" + (joinIdentifiers.length - 3)
514                   + "$I jt" + joins.length + " on %"
515                   + (joinIdentifiers.length - 2) + "$I.%"
516                   + (joinIdentifiers.length - 1) + "$I = jt" + joins.length + ".%" + joinIdentifiers.length + "$I");
517               }
518             }
519             orm = prevOrm;
520           /* Normal case. */
521           } else {
522             prop = XT.Orm.getProperty(orm, orderBy[i].attribute);
523             if (!prop) {
524               plv8.elog(ERROR, 'Attribute not found in map: ' + orderBy[i].attribute);
525             }
526             orderByIdentifiers.push(orderBy[i].attribute);
527             orderByColumnIdentifiers.push("t1");
528             /*
529               We might need to look at toOne if the client is asking for a toOne without specifying
530               the path. Unfortunately, if they do specify the path, then sql2 will fail. So this does
531               work, although we're really sorting by the primary key of the toOne, whereas the
532               user probably wants us to sort by the natural key TODO
533             */
534             orderByColumnIdentifiers.push(prop.attr ? prop.attr.column : prop.toOne.column);
535             orderByParams.push("%" + orderByIdentifiers.length + "$I");
536             orderByColumnParams.push("%" + (orderByColumnIdentifiers.length - 1) + "$I.%" + orderByColumnIdentifiers.length + "$I");
537             groupByColumnParams.push("%" + (orderByColumnIdentifiers.length - 1) + "$I.%" + orderByColumnIdentifiers.length + "$I");
538             pcount = orderByParams.length - 1;
539           }
540
541           if (orderBy[i].isEmpty) {
542             orderByParams[pcount] = "length(" + orderByParams[pcount] + ")=0";
543             orderByColumnParams[pcount] = "length(" + orderByColumnParams[pcount] + ")=0";
544           }
545           if (orderBy[i].descending) {
546             orderByParams[pcount] += " desc";
547             orderByColumnParams[pcount] += " desc";
548           }
549
550           orderByList.push(orderByParams[pcount])
551           orderByColumnList.push(orderByColumnParams[pcount])
552         }
553       }
554
555       ret.orderBy = orderByList.length ? XT.format('order by ' + orderByList.join(','), orderByIdentifiers) : '';
556       ret.orderByColumns = orderByColumnList.length ? XT.format('order by ' + orderByColumnList.join(','), orderByColumnIdentifiers) : '';
557       ret.groupByColumns = groupByColumnParams.length ? XT.format(', ' + groupByColumnParams.join(','), orderByColumnIdentifiers) : '';
558       ret.joins = joins.length ? XT.format(joins.join(' '), joinIdentifiers) : '';
559
560       return ret;
561     },
562
563     /**
564      * Queries whether the current user has been granted the privilege passed.
565      *
566      * @param {String} privilege
567      * @returns {Boolean}
568      */
569     checkPrivilege: function (privilege) {
570       var i,
571         privArray,
572         res,
573         ret = privilege,
574         sql;
575
576       if (typeof privilege === 'string') {
577         if (!this._granted) { this._granted = {}; }
578         if (!this._granted[XT.username]) { this._granted[XT.username] = {}; }
579         if (this._granted[XT.username][privilege] !== undefined) { return this._granted[XT.username][privilege]; }
580
581         /* The privilege name is allowed to be a set of space-delimited privileges */
582         /* If a user has any of the applicable privileges then they get access */
583         privArray = privilege.split(" ");
584         sql = 'select coalesce(usrpriv_priv_id, grppriv_priv_id, -1) > 0 as granted ' +
585                'from priv ' +
586                'left join usrpriv on (priv_id=usrpriv_priv_id) and (usrpriv_username=$1) ' +
587                'left join ( ' +
588                '  select distinct grppriv_priv_id ' +
589                '  from grppriv ' +
590                '    join usrgrp on (grppriv_grp_id=usrgrp_grp_id) and (usrgrp_username=$1) ' +
591                '  ) grppriv on (grppriv_priv_id=priv_id) ' +
592                'where priv_name = $2';
593
594         for (var i = 1; i < privArray.length; i++) {
595           sql = sql + ' or priv_name = $' + (i + 2);
596         }
597         sql = sql + "order by granted desc limit 1;";
598
599         /* Cleverness: the query parameters are just the priv array with the username tacked on front. */
600         privArray.unshift(XT.username);
601
602         if (DEBUG) {
603           XT.debug('checkPrivilege sql =', sql);
604           XT.debug('checkPrivilege values =', privArray);
605         }
606         res = plv8.execute(sql, privArray);
607         ret = res.length ? res[0].granted : false;
608
609         /* Memoize. */
610         this._granted[XT.username][privilege] = ret;
611       }
612
613       if (DEBUG) {
614         XT.debug('Privilege check for "' + XT.username + '" on "' + privilege + '" returns ' + ret);
615       }
616
617       return ret;
618     },
619
620     /**
621      * Validate whether user has read access to data. If a record is passed, check personal privileges of
622      * that record.
623      *
624      * @param {String} name space
625      * @param {String} type name
626      * @param {Object} record - optional
627      * @param {Boolean} is top level, default is true
628      * @returns {Boolean}
629      */
630     checkPrivileges: function (nameSpace, type, record, isTopLevel) {
631       isTopLevel = isTopLevel !== false ? true : false;
632       var action =  record && record.dataState === this.CREATED_STATE ? 'create' :
633                   record && record.dataState === this.DELETED_STATE ? 'delete' :
634                   record && record.dataState === this.UPDATED_STATE ? 'update' : 'read',
635         committing = record ? record.dataState !== this.READ_STATE : false,
636         isGrantedAll = true,
637         isGrantedPersonal = false,
638         map = this.fetchOrm(nameSpace, type),
639         privileges = map.privileges,
640         pkey,
641         old;
642
643       /* If there is no ORM, this isn't a table data type so no check required. */
644       /*
645       if (DEBUG) {
646         XT.debug('orm type is ->', map.type);
647         XT.debug('orm is ->', map);
648       }
649       */
650       if (!map) { return true; }
651
652       /* Can not access 'nested only' records directly. */
653       if (DEBUG) {
654         XT.debug('is top level ->', isTopLevel);
655         XT.debug('is nested ->', map.isNestedOnly);
656       }
657       if (isTopLevel && map.isNestedOnly) { return false; }
658
659       /* Check privileges - first do we have access to anything? */
660       if (privileges) {
661         if (DEBUG) { XT.debug('privileges found', privileges); }
662         if (committing) {
663           if (DEBUG) { XT.debug('is committing'); }
664
665           /* Check if user has 'all' read privileges. */
666           isGrantedAll = privileges.all ? this.checkPrivilege(privileges.all[action]) : false;
667
668           /* Otherwise check for 'personal' read privileges. */
669           if (!isGrantedAll) {
670             isGrantedPersonal =  privileges.personal ?
671               this.checkPrivilege(privileges.personal[action]) : false;
672           }
673         } else {
674           if (DEBUG) { XT.debug('is NOT committing'); }
675
676           /* Check if user has 'all' read privileges. */
677           isGrantedAll = privileges.all ?
678                          this.checkPrivilege(privileges.all.read) ||
679                          this.checkPrivilege(privileges.all.update) : false;
680
681           /* Otherwise check for 'personal' read privileges. */
682           if (!isGrantedAll) {
683             isGrantedPersonal =  privileges.personal ?
684               this.checkPrivilege(privileges.personal.read) ||
685               this.checkPrivilege(privileges.personal.update) : false;
686           }
687         }
688       }
689
690       /* If we're checknig an actual record and only have personal privileges, */
691       /* see if the record allows access. */
692       if (record && !isGrantedAll && isGrantedPersonal && action !== "create") {
693         if (DEBUG) { XT.debug('checking record level personal privileges'); }
694         var that = this,
695
696         /* Shared checker function that checks 'personal' properties for access rights. */
697         checkPersonal = function (record) {
698           var i = 0,
699             isGranted = false,
700             props = privileges.personal.properties,
701             get = function (obj, target) {
702               var idx,
703                 part,
704                 parts = target.split("."),
705                 ret;
706
707               for (var idx = 0; idx < parts.length; idx++) {
708                 part = parts[idx];
709                 ret = ret ? ret[part] : obj[part];
710                 if (ret === null || ret === undefined) {
711                   return null;
712                 }
713               }
714
715               return ret;
716             };
717
718           while (!isGranted && i < props.length) {
719             var prop = props[i],
720                 personalUser = get(record, prop);
721
722             if (personalUser instanceof Array) {
723               for (var userIdx = 0; userIdx < personalUser.length; userIdx++) {
724                 if (personalUser[userIdx].toLowerCase() === XT.username) {
725                   isGranted = true;
726                 }
727               }
728             } else if (personalUser) {
729               isGranted = personalUser.toLowerCase() === XT.username;
730             }
731
732             i++;
733           }
734
735           return isGranted;
736         };
737
738         /* If committing we need to ensure the record in its previous state is editable by this user. */
739         if (committing && (action === 'update' || action === 'delete')) {
740           pkey = XT.Orm.naturalKey(map) || XT.Orm.primaryKey(map);
741           old = this.retrieveRecord({
742             nameSpace: nameSpace,
743             type: type,
744             id: record[pkey],
745             superUser: true,
746             includeKeys: true
747           });
748           isGrantedPersonal = checkPersonal(old.data);
749
750         /* Otherwise check personal privileges on the record passed. */
751         } else if (action === 'read') {
752           isGrantedPersonal = checkPersonal(record);
753         }
754       }
755
756       if (DEBUG) {
757         XT.debug('is granted all ->', isGrantedAll);
758         XT.debug('is granted personal ->', isGrantedPersonal);
759       }
760
761       return isGrantedAll || isGrantedPersonal;
762     },
763
764     /**
765      * Commit array columns with their own statements
766      *
767      * @param {Object} Orm
768      * @param {Object} Record
769      */
770     commitArrays: function (orm, record, encryptionKey) {
771       var pkey = XT.Orm.primaryKey(orm),
772         fkey,
773         ormp,
774         prop,
775         val,
776         values,
777         columnToKey,
778         propToKey,
779
780         resolveKey = function (col) {
781           var attr;
782
783           /* First search properties */
784           var ary = orm.properties.filter(function (prop) {
785             return prop.attr && prop.attr.column === col;
786           });
787
788           if (ary.length) {
789             attr =  ary[0].name;
790
791           } else {
792             /* If not found must be extension, search relations */
793             if (orm.extensions.length) {
794               orm.extensions.forEach(function (ext) {
795                 if (!attr) {
796                   ary = ext.relations.filter(function (prop) {
797                     return prop.column === col;
798                   });
799
800                   if (ary.length) {
801                     attr = ary[0].inverse;
802                   }
803                 }
804               })
805             };
806           }
807           if (attr) { return attr };
808
809           /* If still not found, we have a structural problem */
810           throw new Error("Can not resolve primary id on toMany relation");
811         };
812
813       for (prop in record) {
814         ormp = XT.Orm.getProperty(orm, prop);
815
816         /* If the property is an array of objects they must be records so commit them. */
817         if (ormp.toMany && ormp.toMany.isNested) {
818           fkey = ormp.toMany.inverse;
819           values = record[prop];
820
821           for (var i = 0; i < values.length; i++) {
822             val = values[i];
823
824             /* Populate the parent key into the foreign key field if it's absent. */
825             if (!val[fkey]) {
826               columnToKey = ormp.toMany.column;
827               propToKey = columnToKey ? resolveKey(columnToKey) : pkey;
828               if (!record[propToKey]) {
829                 /* If there's no data, we have a structural problem */
830                 throw new Error("Can not resolve foreign key on toMany relation " + ormp.name);
831               }
832               val[fkey] = record[propToKey];
833             }
834
835             this.commitRecord({
836               nameSpace: orm.nameSpace,
837               type: ormp.toMany.type,
838               data: val,
839               encryptionKey: encryptionKey
840             });
841           }
842         }
843       }
844     },
845
846     /**
847      * Commit metrics that have changed to the database.
848      *
849      * @param {Object} metrics
850      * @returns Boolean
851      */
852     commitMetrics: function (metrics) {
853       var key,
854         sql = 'select setMetric($1,$2)',
855         value;
856
857       for (key in metrics) {
858         value = metrics[key];
859         if (typeof value === 'boolean') {
860           value = value ? 't' : 'f';
861         } else if (typeof value === 'number') {
862           value = value.toString();
863         }
864
865         if (DEBUG) {
866           XT.debug('commitMetrics sql =', sql);
867           XT.debug('commitMetrics values =', [key, value]);
868         }
869         plv8.execute(sql, [key, value]);
870       }
871
872       return true;
873     },
874
875     /**
876      * Commit a record to the database. The record must conform to the object hiearchy as defined by the
877      * record's `ORM` definition. Each object in the tree must include state information on a reserved property
878      * called `dataState`. Valid values are `create`, `update` and `delete`. Objects with other dataState values including
879      * `undefined` will be ignored. State values can be added using `XT.jsonpatch.updateState(obj, state)`.
880      *
881      * @seealso XT.jsonpatch.updateState
882      * @param {Object} Options
883      * @param {String} [options.nameSpace] Namespace. Required.
884      * @param {String} [options.type] Type. Required.
885      * @param {Object} [options.data] The data payload to be processed. Required
886      * @param {Number} [options.etag] Record version for optimistic locking.
887      * @param {Object} [options.lock] Lock information for pessemistic locking.
888      * @param {Boolean} [options.superUser=false] If true ignore privilege checking.
889      * @param {String} [options.encryptionKey] Encryption key.
890      */
891     commitRecord: function (options) {
892       var data = options.data,
893         dataState = data ? data.dataState : false,
894         hasAccess = options.superUser ||
895           this.checkPrivileges(options.nameSpace, options.type, data, false);
896
897       if (!hasAccess) { throw new Error("Access Denied."); }
898       switch (dataState)
899       {
900       case (this.CREATED_STATE):
901         this.createRecord(options);
902         break;
903       case (this.UPDATED_STATE):
904         this.updateRecord(options);
905         break;
906       case (this.DELETED_STATE):
907         this.deleteRecord(options);
908       }
909     },
910
911     /**
912      * Commit insert to the database
913      *
914      * @param {Object} Options
915      * @param {String} [options.nameSpace] Namespace. Required.
916      * @param {String} [options.type] Type. Required.
917      * @param {Object} [options.data] The data payload to be processed. Required.
918      * @param {String} [options.encryptionKey] Encryption key.
919      */
920     createRecord: function (options) {
921       var data = options.data,
922         encryptionKey = options.encryptionKey,
923         i,
924         orm = this.fetchOrm(options.nameSpace, options.type),
925         sql = this.prepareInsert(orm, data, null, encryptionKey),
926         pkey = XT.Orm.primaryKey(orm),
927         rec;
928
929       /* Handle extensions on the same table. */
930       for (var i = 0; i < orm.extensions.length; i++) {
931         if (orm.extensions[i].table === orm.table) {
932           sql = this.prepareInsert(orm.extensions[i], data, sql, encryptionKey);
933         }
934       }
935
936       /* Commit the base record. */
937       if (DEBUG) {
938         XT.debug('createRecord sql =', sql.statement);
939         XT.debug('createRecord values =', sql.values);
940       }
941
942       if (sql.statement) {
943         rec = plv8.execute(sql.statement, sql.values);
944         /* Make sure the primary key is populated */
945         if (!data[pkey]) {
946           data[pkey] = rec[0].id;
947         }
948         /* Make sure the obj_uuid is populated, if applicable */
949         if (!data.obj_uuid && rec[0] && rec[0].obj_uuid) {
950           data.uuid = rec[0].obj_uuid;
951         }
952       }
953
954       /* Handle extensions on other tables. */
955       for (var i = 0; i < orm.extensions.length; i++) {
956         if (orm.extensions[i].table !== orm.table &&
957            !orm.extensions[i].isChild) {
958           sql = this.prepareInsert(orm.extensions[i], data, null, encryptionKey);
959
960           if (DEBUG) {
961             XT.debug('createRecord sql =', sql.statement);
962             XT.debug('createRecord values =', sql.values);
963           }
964
965           if (sql.statement) {
966             plv8.execute(sql.statement, sql.values);
967           }
968         }
969       }
970
971       /* Okay, now lets handle arrays. */
972       this.commitArrays(orm, data, encryptionKey);
973     },
974
975     /**
976      * Use an orm object and a record and build an insert statement. It
977      * returns an object with a table name string, columns array, expressions
978      * array and insert statement string that can be executed.
979      *
980      * The optional params object includes objects columns, expressions
981      * that can be cumulatively added to the result.
982      *
983      * @params {Object} Orm
984      * @params {Object} Record
985      * @params {Object} Params - optional
986      * @params {String} Encryption Key
987      * @returns {Object}
988      */
989     prepareInsert: function (orm, record, params, encryptionKey) {
990       var attr,
991         attributePrivileges,
992         columns,
993         count,
994         encryptQuery,
995         encryptSql,
996         exp,
997         i,
998         iorm,
999         namespace,
1000         nkey,
1001         ormp,
1002         pkey = XT.Orm.primaryKey(orm),
1003         prop,
1004         query,
1005         sql = "select nextval($1) as id",
1006         table,
1007         toOneQuery,
1008         toOneSql,
1009         type,
1010         val,
1011         isValidSql = params && params.statement ? true : false,
1012         canEdit;
1013
1014       params = params || {
1015         table: "",
1016         columns: [],
1017         expressions: [],
1018         identifiers: [],
1019         values: []
1020       };
1021       params.table = orm.table;
1022       count = params.values.length + 1;
1023
1024       /* If no primary key, then create one. */
1025       if (!record[pkey] && orm.idSequenceName) {
1026         if (DEBUG) {
1027           XT.debug('prepareInsert sql =', sql);
1028           XT.debug('prepareInsert values =', [orm.idSequenceName]);
1029         }
1030         record[pkey] = plv8.execute(sql, [orm.idSequenceName])[0].id;
1031       }
1032
1033       /* If extension handle key. */
1034       if (orm.relations) {
1035         for (var i = 0; i < orm.relations.length; i++) {
1036           column = orm.relations[i].column;
1037           if (!params.identifiers.contains(column)) {
1038             params.columns.push("%" + count + "$I");
1039             params.values.push(record[orm.relations[i].inverse]);
1040             params.expressions.push('$' + count);
1041             params.identifiers.push(orm.relations[i].column);
1042             count++;
1043           }
1044         }
1045       }
1046
1047       /* Build up the content for insert of this record. */
1048       for (var i = 0; i < orm.properties.length; i++) {
1049         ormp = orm.properties[i];
1050         prop = ormp.name;
1051
1052         if (ormp.toMany && ormp.toMany.column === 'obj_uuid') {
1053           params.parentUuid = true;
1054         }
1055
1056         attr = ormp.attr ? ormp.attr : ormp.toOne ? ormp.toOne : ormp.toMany;
1057         type = attr.type;
1058         iorm = ormp.toOne ? this.fetchOrm(orm.nameSpace, ormp.toOne.type) : false,
1059         nkey = iorm ? XT.Orm.naturalKey(iorm, true) : false;
1060         val = ormp.toOne && record[prop] instanceof Object ?
1061           record[prop][nkey || ormp.toOne.inverse || 'id'] : record[prop];
1062
1063         /**
1064          * Ignore derived fields for insert/update
1065          */
1066         if (attr.derived) continue;
1067
1068         attributePrivileges = orm.privileges &&
1069           orm.privileges.attribute &&
1070           orm.privileges.attribute[prop];
1071
1072         if(!attributePrivileges || attributePrivileges.create === undefined) {
1073           canEdit = true;
1074         } else if (typeof attributePrivileges.create === 'string') {
1075           canEdit = this.checkPrivilege(attributePrivileges.create);
1076         } else {
1077           canEdit = attributePrivileges.create; /* if it's true or false */
1078         }
1079
1080         /* Handle fixed values. */
1081         if (attr.value !== undefined) {
1082           params.columns.push("%" + count + "$I");
1083           params.expressions.push('$' + count);
1084           params.values.push(attr.value);
1085           params.identifiers.push(attr.column);
1086           isValidSql = true;
1087           count++;
1088
1089         /* Handle passed values. */
1090         } else if (canEdit && val !== undefined && val !== null && !ormp.toMany) {
1091           if (attr.isEncrypted) {
1092             if (encryptionKey) {
1093               encryptQuery = "select encrypt(setbytea(%1$L), setbytea(%2$L), %3$L)";
1094               encryptSql = XT.format(encryptQuery, [record[prop], encryptionKey, 'bf']);
1095               val = record[prop] ? plv8.execute(encryptSql)[0].encrypt : null;
1096               params.columns.push("%" + count + "$I");
1097               params.values.push(val);
1098               params.identifiers.push(attr.column);
1099               params.expressions.push("$" + count);
1100               isValidSql = true;
1101               count++;
1102             } else {
1103               throw new Error("No encryption key provided.");
1104             }
1105           } else {
1106             if (ormp.toOne && nkey) {
1107               if (iorm.table.indexOf(".") > 0) {
1108                 toOneQuery = "select %1$I from %2$I.%3$I where %4$I = $" + count;
1109                 toOneSql = XT.format(toOneQuery, [
1110                     XT.Orm.primaryKey(iorm, true),
1111                     iorm.table.beforeDot(),
1112                     iorm.table.afterDot(),
1113                     nkey
1114                   ]);
1115               } else {
1116                 toOneQuery = "select %1$I from %2$I where %3$I = $" + count;
1117                 toOneSql = XT.format(toOneQuery, [
1118                     XT.Orm.primaryKey(iorm, true),
1119                     iorm.table,
1120                     nkey
1121                   ]);
1122               }
1123               exp = "(" + toOneSql + ")";
1124               params.expressions.push(exp);
1125             } else {
1126               params.expressions.push('$' + count);
1127             }
1128
1129             params.columns.push("%" + count + "$I");
1130             params.values.push(val);
1131             params.identifiers.push(attr.column);
1132             isValidSql = true;
1133             count++;
1134           }
1135         /* Handle null value if applicable. */
1136         } else if (canEdit && val === undefined || val === null) {
1137           if (attr.nullValue) {
1138             params.columns.push("%" + count + "$I");
1139             params.values.push(attr.nullValue);
1140             params.identifiers.push(attr.column);
1141             params.expressions.push('$' + count);
1142             isValidSql = true;
1143             count++;
1144           } else if (attr.required) {
1145             plv8.elog(ERROR, "Attribute " + ormp.name + " is required.");
1146           }
1147         }
1148       }
1149
1150       if (!isValidSql) {
1151         return false;
1152       }
1153
1154       /* Build the insert statement */
1155       columns = params.columns.join(', ');
1156       columns = XT.format(columns, params.identifiers);
1157       expressions = params.expressions.join(', ');
1158       expressions = XT.format(expressions, params.identifiers);
1159
1160       if (params.table.indexOf(".") > 0) {
1161         namespace = params.table.beforeDot();
1162         table = params.table.afterDot();
1163         query = 'insert into %1$I.%2$I (' + columns + ') values (' + expressions + ')';
1164         params.statement = XT.format(query, [namespace, table]);
1165       } else {
1166         query = 'insert into %1$I (' + columns + ') values (' + expressions + ')';
1167         params.statement = XT.format(query, [params.table]);
1168       }
1169
1170       /* If we can get the primary key column we want to return that
1171          for cases where it is determined behind the scenes */
1172       if (!record[pkey] && !params.primaryKey) {
1173         params.primaryKey = XT.Orm.primaryKey(orm, true);
1174       }
1175
1176       if (params.primaryKey && params.parentUuid) {
1177         params.statement = params.statement + ' returning ' + params.primaryKey + ' as id, obj_uuid';
1178       } else if (params.parentUuid) {
1179         params.statement = params.statement + ' returning obj_uuid';
1180       } else if (params.primaryKey) {
1181         params.statement = params.statement + ' returning ' + params.primaryKey + ' as id';
1182       }
1183
1184       if (DEBUG) {
1185         XT.debug('prepareInsert statement =', params.statement);
1186         XT.debug('prepareInsert values =', params.values);
1187       }
1188
1189       return params;
1190     },
1191
1192     /**
1193      * Commit update to the database
1194      *
1195      * @param {Object} Options
1196      * @param {String} [options.nameSpace] Namespace. Required.
1197      * @param {String} [options.type] Type. Required.
1198      * @param {Object} [options.data] The data payload to be processed. Required.
1199      * @param {Number} [options.etag] Record version for optimistic locking.
1200      * @param {Object} [options.lock] Lock information for pessemistic locking.
1201      * @param {String} [options.encryptionKey] Encryption key.
1202      */
1203     updateRecord: function (options) {
1204       var data = options.data,
1205         encryptionKey = options.encryptionKey,
1206         orm = this.fetchOrm(options.nameSpace, options.type),
1207         pkey = XT.Orm.primaryKey(orm),
1208         id = data[pkey],
1209         ext,
1210         etag = this.getVersion(orm, id),
1211         i,
1212         iORuQuery,
1213         iORuSql,
1214         lock,
1215         lockKey = options.lock && options.lock.key ? options.lock.key : false,
1216         lockTable = orm.lockTable || orm.table,
1217         rows,
1218         sql = this.prepareUpdate(orm, data, null, encryptionKey);
1219
1220       /* Test for optimistic lock. */
1221       if (!XT.disableLocks && etag && options.etag !== etag) {
1222       // TODO - Improve error handling.
1223         plv8.elog(ERROR, "The version being updated is not current.");
1224       }
1225       /* Test for pessimistic lock. */
1226       if (orm.lockable) {
1227         lock = this.tryLock(lockTable, id, {key: lockKey});
1228         if (!lock.key) {
1229           // TODO - Improve error handling.
1230           plv8.elog(ERROR, "Can not obtain a lock on the record.");
1231         }
1232       }
1233
1234       /* Okay, now lets handle arrays. */
1235       this.commitArrays(orm, data, encryptionKey);
1236
1237       /* Handle extensions on the same table. */
1238       for (var i = 0; i < orm.extensions.length; i++) {
1239         if (orm.extensions[i].table === orm.table) {
1240           sql = this.prepareUpdate(orm.extensions[i], data, sql, encryptionKey);
1241         }
1242       }
1243
1244       sql.values.push(id);
1245
1246       /* Commit the base record. */
1247       if (DEBUG) {
1248         XT.debug('updateRecord sql =', sql.statement);
1249         XT.debug('updateRecord values =', sql.values);
1250       }
1251       plv8.execute(sql.statement, sql.values);
1252
1253       /* Handle extensions on other tables. */
1254       for (var i = 0; i < orm.extensions.length; i++) {
1255         ext = orm.extensions[i];
1256         if (ext.table !== orm.table &&
1257            !ext.isChild) {
1258
1259           /* Determine whether to insert or update. */
1260           if (ext.table.indexOf(".") > 0) {
1261             iORuQuery = "select %1$I from %2$I.%3$I where %1$I = $1;";
1262             iORuSql = XT.format(iORuQuery, [
1263                 ext.relations[0].column,
1264                 ext.table.beforeDot(),
1265                 ext.table.afterDot()
1266               ]);
1267           } else {
1268             iORuQuery = "select %1$I from %2$I where %1$I = $1;";
1269             iORuSql = XT.format(iORuQuery, [ext.relations[0].column, ext.table]);
1270           }
1271
1272           if (DEBUG) {
1273             XT.debug('updateRecord sql =', iORuSql);
1274             XT.debug('updateRecord values =', [data[pkey]]);
1275           }
1276           rows = plv8.execute(iORuSql, [data[pkey]]);
1277
1278           if (rows.length) {
1279             sql = this.prepareUpdate(ext, data, null, encryptionKey);
1280             sql.values.push(id);
1281           } else {
1282             sql = this.prepareInsert(ext, data, null, encryptionKey);
1283           }
1284
1285           if (DEBUG) {
1286             XT.debug('updateRecord sql =', sql.statement);
1287             XT.debug('updateRecord values =', sql.values);
1288           }
1289
1290           if (sql.statement) {
1291             plv8.execute(sql.statement, sql.values);
1292           }
1293         }
1294       }
1295
1296       /* Release any lock. */
1297       if (orm.lockable) {
1298         this.releaseLock({table: lockTable, id: id});
1299       }
1300     },
1301
1302     /**
1303      * Use an orm object and a record and build an update statement. It
1304      * returns an object with a table name string, expressions array and
1305      * insert statement string that can be executed.
1306      *
1307      * The optional params object includes objects columns, expressions
1308      * that can be cumulatively added to the result.
1309      *
1310      * @params {Object} Orm
1311      * @params {Object} Record
1312      * @params {Object} Params - optional
1313      * @returns {Object}
1314      */
1315     prepareUpdate: function (orm, record, params, encryptionKey) {
1316       var attr,
1317         attributePrivileges,
1318         columnKey,
1319         count,
1320         encryptQuery,
1321         encryptSql,
1322         exp,
1323         expressions,
1324         iorm,
1325         key,
1326         keyValue,
1327         namespace,
1328         ormp,
1329         pkey,
1330         prop,
1331         query,
1332         table,
1333         toOneQuery,
1334         toOneSql,
1335         type,
1336         val,
1337         isValidSql = false,
1338         canEdit;
1339
1340       params = params || {
1341         table: "",
1342         expressions: [],
1343         identifiers: [],
1344         values: []
1345       };
1346       params.table = orm.table;
1347       count = params.values.length + 1;
1348
1349       if (orm.relations) {
1350         /* Extension. */
1351         pkey = orm.relations[0].inverse;
1352         columnKey = orm.relations[0].column;
1353       } else {
1354         /* Base. */
1355         pkey = XT.Orm.primaryKey(orm);
1356         columnKey = XT.Orm.primaryKey(orm, true);
1357       }
1358
1359       /* Build up the content for update of this record. */
1360       for (var i = 0; i < orm.properties.length; i++) {
1361         ormp = orm.properties[i];
1362         prop = ormp.name;
1363         attr = ormp.attr ? ormp.attr : ormp.toOne ? ormp.toOne : ormp.toMany;
1364         type = attr.type;
1365         iorm = ormp.toOne ? this.fetchOrm(orm.nameSpace, ormp.toOne.type) : false;
1366         nkey = iorm ? XT.Orm.naturalKey(iorm, true) : false;
1367         val = ormp.toOne && record[prop] instanceof Object ?
1368           record[prop][nkey || ormp.toOne.inverse || 'id'] : record[prop],
1369
1370         attributePrivileges = orm.privileges &&
1371           orm.privileges.attribute &&
1372           orm.privileges.attribute[prop];
1373
1374         /**
1375          * Ignore derived fields for insert/update
1376          */
1377         if (attr.derived) continue;
1378
1379         if(!attributePrivileges || attributePrivileges.update === undefined) {
1380           canEdit = true;
1381         } else if (typeof attributePrivileges.update === 'string') {
1382           canEdit = this.checkPrivilege(attributePrivileges.update);
1383         } else {
1384           canEdit = attributePrivileges.update; /* if it's true or false */
1385         }
1386
1387         if (canEdit && val !== undefined && !ormp.toMany) {
1388
1389           /* Handle encryption if applicable. */
1390           if (attr.isEncrypted) {
1391             if (encryptionKey) {
1392               encryptQuery = "select encrypt(setbytea(%1$L), setbytea(%2$L), %3$L)";
1393               encryptSql = XT.format(encryptQuery, [val, encryptionKey, 'bf']);
1394               val = record[prop] ? plv8.execute(encryptSql)[0].encrypt : null;
1395               params.values.push(val);
1396               params.identifiers.push(attr.column);
1397               params.expressions.push("%" + count + "$I = $" + count);
1398               isValidSql = true;
1399               count++;
1400             } else {
1401               // TODO - Improve error handling.
1402               throw new Error("No encryption key provided.");
1403             }
1404           } else if (ormp.name !== pkey) {
1405             if (val === null) {
1406               if (attr.required) {
1407                 plv8.elog(ERROR, "Attribute " + ormp.name + " is required.");
1408               } else {
1409                 params.values.push(attr.nullValue || null);
1410                 params.expressions.push("%" + count + "$I = $" + count);
1411               }
1412             } else if (ormp.toOne && nkey) {
1413               if (iorm.table.indexOf(".") > 0) {
1414                 toOneQuery = "select %1$I from %2$I.%3$I where %4$I = $" + count;
1415                 toOneSql = XT.format(toOneQuery, [
1416                     XT.Orm.primaryKey(iorm, true),
1417                     iorm.table.beforeDot(),
1418                     iorm.table.afterDot(),
1419                     nkey
1420                   ]);
1421               } else {
1422                 toOneQuery = "select %1$I from %2$I where %3$I = $" + count;
1423                 toOneSql = XT.format(toOneQuery, [
1424                     XT.Orm.primaryKey(iorm, true),
1425                     iorm.table,
1426                     nkey
1427                   ]);
1428               }
1429
1430               exp = "%" + count + "$I = (" + toOneSql + ")";
1431               params.values.push(val);
1432               params.expressions.push(exp);
1433             } else {
1434               params.values.push(val);
1435               params.expressions.push("%" + count + "$I = $" + count);
1436             }
1437             params.identifiers.push(attr.column);
1438             isValidSql = true;
1439             count++;
1440           }
1441         }
1442       }
1443
1444       /* Build the update statement */
1445       expressions = params.expressions.join(', ');
1446       expressions = XT.format(expressions, params.identifiers);
1447
1448       // do not send an invalid sql statement
1449       if (!isValidSql) { return params; }
1450
1451       if (params.table.indexOf(".") > 0) {
1452         namespace = params.table.beforeDot();
1453         table = params.table.afterDot();
1454         query = 'update %1$I.%2$I set ' + expressions + ' where %3$I = $' + count + ';';
1455         params.statement = XT.format(query, [namespace, table, columnKey]);
1456       } else {
1457         query = 'update %1$I set ' + expressions + ' where %2$I = $' + count + ';';
1458         params.statement = XT.format(query, [params.table, columnKey]);
1459       }
1460
1461       if (DEBUG) {
1462         XT.debug('prepareUpdate statement =', params.statement);
1463         XT.debug('prepareUpdate values =', params.values);
1464       }
1465
1466       return params;
1467     },
1468
1469     /**
1470      * Commit deletion to the database
1471      *
1472      * @param {Object} Options
1473      * @param {String} [options.nameSpace] Namespace. Required.
1474      * @param {String} [options.type] Type. Required.
1475      * @param {Object} [options.data] The data payload to be processed. Required.
1476      * @param {Number} [options.etag] Optional record id version for optimistic locking.
1477      *  If set and version does not match, delete will fail.
1478      * @param {Number} [options.lock] Lock information for pessemistic locking.
1479      */
1480     deleteRecord: function (options) {
1481       var data = options.data,
1482         orm = this.fetchOrm(options.nameSpace, options.type, {silentError: true}),
1483         pkey,
1484         nkey,
1485         id,
1486         columnKey,
1487         etag,
1488         ext,
1489         i,
1490         lockKey = options.lock && options.lock.key ? options.lock.key : false,
1491         lockTable,
1492         namespace,
1493         prop,
1494         ormp,
1495         query = '',
1496         sql = '',
1497         table,
1498         values;
1499
1500       /* Set variables or return false with message. */
1501       if (!orm) {
1502         throw new handleError("Not Found", 404);
1503       }
1504
1505       pkey = XT.Orm.primaryKey(orm);
1506       nkey = XT.Orm.naturalKey(orm);
1507       lockTable = orm.lockTable || orm.table;
1508       if (!pkey && !nkey) {
1509         throw new handleError("Not Found", 404);
1510       }
1511
1512       id = nkey ? this.getId(orm, data[nkey]) : data[pkey];
1513       if (!id) {
1514         throw new handleError("Not Found", 404);
1515       }
1516
1517       /* Test for optional optimistic lock. */
1518       etag = this.getVersion(orm, id);
1519       if (etag && options.etag && etag !== options.etag) {
1520         throw new handleError("Precondition Required", 428);
1521       }
1522
1523       /* Test for pessemistic lock. */
1524       if (orm.lockable) {
1525         lock = this.tryLock(lockTable, id, {key: lockKey});
1526         if (!lock.key) {
1527           throw new handleError("Conflict", 409);
1528         }
1529       }
1530
1531       /* Delete children first. */
1532       for (prop in data) {
1533         ormp = XT.Orm.getProperty(orm, prop);
1534
1535         /* If the property is an array of objects they must be records so delete them. */
1536         if (ormp.toMany && ormp.toMany.isNested) {
1537           values = data[prop];
1538           for (var i = 0; i < values.length; i++) {
1539             this.deleteRecord({
1540               nameSpace: options.nameSpace,
1541               type: ormp.toMany.type,
1542               data: values[i]
1543             });
1544           }
1545         }
1546       }
1547
1548       /* Next delete from extension tables. */
1549       for (var i = 0; i < orm.extensions.length; i++) {
1550         ext = orm.extensions[i];
1551         if (ext.table !== orm.table &&
1552             !ext.isChild) {
1553           columnKey = ext.relations[0].column;
1554           nameKey = ext.relations[0].inverse;
1555
1556           if (ext.table.indexOf(".") > 0) {
1557             namespace = ext.table.beforeDot();
1558             table = ext.table.afterDot();
1559             query = 'delete from %1$I.%2$I where %3$I = $1';
1560             sql = XT.format(query, [namespace, table, columnKey]);
1561           } else {
1562             query = 'delete from %1$I where %2$I = $1';
1563             sql = XT.format(query, [ext.table, columnKey]);
1564           }
1565
1566           if (DEBUG) {
1567             XT.debug('deleteRecord sql =', sql);
1568             XT.debug('deleteRecord values =',  [id]);
1569           }
1570           plv8.execute(sql, [id]);
1571         }
1572       }
1573
1574       /* Now delete the top. */
1575       nameKey = XT.Orm.primaryKey(orm);
1576       columnKey = XT.Orm.primaryKey(orm, true);
1577
1578       if (orm.table.indexOf(".") > 0) {
1579         namespace = orm.table.beforeDot();
1580         table = orm.table.afterDot();
1581         query = 'delete from %1$I.%2$I where %3$I = $1';
1582         sql = XT.format(query, [namespace, table, columnKey]);
1583       } else {
1584         query = 'delete from %1$I where %2$I = $1';
1585         sql = XT.format(query, [orm.table, columnKey]);
1586       }
1587
1588       /* Commit the record.*/
1589       if (DEBUG) {
1590         XT.debug('deleteRecord sql =', sql);
1591         XT.debug('deleteRecord values =', [id]);
1592       }
1593       plv8.execute(sql, [id]);
1594
1595       /* Release any lock. */
1596       if (orm.lockable) {
1597         this.releaseLock({table: lockTable, id: id});
1598       }
1599     },
1600
1601     /**
1602      * Decrypts properties where applicable.
1603      *
1604      * @param {String} name space
1605      * @param {String} type
1606      * @param {Object} record
1607      * @param {Object} encryption key
1608      * @returns {Object}
1609      */
1610     decrypt: function (nameSpace, type, record, encryptionKey) {
1611       var result,
1612         that = this,
1613         hexToAlpha = function (hex) {
1614           var str = '', i;
1615           for (i = 2; i < hex.length; i += 2) {
1616             str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
1617           }
1618           return str;
1619         },
1620         orm = this.fetchOrm(nameSpace, type);
1621
1622       for (prop in record) {
1623         var ormp = XT.Orm.getProperty(orm, prop.camelize());
1624
1625         /* Decrypt property if applicable. */
1626         if (ormp && ormp.attr && ormp.attr.isEncrypted) {
1627           if (encryptionKey) {
1628             sql = "select formatbytea(decrypt($1, setbytea($2), 'bf')) as result";
1629             // TODO - Handle not found error.
1630
1631             if (DEBUG && false) {
1632               XT.debug('decrypt prop =', prop);
1633               XT.debug('decrypt sql =', sql);
1634               XT.debug('decrypt values =', [record[prop], encryptionKey]);
1635             }
1636             result = plv8.execute(sql, [record[prop], encryptionKey])[0].result;
1637             /* we SOMETIMES need to translate from hex here */
1638             if(typeof result === 'string' && result.substring(0, 2) === '\\x') {
1639               result = result ? hexToAlpha(result) : result;
1640             }
1641             /* in the special case of encrypted credit card numbers, we don't give the
1642               user the full decrypted number EVEN IF they have the encryption key */
1643             if(ormp.attr.isEncrypted === "credit_card_number" && result && result.length >= 4) {
1644               record[prop] = "************" + result.substring(result.length - 4);
1645             } else {
1646               record[prop] = result;
1647             }
1648           } else {
1649             record[prop] = '**********';
1650           }
1651
1652         /* Check recursively. */
1653         } else if (ormp.toOne && ormp.toOne.isNested) {
1654           that.decrypt(nameSpace, ormp.toOne.type, record[prop], encryptionKey);
1655
1656         } else if (ormp.toMany && ormp.toMany.isNested) {
1657           record[prop].map(function (subdata) {
1658             that.decrypt(nameSpace, ormp.toMany.type, subdata, encryptionKey);
1659           });
1660         }
1661       }
1662
1663       return record;
1664     },
1665
1666     /**
1667       Fetches the ORM. Caches the result in this data object, where it can be used
1668       for this request but will be conveniently forgotten between requests.
1669      */
1670     fetchOrm: function (nameSpace, type) {
1671       var res,
1672         ret,
1673         recordType = nameSpace + '.'+ type;
1674
1675       if (!this._maps) {
1676         this._maps = [];
1677       }
1678
1679       res = this._maps.findProperty('recordType', recordType);
1680       if (res) {
1681         ret = res.map;
1682       } else {
1683         ret = XT.Orm.fetch(nameSpace, type);
1684
1685         /* cache the result so we don't requery needlessly */
1686         this._maps.push({ "recordType": recordType, "map": ret});
1687       }
1688       return ret;
1689     },
1690
1691     /**
1692      * Get the oid for a given table name.
1693      *
1694      * @param {String} table name
1695      * @returns {Number}
1696      */
1697     getTableOid: function (table) {
1698       var tableName = this.getTableFromNamespacedTable(table).toLowerCase(), /* be generous */
1699         namespace = this.getNamespaceFromNamespacedTable(table),
1700         ret,
1701         sql = "select pg_class.oid::integer as oid " +
1702              "from pg_class join pg_namespace on relnamespace = pg_namespace.oid " +
1703              "where relname = $1 and nspname = $2";
1704
1705       if (DEBUG) {
1706         XT.debug('getTableOid sql =', sql);
1707         XT.debug('getTableOid values =', [tableName, namespace]);
1708       }
1709       ret = plv8.execute(sql, [tableName, namespace])[0].oid - 0;
1710
1711       // TODO - Handle not found error.
1712
1713       return ret;
1714     },
1715
1716     /**
1717      * Get the primary key id for an object based on a passed in natural key.
1718      *
1719      * @param {Object} Orm
1720      * @param {String} Natural key value
1721      */
1722     getId: function (orm, value) {
1723       var ncol = XT.Orm.naturalKey(orm, true),
1724         pcol = XT.Orm.primaryKey(orm, true),
1725         query,
1726         ret,
1727         sql;
1728
1729       if (orm.table.indexOf(".") > 0) {
1730         namespace = orm.table.beforeDot();
1731         table = orm.table.afterDot();
1732         query = "select %1$I as id from %2$I.%3$I where %4$I = $1";
1733         sql = XT.format(query, [pcol, namespace, table, ncol]);
1734       } else {
1735         query = "select %1$I as id from %2$I where %3$I = $1";
1736         sql = XT.format(query, [pcol, orm.table, ncol]);
1737       }
1738
1739       if (DEBUG) {
1740         XT.debug('getId sql =', sql);
1741         XT.debug('getId values =', [value]);
1742       }
1743
1744       ret = plv8.execute(sql, [value]);
1745
1746       if(ret.length) {
1747         return ret[0].id;
1748       } else {
1749         throw new handleError("Primary Key not found on " + orm.table +
1750           " where " + ncol + " = " + value, 400);
1751       }
1752     },
1753
1754     getNamespaceFromNamespacedTable: function (fullName) {
1755       return fullName.indexOf(".") > 0 ? fullName.beforeDot() : "public";
1756     },
1757
1758     getTableFromNamespacedTable: function (fullName) {
1759       return fullName.indexOf(".") > 0 ? fullName.afterDot() : fullName;
1760     },
1761
1762     getPgTypeFromOrmType: function (schema, table, column) {
1763       var sql = "select data_type from information_schema.columns " +
1764                 "where true " +
1765                 "and table_schema = $1 " +
1766                 "and table_name = $2 " +
1767                 "and column_name = $3;",
1768           pgType,
1769           values = [schema, table, column];
1770
1771       if (DEBUG) {
1772         XT.debug('getPgTypeFromOrmType sql =', sql);
1773         XT.debug('getPgTypeFromOrmType values =', values);
1774       }
1775
1776       pgType = plv8.execute(sql, values);
1777       pgType = pgType && pgType[0] ? pgType[0].data_type : false;
1778
1779       return pgType;
1780     },
1781
1782     /**
1783      * Get the natural key id for an object based on a passed in primary key.
1784      *
1785      * @param {Object} Orm
1786      * @param {Number|String} Primary key value
1787      * @param {Boolean} safe Return the original value instead of erroring if no match is found
1788      */
1789     getNaturalId: function (orm, value, safe) {
1790       var ncol = XT.Orm.naturalKey(orm, true),
1791         pcol = XT.Orm.primaryKey(orm, true),
1792         query,
1793         ret,
1794         sql;
1795
1796       if (orm.table.indexOf(".") > 0) {
1797         namespace = orm.table.beforeDot();
1798         table = orm.table.afterDot();
1799         query = "select %1$I as id from %2$I.%3$I where %4$I = $1";
1800         sql = XT.format(query, [ncol, namespace, table, pcol]);
1801       } else {
1802         query = "select %1$I as id from %2$I where %3$I = $1";
1803         sql = XT.format(query, [ncol, orm.table, pcol]);
1804       }
1805
1806       if (DEBUG) {
1807         XT.debug('getNaturalId sql =', sql);
1808         XT.debug('getNaturalId values =', [value]);
1809       }
1810
1811       ret = plv8.execute(sql, [value]);
1812
1813       if (ret.length) {
1814         return ret[0].id;
1815       } else if (safe) {
1816         return value;
1817       } else {
1818         throw new handleError("Natural Key Not Found: " + orm.nameSpace + "." + orm.type, 400);
1819       }
1820     },
1821
1822     /**
1823      * Returns the current version of a record.
1824      *
1825      * @param {Object} Orm
1826      * @param {Number|String} Record id
1827      */
1828     getVersion: function (orm, id) {
1829       if (!orm.lockable) { return; }
1830
1831       var etag,
1832         oid = this.getTableOid(orm.lockTable || orm.table),
1833         res,
1834         sql = 'select ver_etag from xt.ver where ver_table_oid = $1 and ver_record_id = $2;';
1835
1836       if (DEBUG) {
1837         XT.debug('getVersion sql = ', sql);
1838         XT.debug('getVersion values = ', [oid, id]);
1839       }
1840       res = plv8.execute(sql, [oid, id]);
1841       etag = res.length ? res[0].ver_etag : false;
1842
1843       if (!etag) {
1844         etag = XT.generateUUID();
1845         sql = 'insert into xt.ver (ver_table_oid, ver_record_id, ver_etag) values ($1, $2, $3::uuid);';
1846         // TODO - Handle insert error.
1847
1848         if (DEBUG) {
1849           XT.debug('getVersion insert sql = ', sql);
1850           XT.debug('getVersion insert values = ', [oid, id, etag]);
1851         }
1852         plv8.execute(sql, [oid, id, etag]);
1853       }
1854
1855       return etag;
1856     },
1857
1858     /**
1859      * Fetch an array of records from the database.
1860      *
1861      * @param {Object} Options
1862      * @param {String} [dataHash.nameSpace] Namespace. Required.
1863      * @param {String} [dataHash.type] Type. Required.
1864      * @param {Array} [dataHash.parameters] Parameters
1865      * @param {Array} [dataHash.orderBy] Order by - optional
1866      * @param {Number} [dataHash.rowLimit] Row limit - optional
1867      * @param {Number} [dataHash.rowOffset] Row offset - optional
1868      * @returns Array
1869      */
1870     fetch: function (options) {
1871       var nameSpace = options.nameSpace,
1872         type = options.type,
1873         query = options.query || {},
1874         encryptionKey = options.encryptionKey,
1875         orderBy = query.orderBy,
1876         orm = this.fetchOrm(nameSpace, type),
1877         table,
1878         tableNamespace,
1879         parameters = query.parameters,
1880         clause = this.buildClause(nameSpace, type, parameters, orderBy),
1881         i,
1882         pkey = XT.Orm.primaryKey(orm),
1883         pkeyColumn = XT.Orm.primaryKey(orm, true),
1884         nkey = XT.Orm.naturalKey(orm),
1885         limit = query.rowLimit ? XT.format('limit %1$L', [query.rowLimit]) : '',
1886         offset = query.rowOffset ? XT.format('offset %1$L', [query.rowOffset]) : '',
1887         parts,
1888         ret = {
1889           nameSpace: nameSpace,
1890           type: type
1891         },
1892         qry,
1893         ids = [],
1894         idParams = [],
1895         counter = 1,
1896         sqlCount,
1897         etags,
1898         sql_etags,
1899         sql1 = 'select t1.%3$I as id from %1$I.%2$I t1 {joins} where {conditions} group by t1.%3$I{groupBy} {orderBy} {limit} {offset};',
1900         sql2 = 'select * from %1$I.%2$I where %3$I in ({ids}) {orderBy}';
1901
1902       /* Validate - don't bother running the query if the user has no privileges. */
1903       if (!this.checkPrivileges(nameSpace, type)) { return []; }
1904
1905       tableNamespace = this.getNamespaceFromNamespacedTable(orm.table);
1906       table = this.getTableFromNamespacedTable(orm.table);
1907
1908       if (query.count) {
1909         /* Just get the count of rows that match the conditions */
1910         sqlCount = 'select count(distinct t1.%3$I) as count from %1$I.%2$I t1 {joins} where {conditions};';
1911         sqlCount = XT.format(sqlCount, [tableNamespace.decamelize(), table.decamelize(), pkeyColumn]);
1912         sqlCount = sqlCount.replace('{joins}', clause.joins)
1913                            .replace('{conditions}', clause.conditions);
1914
1915         if (DEBUG) {
1916           XT.debug('fetch sqlCount = ', sqlCount);
1917           XT.debug('fetch values = ', clause.parameters);
1918         }
1919
1920         ret.data = plv8.execute(sqlCount, clause.parameters);
1921         return ret;
1922       }
1923
1924       /* Because we query views of views, you can get inconsistent results */
1925       /* when doing limit and offest queries without an order by. Add a default. */
1926       if (limit && offset && (!orderBy || !orderBy.length) && !clause.orderByColumns) {
1927         /* We only want this on sql1, not sql2's clause.orderBy. */
1928         clause.orderByColumns = XT.format('order by t1.%1$I', [pkeyColumn]);
1929       }
1930
1931       /* Query the model. */
1932       sql1 = XT.format(sql1, [tableNamespace.decamelize(), table.decamelize(), pkeyColumn]);
1933       sql1 = sql1.replace('{joins}', clause.joins)
1934                  .replace('{conditions}', clause.conditions)
1935                  .replace(/{groupBy}/g, clause.groupByColumns)
1936                  .replace(/{orderBy}/g, clause.orderByColumns)
1937                  .replace('{limit}', limit)
1938                  .replace('{offset}', offset);
1939
1940       if (DEBUG) {
1941         XT.debug('fetch sql1 = ', sql1);
1942         XT.debug('fetch values = ', clause.parameters);
1943       }
1944
1945       /* First query for matching ids, then get entire result set. */
1946       /* This improves performance over a direct query on the view due */
1947       /* to the way sorting is handled by the query optimizer */
1948       qry = plv8.execute(sql1, clause.parameters) || [];
1949       if (!qry.length) { return [] };
1950       qry.forEach(function (row) {
1951         ids.push(row.id);
1952         idParams.push("$" + counter);
1953         counter++;
1954       });
1955
1956       if (orm.lockable) {
1957         sql_etags = "select ver_etag as etag, ver_record_id as id " +
1958                     "from xt.ver " +
1959                     "where ver_table_oid = ( " +
1960                       "select pg_class.oid::integer as oid " +
1961                       "from pg_class join pg_namespace on relnamespace = pg_namespace.oid " +
1962                       /* Note: using $L for quoted literal e.g. 'contact', not an identifier. */
1963                       "where nspname = %1$L and relname = %2$L " +
1964                     ") " +
1965                     "and ver_record_id in ({ids})";
1966         sql_etags = XT.format(sql_etags, [tableNamespace, table]);
1967         sql_etags = sql_etags.replace('{ids}', idParams.join());
1968
1969         if (DEBUG) {
1970           XT.debug('fetch sql_etags = ', sql_etags);
1971           XT.debug('fetch etags_values = ', JSON.stringify(ids));
1972         }
1973         etags = plv8.execute(sql_etags, ids) || {};
1974         ret.etags = {};
1975       }
1976
1977       sql2 = XT.format(sql2, [nameSpace.decamelize(), type.decamelize(), pkey]);
1978       sql2 = sql2.replace(/{orderBy}/g, clause.orderBy)
1979                  .replace('{ids}', idParams.join());
1980
1981       if (DEBUG) {
1982         XT.debug('fetch sql2 = ', sql2);
1983         XT.debug('fetch values = ', JSON.stringify(ids));
1984       }
1985       ret.data = plv8.execute(sql2, ids) || [];
1986
1987       for (var i = 0; i < ret.data.length; i++) {
1988         ret.data[i] = this.decrypt(nameSpace, type, ret.data[i], encryptionKey);
1989
1990         if (etags) {
1991           /* Add etags to result in pkey->etag format. */
1992           for (var j = 0; j < etags.length; j++) {
1993             if (etags[j].id === ret.data[i][pkey]) {
1994               ret.etags[ret.data[i][nkey]] = etags[j].etag;
1995             }
1996           }
1997         }
1998       }
1999
2000       this.sanitize(nameSpace, type, ret.data, options);
2001
2002       return ret;
2003     },
2004
2005     /**
2006     Fetch a metric value.
2007
2008     @param {String} Metric name
2009     @param {String} Return type 'text', 'boolean' or 'number' (default 'text')
2010     */
2011     fetchMetric: function (name, type) {
2012       var fn = 'fetchmetrictext';
2013       if (type === 'boolean') {
2014         fn = 'fetchmetricbool';
2015       } else if (type === 'number') {
2016         fn = 'fetchmetricvalue';
2017       }
2018       return plv8.execute("select " + fn + "($1) as resp", [name])[0].resp;
2019     },
2020
2021     /**
2022      * Retreives a record from the database. If the user does not have appropriate privileges an
2023      * error will be thrown unless the `silentError` option is passed.
2024      *
2025      * If `context` is passed as an option then a record will only be returned if it exists in the context (parent)
2026      * record which itself must be accessible by the effective user.
2027      *
2028      * @param {Object} options
2029      * @param {String} [options.nameSpace] Namespace. Required.
2030      * @param {String} [options.type] Type. Required.
2031      * @param {Number} [options.id] Record id. Required.
2032      * @param {Boolean} [options.superUser=false] If true ignore privilege checking.
2033      * @param {String} [options.encryptionKey] Encryption key
2034      * @param {Boolean} [options.silentError=false] Silence errors
2035      * @param {Object} [options.context] Context
2036      * @param {String} [options.context.nameSpace] Context namespace.
2037      * @param {String} [options.context.type] The type of context object.
2038      * @param {String} [options.context.value] The value of the context's primary key.
2039      * @param {String} [options.context.relation] The name of the attribute on the type to which this record is related.
2040      * @returns Object
2041      */
2042     retrieveRecord: function (options) {
2043       options = options ? options : {};
2044       options.obtainLock = false;
2045
2046       var id = options.id,
2047         nameSpace = options.nameSpace,
2048         type = options.type,
2049         map = this.fetchOrm(nameSpace, type),
2050         context = options.context,
2051         encryptionKey = options.encryptionKey,
2052         join = "",
2053         lockTable = map.lockTable || map.table,
2054         nkey = XT.Orm.naturalKey(map),
2055         params = {},
2056         pkey = XT.Orm.primaryKey(map),
2057         ret = {
2058           nameSpace: nameSpace,
2059           type: type,
2060           id: id
2061         },
2062         sql;
2063
2064       if (!pkey) {
2065         throw new Error('No key found for {nameSpace}.{type}'
2066                         .replace("{nameSpace}", nameSpace)
2067                         .replace("{type}", type));
2068       }
2069
2070       /* If this object uses a natural key, go get the primary key id. */
2071       if (nkey) {
2072         id = this.getId(map, id);
2073         if (!id) {
2074           return false;
2075         }
2076       }
2077
2078       /* Context means search for this record inside another. */
2079       if (context) {
2080         context.nameSpace = context.nameSpace || context.recordType.beforeDot();
2081         context.type = context.type || context.recordType.afterDot()
2082         context.map = this.fetchOrm(context.nameSpace, context.type);
2083         context.prop = XT.Orm.getProperty(context.map, context.relation);
2084         context.pertinentExtension = XT.Orm.getProperty(context.map, context.relation, true);
2085         context.underlyingTable = context.pertinentExtension.table,
2086         context.underlyingNameSpace = this.getNamespaceFromNamespacedTable(context.underlyingTable);
2087         context.underlyingType = this.getTableFromNamespacedTable(context.underlyingTable);
2088         context.fkey = context.prop.toMany.inverse;
2089         context.fkeyColumn = context.prop.toMany.column;
2090         context.pkey = XT.Orm.naturalKey(context.map) || XT.Orm.primaryKey(context.map);
2091         params.attribute = context.pkey;
2092         params.value = context.value;
2093
2094         join = 'join %1$I.%2$I on (%1$I.%2$I.%3$I = %4$I.%5$I)';
2095         join = XT.format(join, [
2096             context.underlyingNameSpace,
2097             context.underlyingType,
2098             context.fkeyColumn,
2099             type.decamelize(),
2100             context.fkey
2101           ]);
2102       }
2103
2104       /* Validate - don't bother running the query if the user has no privileges. */
2105       if(!options.superUser && !context && !this.checkPrivileges(nameSpace, type)) {
2106         if (options.silentError) {
2107           return false;
2108         } else {
2109           throw new handleError("Unauthorized", 401);
2110         }
2111       }
2112
2113       ret.etag = this.getVersion(map, id);
2114
2115       /* Obtain lock if required. */
2116       if (map.lockable) {
2117         ret.lock = this.tryLock(lockTable, id, options);
2118       }
2119
2120       /* Data sql. */
2121       sql = 'select %1$I.* from %2$I.%1$I {join} where %1$I.%3$I = $1;';
2122       sql = sql.replace(/{join}/, join);
2123       sql = XT.format(sql, [type.decamelize(), nameSpace.decamelize(), pkey]);
2124
2125       /* Query the map. */
2126       if (DEBUG) {
2127         XT.debug('retrieveRecord sql = ', sql);
2128         XT.debug('retrieveRecord values = ', [id]);
2129       }
2130       ret.data = plv8.execute(sql, [id])[0] || {};
2131
2132       if (!context) {
2133         /* Check privileges again, this time against record specific criteria where applicable. */
2134         if(!options.superUser && !this.checkPrivileges(nameSpace, type, ret.data)) {
2135           if (options.silentError) {
2136             return false;
2137           } else {
2138             throw new handleError("Unauthorized", 401);
2139           }
2140         }
2141         /* Decrypt result where applicable. */
2142         ret.data = this.decrypt(nameSpace, type, ret.data, encryptionKey);
2143       }
2144
2145       this.sanitize(nameSpace, type, ret.data, options);
2146
2147       /* Return the results. */
2148       return ret || {};
2149     },
2150
2151     /**
2152      *  Remove unprivileged attributes, primary and foreign keys from the data.
2153      *  Only removes the primary key if a natural key has been specified in the ORM.
2154      *  Also format for printing using XT.format functions if printFormat=true'
2155      *
2156      * @param {String} Namespace
2157      * @param {String} Type
2158      * @param {Object|Array} Data
2159      * @param {Object} Options
2160      * @param {Boolean} [options.includeKeys=false] Do not remove primary and foreign keys.
2161      * @param {Boolean} [options.superUser=false] Do not remove unprivileged attributes.
2162      * @param {Boolean} [options.printFormat=true] Format for printing.
2163      */
2164     sanitize: function (nameSpace, type, data, options) {
2165       options = options || {};
2166       if (options.includeKeys && options.superUser) { return; }
2167       if (XT.typeOf(data) !== "array") { data = [data]; }
2168       var orm = this.fetchOrm(nameSpace, type),
2169         pkey = XT.Orm.primaryKey(orm),
2170         nkey = XT.Orm.naturalKey(orm),
2171         props = orm.properties,
2172         attrPriv = orm.privileges && orm.privileges.attribute ?
2173           orm.privileges.attribute : false,
2174         inclKeys = options.includeKeys,
2175         superUser = options.superUser,
2176         printFormat = options.printFormat,
2177         c,
2178         i,
2179         item,
2180         n,
2181         prop,
2182         itemAttr,
2183         filteredProps,
2184         val,
2185         preOffsetDate,
2186         offsetDate,
2187         check = function (p) {
2188           return p.name === itemAttr;
2189         };
2190
2191       for (var c = 0; c < data.length; c++) {
2192         item = data[c];
2193
2194         /* Remove primary key if applicable */
2195         if (!inclKeys && nkey && nkey !== pkey) { delete item[pkey]; }
2196
2197         for (itemAttr in item) {
2198           if (!item.hasOwnProperty(itemAttr)) {
2199             continue;
2200           }
2201           filteredProps = orm.properties.filter(check);
2202
2203           if (filteredProps.length === 0 && orm.extensions.length > 0) {
2204             /* Try to get the orm prop from an extension if it's not in the core*/
2205             orm.extensions.forEach(function (ext) {
2206               if (filteredProps.length === 0) {
2207                 filteredProps = ext.properties.filter(check);
2208               }
2209             });
2210           }
2211
2212           /* Remove attributes not found in the ORM */
2213           if (filteredProps.length === 0) {
2214             delete item[itemAttr];
2215           } else {
2216             prop = filteredProps[0];
2217           }
2218
2219           /* Remove unprivileged attribute if applicable */
2220           if (!superUser && attrPriv && attrPriv[prop.name] &&
2221             (attrPriv[prop.name].view !== undefined) &&
2222             !this.checkPrivilege(attrPriv[prop.name].view)) {
2223             delete item[prop.name];
2224           }
2225
2226           /*  Format for printing if printFormat and not an object */
2227           if (printFormat && !prop.toOne && !prop.toMany) {
2228             switch(prop.attr.type) {
2229               case "Date":
2230               case "DueDate":
2231                 preOffsetDate = item[itemAttr];
2232                 offsetDate = preOffsetDate &&
2233                   new Date(preOffsetDate.valueOf() + 60000 * preOffsetDate.getTimezoneOffset());
2234                 item[itemAttr] = XT.formatDate(offsetDate).formatdate;
2235               break;
2236               case "Cost":
2237                 item[itemAttr] = XT.formatCost(item[itemAttr]).formatcost.toString();
2238               break;
2239               case "Number":
2240                 item[itemAttr] = XT.formatNumeric(item[itemAttr], "").formatnumeric.toString();
2241               break;
2242               case "Money":
2243                 item[itemAttr] = XT.formatMoney(item[itemAttr]).formatmoney.toString();
2244               break;
2245               case "SalesPrice":
2246                 item[itemAttr] = XT.formatSalesPrice(item[itemAttr]).formatsalesprice.toString();
2247               break;
2248               case "PurchasePrice":
2249                 item[itemAttr] = XT.formatPurchPrice(item[itemAttr]).formatpurchprice.toString();
2250               break;
2251               case "ExtendedPrice":
2252                 item[itemAttr] = XT.formatExtPrice(item[itemAttr]).formatextprice.toString();
2253               break;
2254               case "Quantity":
2255                 item[itemAttr] = XT.formatQty(item[itemAttr]).formatqty.toString();
2256               break;
2257               case "QuantityPer":
2258                 item[itemAttr] = XT.formatQtyPer(item[itemAttr]).formatqtyper.toString();
2259               break;
2260               case "UnitRatioScale":
2261                 item[itemAttr] = XT.formatRatio(item[itemAttr]).formatratio.toString();
2262               break;
2263               case "Percent":
2264                 item[itemAttr] = XT.formatPrcnt(item[itemAttr]).formatprcnt.toString();
2265               break;
2266               case "WeightScale":
2267                 item[itemAttr] = XT.formatWeight(item[itemAttr]).formatweight.toString();
2268               break;
2269               default:
2270                 item[itemAttr] = (item[itemAttr] || "").toString();
2271             }
2272           }
2273
2274           /* Handle composite types */
2275           if (prop.toOne && prop.toOne.isNested && item[prop.name]) {
2276             this.sanitize(nameSpace, prop.toOne.type, item[prop.name], options);
2277           } else if (prop.toMany && prop.toMany.isNested && item[prop.name]) {
2278             for (var n = 0; n < item[prop.name].length; n++) {
2279               val = item[prop.name][n];
2280
2281               /* Remove foreign key if applicable */
2282               if (!inclKeys) { delete val[prop.toMany.inverse]; }
2283               this.sanitize(nameSpace, prop.toMany.type, val, options);
2284             }
2285           }
2286         }
2287       }
2288     },
2289
2290     /**
2291      * Returns a array of key value pairs of metric settings that correspond with an array of passed keys.
2292      *
2293      * @param {Array} array of metric names
2294      * @returns {Array}
2295      */
2296     retrieveMetrics: function (keys) {
2297       var literals = [],
2298         prop,
2299         qry,
2300         ret = {},
2301         sql = 'select metric_name as setting, metric_value as value '
2302             + 'from metric '
2303             + 'where metric_name in ({literals})';
2304
2305       for (var i = 0; i < keys.length; i++) {
2306         literals[i] = "%" + (i + 1) + "$L";
2307       }
2308
2309       sql = sql.replace(/{literals}/, literals.join(','));
2310       sql = XT.format(sql, keys)
2311
2312       if (DEBUG) {
2313         XT.debug('retrieveMetrics sql = ', sql);
2314       }
2315       qry = plv8.execute(sql);
2316
2317       /* Recast where applicable. */
2318       for (var i = 0; i < qry.length; i++) {
2319         prop = qry[i].setting;
2320         if(qry[i].value === 't') { ret[prop] = true; }
2321         else if(qry[i].value === 'f') { ret[prop] = false }
2322         else if(!isNaN(qry[i].value)) { ret[prop] = qry[i].value - 0; }
2323         else { ret[prop] = qry[i].value; }
2324       }
2325
2326       /* Make sure there is a result at all times */
2327       keys.forEach(function (key) {
2328         if (ret[key] === undefined) { ret[key] = null; }
2329       });
2330
2331       return ret;
2332     },
2333
2334     /**
2335      * Creates and returns a lock for a given table. Defaults to a time based lock of 30 seconds
2336      * unless aternate timeout option or process id (pid) is passed. If a pid is passed, the lock
2337      * is considered infinite as long as the pid is valid. If a previous lock key is passed and it is
2338      * valid, a new lock will be granted.
2339      *
2340      * @param {String | Number} Table name or oid
2341      * @param {Number} Record id
2342      * @param {Object} Options
2343      * @param {Number} [options.timeout=30]
2344      * @param {Number} [options.pid] Process id
2345      * @param {Number} [options.key] Key
2346      * @param {Boolean} [options.obtainLock=true] If false, only checks for existing lock
2347      */
2348     tryLock: function (table, id, options) {
2349       options = options ? options : {};
2350
2351       var deleteSql = "delete from xt.lock where lock_id = $1;",
2352         timeout = options.timeout || 30,
2353         expires = new Date(),
2354         i,
2355         insertSqlExp = "insert into xt.lock (lock_table_oid, lock_record_id, lock_username, lock_expires) " +
2356                        "values ($1, $2, $3, $4) returning lock_id, lock_effective;",
2357         insertSqlPid = "insert into xt.lock (lock_table_oid, lock_record_id, lock_username, lock_pid) " +
2358                        "values ($1, $2, $3, $4) returning lock_id, lock_effective;",
2359         lock,
2360         lockExp,
2361         oid,
2362         pcheck,
2363         pid = options.pid || null,
2364         pidSql = "select usename, procpid " +
2365                  "from pg_stat_activity " +
2366                  "where datname=current_database() " +
2367                  " and usename=$1 " +
2368                  " and procpid=$2;",
2369         query,
2370         selectSql = "select * " +
2371                     "from xt.lock " +
2372                     "where lock_table_oid = $1 " +
2373                     " and lock_record_id = $2;",
2374         username = XT.username;
2375
2376       /* If passed a table name, look up the oid. */
2377       oid = typeof table === "string" ? this.getTableOid(table) : table;
2378
2379       if (DEBUG) XT.debug("Trying lock table", [oid, id]);
2380
2381       /* See if there are existing lock(s) for this record. */
2382       if (DEBUG) {
2383         XT.debug('tryLock sql = ', selectSql);
2384         XT.debug('tryLock values = ', [oid, id]);
2385       }
2386       query = plv8.execute(selectSql, [oid, id]);
2387
2388       /* Validate result */
2389       if (query.length > 0) {
2390         while (query.length) {
2391           lock = query.shift();
2392
2393           /* See if we are confirming our own lock. */
2394           if (options.key && options.key === lock.lock_id) {
2395             /* Go on and we'll get a new lock. */
2396
2397           /* Make sure if they are pid locks users is still connected. */
2398           } else if (lock.lock_pid) {
2399             if (DEBUG) {
2400               XT.debug('tryLock sql = ', pidSql);
2401               XT.debug('tryLock values = ', [lock.lock_username, lock.lock_pid]);
2402             }
2403             pcheck = plv8.execute(pidSql, [lock.lock_username, lock.lock_pid]);
2404             if (pcheck.length) { break; } /* valid lock */
2405           } else {
2406             lockExp = new Date(lock.lock_expires);
2407             if (DEBUG) { XT.debug("Lock found", [lockExp > expires, lockExp, expires]); }
2408             if (lockExp > expires) { break; } /* valid lock */
2409           }
2410
2411           /* Delete invalid or expired lock. */
2412           if (DEBUG) {
2413             XT.debug('tryLock sql = ', deleteSql);
2414             XT.debug('tryLock values = ', [lock.lock_id]);
2415           }
2416           plv8.execute(deleteSql, [lock.lock_id]);
2417           lock = undefined;
2418         }
2419
2420         if (lock) {
2421           if (DEBUG) XT.debug("Lock found", lock.lock_username);
2422
2423           return {
2424             username: lock.lock_username,
2425             effective: lock.lock_effective
2426           }
2427         }
2428       }
2429
2430       if (options.obtainLock === false) { return; }
2431
2432       if (DEBUG) { XT.debug("Creating lock."); }
2433       if (DEBUG) { XT.debug('tryLock sql = ', insertSqlPid); }
2434
2435       if (pid) {
2436         if (DEBUG) { XT.debug('tryLock values = ', [oid, id, username, pid]); }
2437         lock = plv8.execute(insertSqlPid, [oid, id, username, pid])[0];
2438       } else {
2439         expires = new Date(expires.setSeconds(expires.getSeconds() + timeout));
2440         if (DEBUG) { XT.debug('tryLock values = ', [oid, id, username, expires]); }
2441         lock = plv8.execute(insertSqlExp, [oid, id, username, expires])[0];
2442       }
2443
2444       if (DEBUG) { XT.debug("Lock returned is", lock.lock_id); }
2445
2446       return {
2447         username: username,
2448         effective: lock.lock_effective,
2449         key: lock.lock_id
2450       }
2451     },
2452
2453     /**
2454      * Release a lock. Pass either options with a key, or table, id and username.
2455      *
2456      * @param {Object} Options: key or table and id
2457      */
2458     releaseLock: function (options) {
2459       var oid,
2460         sqlKey = 'delete from xt.lock where lock_id = $1;',
2461         sqlUsr = 'delete from xt.lock where lock_table_oid = $1 and lock_record_id = $2 and lock_username = $3;',
2462         username = XT.username;
2463
2464       if (options.key) {
2465         if (DEBUG) {
2466           XT.debug('releaseLock sql = ', sqlKey);
2467           XT.debug('releaseLock values = ', [options.key]);
2468         }
2469         plv8.execute(sqlKey, [options.key]);
2470       } else {
2471         oid = typeof options.table === "string" ? this.getTableOid(options.table) : options.table;
2472
2473         if (DEBUG) {
2474           XT.debug('releaseLock sql = ', sqlUsr);
2475           XT.debug('releaseLock values = ', [oid, options.id, username]);
2476         }
2477         plv8.execute(sqlUsr, [oid, options.id, username]);
2478       }
2479
2480       return true;
2481     },
2482
2483     /**
2484      * Renew a lock. Defaults to rewing the lock for 30 seconds.
2485      *
2486      * @param {Number} Key
2487      * @params {Object} Options: timeout
2488      * @returns {Date} New expiration or false.
2489      */
2490     renewLock: function (key, options) {
2491       var expires = new Date(),
2492         query,
2493         selectSql = "select * from xt.lock where lock_id = $1;",
2494         timeout = options && options.timeout ? options.timeout : 30,
2495         updateSql = "update xt.lock set lock_expires = $1 where lock_id = $2;";
2496
2497       if (typeof key !== "number") { return false; }
2498       expires = new Date(expires.setSeconds(expires.getSeconds() + timeout));
2499
2500       if (DEBUG) {
2501         XT.debug('renewLock sql = ', selectSql);
2502         XT.debug('renewLock values = ', [key]);
2503       }
2504       query = plv8.execute(selectSql, [key]);
2505
2506       if (query.length) {
2507         if (DEBUG) {
2508           XT.debug('renewLock sql = ', updateSql);
2509           XT.debug('renewLock values = ', [expires, key]);
2510         }
2511         plv8.execute(updateSql, [expires, key]);
2512
2513         return true;
2514       }
2515
2516       return false;
2517     }
2518   }
2519
2520 }());
2521
2522 $$ );