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