if (customer) {
this.$.customerShiptoWidget.setDisabled(false);
this.$.customerShiptoWidget.addParameter({
- attribute: "customer",
+ attribute: "customer.number",
value: customer.id
});
if (this.$.creditCardWidget) {
"xt/functions/add_primary_key.sql",
"xt/functions/any_numeric.sql",
"xt/functions/any_text.sql",
+ "xt/functions/any_uuid.sql",
"xt/functions/begins_with.sql",
"xt/functions/commit_record.sql",
"xt/functions/create_table.sql",
"xt/trigger_functions/record_did_change.sql",
"xt/operators/any_numeric.sql",
"xt/operators/any_text.sql",
+ "xt/operators/any_uuid.sql",
"xt/operators/begins_with.sql",
"xt/operators/ends_with.sql",
"xt/operators/not_any_numeric.sql",
return result ? result.id : 0;
};
+ /**
+ Returns a complex query's results.
+ Sample usage:
+ select xt.post('{
+ "username": "admin",
+ "nameSpace": "XM",
+ "type": "Model",
+ "dispatch":{
+ "functionName":"query",
+ "parameters":[
+ "Address", {"query": {"parameters": [{"attribute": "city","operator": "=","value": "Norfolk"}]}}
+ ]
+ }
+ }');
+
+ @param {String} recordType to query
+ @param {Object} options: query
+ @returns Object
+ */
+ XM.Model.query = function (recordType, options) {
+ options = options || {};
+ var query = {};
+
+ if (recordType && options && options.query) {
+ query.username = XT.username;
+ query.nameSpace = 'XM';
+ query.type = recordType;
+ query.query = options.query;
+ }
+
+ result = XT.Rest.get(query);
+
+ return result;
+ };
+ XM.Model.query.scope = "Model";
+ XM.Model.query.description = "Perform an complex query on a resource. This allows you to use a POST body for the query vs. a long URL.";
+ XM.Model.query.request = {
+ "$ref": "Query"
+ };
+ XM.Model.query.parameterOrder = ["recordType", "options"];
+ // For JSON-Schema deff, see:
+ // https://github.com/fge/json-schema-validator/issues/46#issuecomment-14681103
+ XM.Model.query.schema = {
+ Query: {
+ properties: {
+ attributes: {
+ title: "Service request attributes",
+ description: "An array of attributes needed to perform a complex query.",
+ type: "array",
+ items: [
+ {
+ title: "Resource",
+ description: "The resource to query.",
+ type: "string",
+ required: true
+ },
+ {
+ title: "Options",
+ type: "object",
+ "$ref": "QueryOptions"
+ }
+ ],
+ "minItems": 2,
+ "maxItems": 2,
+ required: true
+ }
+ }
+ },
+ QueryOptions: {
+ properties: {
+ query: {
+ title: "query",
+ description: "The query to perform.",
+ type: "object",
+ "$ref": "QueryOptionsQuery"
+ }
+ }
+ },
+ QueryOptionsQuery: {
+ properties: {
+ parameters: {
+ title: "Parameters",
+ description: "The query parameters.",
+ type: "array",
+ items: [
+ {
+ title: "Attribute",
+ type: "object",
+ "$ref": "QueryOptionsParameters"
+ }
+ ],
+ "minItems": 1
+ },
+ orderBy: {
+ title: "Order By",
+ description: "The query order by.",
+ type: "array",
+ items: [
+ {
+ title: "Attribute",
+ type: "object",
+ "$ref": "QueryOptionsOrderBy"
+ }
+ ]
+ },
+ rowlimit: {
+ title: "Row Limit",
+ description: "The query for paged results.",
+ type: "integer"
+ },
+ maxresults: {
+ title: "Max Results",
+ description: "The query limit for total results.",
+ type: "integer"
+ },
+ pagetoken: {
+ title: "Page Token",
+ description: "The query offset page token.",
+ type: "integer"
+ },
+ count: {
+ title: "Count",
+ description: "Set to true to return only the count of results for this query.",
+ type: "boolean"
+ }
+ }
+ },
+ QueryOptionsParameters: {
+ properties: {
+ attribute: {
+ title: "Attribute",
+ description: "The column name used to construct the query's WHERE clasues.",
+ type: "string",
+ required: true
+ },
+ operator: {
+ title: "Operator",
+ description: "The operator used to construct the query's WHERE clasues.",
+ type: "string"
+ },
+ value: {
+ title: "Value",
+ description: "The value used to construct the query's WHERE clasues.",
+ required: true
+ }
+ }
+ },
+ QueryOptionsOrderBy: {
+ properties: {
+ attribute: {
+ title: "Attribute",
+ description: "The column name used to construct the query's ORDER BY.",
+ type: "string",
+ required: true
+ },
+ descending: {
+ title: "Direction",
+ description: "Set to true so the query's ORDER BY will be DESC.",
+ type: "boolean"
+ }
+ }
+ }
+ };
+
+ /**
+ Returns a complex query's results using the REST query structure. This is a
+ wrapper for XM.Model.query that reformats the query structure from a
+ rest_query to our XT.Rest structure. This dispatch function can be used by
+ a REST API client to query a resource when the query would be too long to
+ pass to the API as a GET URL query.
+
+ Sample usage:
+ select xt.post('{
+ "username": "admin",
+ "nameSpace": "XM",
+ "type": "Model",
+ "dispatch":{
+ "functionName":"restQuery",
+ "parameters":["Address", {"query": [{"city":{"EQUALS":"Norfolk"}}], "orderby": [{"ASC": "line1"}, {"DESC": "line2"}]}]
+ }
+ }');
+
+ @param {String} recordType to query
+ @param {Object} options: query
+ @returns Object
+ */
+ XM.Model.restQuery = function (recordType, options) {
+ options = options || {};
+ var order = {},
+ param = {},
+ query = {},
+ mapOperator = function (op) {
+ var operators = {
+ value: {
+ ANY: 'ANY',
+ NOT_ANY: 'NOT ANY',
+ EQUALS: '=',
+ NOT_EQUALS: '!=',
+ LESS_THAN: '<',
+ AT_MOST: '<=',
+ GREATER_THAN: '>',
+ AT_LEAST: '>=',
+ MATCHES: 'MATCHES',
+ BEGINS_WITH: 'BEGINS_WITH'
+ }
+ };
+
+ return operators.value[op];
+ };
+
+ /* Convert from rest_query to XM.Model.query structure. */
+ if (recordType && options) {
+ if (options.query) {
+ query.parameters = [];
+ for (var i = 0; i < options.query.length; i++) {
+ for (var column in options.query[i]) {
+ for (var op in options.query[i][column]) {
+ param = {};
+ param.attribute = column;
+ param.operator = mapOperator(op);
+ param.value = options.query[i][column][op];
+ query.parameters.push(param);
+ }
+ }
+ }
+ }
+ if (options.orderby || options.orderBy) {
+ options.orderBy = options.orderby || options.orderBy;
+ query.orderBy = [];
+ for (var o = 0; o < options.orderBy.length; o++) {
+ for (var column in options.orderBy[o]) {
+ order = {};
+ order.attribute = options.orderBy[o][column];
+ if (column === 'DESC') {
+ order.descending = true;
+ }
+ query.orderBy.push(order);
+ }
+ }
+ }
+ if (options.rowlimit || options.rowLimit) {
+ options.rowLimit = options.rowlimit || options.rowLimit;
+ query.rowLimit = options.rowLimit;
+ }
+ if (options.maxresults || options.maxResults) {
+ options.maxResults = options.maxresults || options.maxResults;
+ query.rowLimit = options.maxResults;
+ }
+ if (options.pagetoken || options.pageToken) {
+ options.pageToken = options.pagetoken || options.pageToken;
+ if (query.rowLimit) {
+ query.rowOffset = (options.pageToken || 0) * (query.rowLimit);
+ } else {
+ query.rowOffset = (options.pageToken || 0);
+ }
+ }
+ if (options.count) {
+ query.count = options.count;
+ }
+ }
+
+ result = XM.Model.query(recordType, {"query": query});
+
+ return result;
+ };
+ XM.Model.restQuery.description = "Perform an complex query on a resource using the REST query structure. This allows you to use a POST body for the query vs. a long URL.";
+ XM.Model.restQuery.request = {
+ "$ref": "RestQuery"
+ };
+ XM.Model.restQuery.parameterOrder = ["recordType", "options"];
+ // For JSON-Schema deff, see:
+ // https://github.com/fge/json-schema-validator/issues/46#issuecomment-14681103
+ XM.Model.restQuery.schema = {
+ RestQuery: {
+ properties: {
+ attributes: {
+ title: "Service request attributes",
+ description: "An array of attributes needed to perform a complex query.",
+ type: "array",
+ items: [
+ {
+ title: "Resource",
+ description: "The resource to query.",
+ type: "string",
+ required: true
+ },
+ {
+ title: "Options",
+ type: "object",
+ "$ref": "RestQueryOptions"
+ }
+ ],
+ "minItems": 2,
+ "maxItems": 2,
+ required: true
+ }
+ }
+ },
+ RestQueryOptions: {
+ properties: {
+ query: {
+ title: "query",
+ description: "The query to perform.",
+ type: "array",
+ items: [
+ {
+ title: "column",
+ type: "object"
+ }
+ ],
+ "minItems": 1
+ },
+ orderby: {
+ title: "Order By",
+ description: "The query order by.",
+ type: "array",
+ items: [
+ {
+ title: "column",
+ type: "object"
+ }
+ ]
+ },
+ rowlimit: {
+ title: "Row Limit",
+ description: "The query for paged results.",
+ type: "integer"
+ },
+ maxresults: {
+ title: "Max Results",
+ description: "The query limit for total results.",
+ type: "integer"
+ },
+ pagetoken: {
+ title: "Page Token",
+ description: "The query offset page token.",
+ type: "integer"
+ },
+ count: {
+ title: "Count",
+ description: "Set to true to return only the count of results for this query.",
+ type: "boolean"
+ }
+ }
+ }
+ };
+
/**
Used to determine whether a model is used or not.
attr,
seq,
tableName,
- fkIndex,
- fkey,
- propIndex,
+ fkIndex,
+ fkey,
+ propIndex,
probObj;
if (nkey) {
if (DEBUG) { XT.debug('XM.Model.used keys length:', fkeys.length) }
if (DEBUG) { XT.debug('XM.Model.used keys:', JSON.stringify(fkeys)) }
-
+
for (i = 0; i < fkeys.length; i++) {
/* Validate */
XM.Model.used = function(recordType, id) {
return XM.PrivateModel.used(recordType, id);
};
-
+
$$ );
--- /dev/null
+create or replace function xt.any_uuid(arg1 uuid, arg2 uuid[]) returns boolean immutable as $$
+ select array[$1] <@ $2;
+$$ language 'sql';
* @param {Array} Parameters - optional
* @returns {Object}
*/
- buildClause: function (nameSpace, type, parameters, orderBy) {
+ buildClauseOptimized: function (nameSpace, type, parameters, orderBy) {
parameters = parameters || [];
- var arrayIdentifiers = [],
+ var that = this,
+ arrayIdentifiers = [],
arrayParams,
charSql,
childOrm,
clauses = [],
count = 1,
+ fromKeyProp,
+ groupByColumnParams = [],
identifiers = [],
- list = [],
+ joinIdentifiers = [],
+ orderByList = [],
+ orderByColumnList = [],
isArray = false,
op,
orClause,
orderByIdentifiers = [],
+ orderByColumnIdentifiers = [],
orderByParams = [],
+ orderByColumnParams = [],
+ joins = [],
orm = this.fetchOrm(nameSpace, type),
param,
params = [],
parts,
pcount,
+ pertinentExtension,
+ pgType,
prevOrm,
privileges = orm.privileges,
prop,
+ sourceTableAlias,
ret = {};
ret.conditions = "";
});
}
+ /* Support the short cut wherein the client asks for a filter on a toOne with a
+ string. Technically they should use "theAttr.theAttrNaturalKey", but if they
+ don't, massage the inputs as if they did */
+ parameters.map(function (parameter) {
+ var attributeIsString = typeof parameter.attribute === 'string';
+ attributes = attributeIsString ? [parameter.attribute] : parameter.attribute;
+
+ attributes.map(function (attribute) {
+ var prop = XT.Orm.getProperty(orm, attribute),
+ propName = prop.name,
+ childOrm,
+ naturalKey,
+ index;
+
+ if ((prop.toOne || prop.toMany) && attribute.indexOf('.') < 0) {
+ /* Someone is querying on a toOne without using a path */
+ /* TODO: even if there's a path x.y, it's possible that it's still not
+ correct because the correct path maybe is x.y.naturalKeyOfY */
+ if (prop.toOne && prop.toOne.type) {
+ childOrm = that.fetchOrm(nameSpace, prop.toOne.type);
+ } else if (prop.toMany && prop.toMany.type) {
+ childOrm = that.fetchOrm(nameSpace, prop.toMany.type);
+ } else {
+ plv8.elog(ERROR, "toOne or toMany property is missing it's 'type': " + prop.name);
+ }
+ naturalKey = XT.Orm.naturalKey(childOrm);
+ if (attributeIsString) {
+ /* add the natural key to the end of the requested attribute */
+ parameter.attribute = attribute + "." + naturalKey;
+ } else {
+ /* swap out the attribute in the array for the one with the prepended natural key */
+ index = parameter.attribute.indexOf(attribute);
+ parameter.attribute.splice(index, 1);
+ parameter.attribute.push(attribute + "." + naturalKey);
+ }
+ }
+ });
+ });
+
/* Handle parameters. */
if (parameters.length) {
for (var i = 0; i < parameters.length; i++) {
prop = XT.Orm.getProperty(orm, 'characteristics');
/* Build the characteristics query clause. */
+ identifiers.push(XT.Orm.primaryKey(orm, true));
identifiers.push(prop.toMany.inverse);
identifiers.push(orm.nameSpace.toLowerCase());
identifiers.push(prop.toMany.type.decamelize());
identifiers.push(param.attribute);
identifiers.push(param.value);
- charSql = 'id in (' +
+ charSql = '%' + (identifiers.length - 5) + '$I in (' +
' select %' + (identifiers.length - 4) + '$I '+
' from %' + (identifiers.length - 3) + '$I.%' + (identifiers.length - 2) + '$I ' +
' join char on (char_name = characteristic)' +
plv8.elog(ERROR, 'Attribute not found in object map: ' + parts[n]);
}
- /* Build path. e.g. ((%1$I).%2$I).%3$I */
- identifiers.push(parts[n]);
- params[pcount] += "%" + identifiers.length + "$I";
- if (n < parts.length - 1) {
- params[pcount] = "(" + params[pcount] + ").";
- childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
+ /* Build path. */
+ if (n === parts.length - 1) {
+ identifiers.push("jt" + (joins.length - 1));
+ identifiers.push(prop.attr.column);
+ pgType = this.getPgTypeFromOrmType(
+ this.getNamespaceFromNamespacedTable(childOrm.table),
+ this.getTableFromNamespacedTable(childOrm.table),
+ prop.attr.column
+ );
+ pgType = pgType ? "::" + pgType + "[]" : '';
+ params[pcount] += "%" + (identifiers.length - 1) + "$I.%" + identifiers.length + "$I";
+ params[pcount] += ' ' + op + ' ARRAY[' + param.value.join(',') + ']' + pgType;
} else {
- params[pcount] += op + ' ARRAY[' + param.value.join(',') + ']';
+ childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
+ sourceTableAlias = n === 0 ? "t1" : "jt" + joins.length - 1;
+ joinIdentifiers.push(
+ this.getNamespaceFromNamespacedTable(childOrm.table),
+ this.getTableFromNamespacedTable(childOrm.table),
+ sourceTableAlias, prop.toOne.column,
+ XT.Orm.primaryKey(childOrm, true));
+ joins.push("left join %" + (joinIdentifiers.length - 4) + "$I.%" + (joinIdentifiers.length - 3)
+ + "$I jt" + joins.length + " on %"
+ + (joinIdentifiers.length - 2) + "$I.%"
+ + (joinIdentifiers.length - 1) + "$I = jt" + joins.length + ".%" + joinIdentifiers.length + "$I");
}
}
} else {
- identifiers.push(param.attribute);
- params.push("%" + identifiers.length + "$I " + op + ' ARRAY[' + param.value.join(',') + ']');
+ prop = XT.Orm.getProperty(orm, param.attribute);
+ if (!prop) {
+ plv8.elog(ERROR, 'Attribute not found in object map: ' + param.attribute[c]);
+ }
+ identifiers.push("t1");
+ identifiers.push(prop.attr.column);
+ pgType = this.getPgTypeFromOrmType(
+ this.getNamespaceFromNamespacedTable(orm.table),
+ this.getTableFromNamespacedTable(orm.table),
+ prop.attr.column
+ );
+ pgType = pgType ? "::" + pgType + "[]" : '';
+ params.push("%" + (identifiers.length - 1) + "$I.%" + identifiers.length + "$I " + op + ' ARRAY[' + param.value.join(',') + ']' + pgType);
pcount = params.length - 1;
}
clauses.push(params[pcount]);
}
if (m < parts.length - 1) {
- childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
+ if (prop.toOne && prop.toOne.type) {
+ childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
+ } else if (prop.toMany && prop.toMany.type) {
+ childOrm = this.fetchOrm(nameSpace, prop.toMany.type);
+ } else {
+ plv8.elog(ERROR, "toOne or toMany property is missing it's 'type': " + prop.name);
+ }
} else if (prop.attr && prop.attr.type === 'Array') {
/* The last property in the path is an array. */
isArray = true;
/* Do a persional privs array search e.g. 'admin' = ANY (usernames_array). */
if (param.isUsernamePrivFilter && isArray) {
- identifiers.push(parts[n]);
+ identifiers.push(prop.attr.column);
arrayIdentifiers.push(identifiers.length);
if (n < parts.length - 1) {
childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
}
} else {
- /* Build path. e.g. ((%1$I).%2$I).%3$I */
- identifiers.push(parts[n]);
- params[pcount] += "%" + identifiers.length + "$I";
-
- if (n < parts.length - 1) {
- params[pcount] = "(" + params[pcount] + ").";
- childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
- } else if (param.isLower) {
- params[pcount] = "lower(" + params[pcount] + ")";
+ /* Build path, e.g. table_name.column_name */
+ if (n === parts.length - 1) {
+ identifiers.push("jt" + (joins.length - 1));
+ identifiers.push(prop.attr.column);
+ params[pcount] += "%" + (identifiers.length - 1) + "$I.%" + identifiers.length + "$I";
+ if (param.isLower) {
+ params[pcount] = "lower(" + params[pcount] + ")";
+ }
+ } else {
+ sourceTableAlias = n === 0 ? "t1" : "jt" + joins.length - 1;
+ if (prop.toOne && prop.toOne.type) {
+ childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
+ joinIdentifiers.push(
+ this.getNamespaceFromNamespacedTable(childOrm.table),
+ this.getTableFromNamespacedTable(childOrm.table),
+ sourceTableAlias, prop.toOne.column,
+ XT.Orm.primaryKey(childOrm, true)
+ );
+ } else if (prop.toMany && prop.toMany.type) {
+ childOrm = this.fetchOrm(nameSpace, prop.toMany.type);
+ joinIdentifiers.push(
+ this.getNamespaceFromNamespacedTable(childOrm.table),
+ this.getTableFromNamespacedTable(childOrm.table),
+ sourceTableAlias, prop.toMany.column,
+ XT.Orm.primaryKey(childOrm, true)
+ );
+ }
+ joins.push("left join %" + (joinIdentifiers.length - 4) + "$I.%" + (joinIdentifiers.length - 3)
+ + "$I jt" + joins.length + " on %"
+ + (joinIdentifiers.length - 2) + "$I.%"
+ + (joinIdentifiers.length - 1) + "$I = jt" + joins.length + ".%" + joinIdentifiers.length + "$I");
}
}
}
} else {
/* Validate attribute. */
prop = XT.Orm.getProperty(orm, param.attribute[c]);
+ pertinentExtension = XT.Orm.getProperty(orm, param.attribute[c], true);
+ if(pertinentExtension.isChild) {
+ /* We'll need to join this orm extension */
+ fromKeyProp = XT.Orm.getProperty(orm, pertinentExtension.relations[0].inverse);
+ joinIdentifiers.push(
+ this.getNamespaceFromNamespacedTable(pertinentExtension.table),
+ this.getTableFromNamespacedTable(pertinentExtension.table),
+ fromKeyProp.attr.column,
+ pertinentExtension.relations[0].column);
+ joins.push("left join %" + (joinIdentifiers.length - 3) + "$I.%" + (joinIdentifiers.length - 2)
+ + "$I jt" + joins.length + " on t1.%"
+ + (joinIdentifiers.length - 1) + "$I = jt" + joins.length + ".%" + joinIdentifiers.length + "$I");
+ }
if (!prop) {
plv8.elog(ERROR, 'Attribute not found in object map: ' + param.attribute[c]);
}
- identifiers.push(param.attribute[c]);
+ identifiers.push(pertinentExtension.isChild ? "jt" + (joins.length - 1) : "t1");
+ identifiers.push(prop.attr.column);
/* Do a persional privs array search e.g. 'admin' = ANY (usernames_array). */
if (param.isUsernamePrivFilter && ((prop.toMany && !prop.isNested) ||
pcount = params.length - 1;
arrayIdentifiers.push(identifiers.length);
} else {
- params.push("%" + identifiers.length + "$I");
+ params.push("%" + (identifiers.length - 1) + "$I.%" + identifiers.length + "$I");
pcount = params.length - 1;
}
}
if (param.isUsernamePrivFilter && ((prop.toMany && !prop.isNested)
|| (prop.attr && prop.attr.type === 'Array') || isArray)) {
+ /* XXX: this bit of code has not been touched by the optimization refactor */
/* e.g. 'admin' = ANY (usernames_array) */
arrayParams = "";
params[pcount] += ' ' + op + ' ANY (';
ret.conditions = (clauses.length ? '(' + XT.format(clauses.join(' and '), identifiers) + ')' : ret.conditions) || true;
- /* Massage ordeBy with quoted identifiers. */
+ /* Massage orderBy with quoted identifiers. */
+ /* We need to support the xm case for sql2 and the xt/public (column) optimized case for sql1 */
+ /* In practice we build the two lists independently of one another */
if (orderBy) {
for (var i = 0; i < orderBy.length; i++) {
/* Handle path case. */
parts = orderBy[i].attribute.split('.');
prevOrm = orm;
orderByParams.push("");
+ orderByColumnParams.push("");
+ groupByColumnParams.push("");
pcount = orderByParams.length - 1;
for (var n = 0; n < parts.length; n++) {
orderByIdentifiers.push(parts[n]);
orderByParams[pcount] += "%" + orderByIdentifiers.length + "$I";
- if (n < parts.length - 1) {
+ if (n === parts.length - 1) {
+ orderByColumnIdentifiers.push("jt" + (joins.length - 1));
+ orderByColumnIdentifiers.push(prop.attr.column);
+ orderByColumnParams[pcount] += "%" + (orderByColumnIdentifiers.length - 1) + "$I.%" + orderByColumnIdentifiers.length + "$I"
+ groupByColumnParams[pcount] += "%" + (orderByColumnIdentifiers.length - 1) + "$I.%" + orderByColumnIdentifiers.length + "$I"
+ } else {
orderByParams[pcount] = "(" + orderByParams[pcount] + ").";
orm = this.fetchOrm(nameSpace, prop.toOne.type);
+ sourceTableAlias = n === 0 ? "t1" : "jt" + joins.length - 1;
+ joinIdentifiers.push(
+ this.getNamespaceFromNamespacedTable(orm.table),
+ this.getTableFromNamespacedTable(orm.table),
+ sourceTableAlias, prop.toOne.column,
+ XT.Orm.primaryKey(orm, true));
+ joins.push("left join %" + (joinIdentifiers.length - 4) + "$I.%" + (joinIdentifiers.length - 3)
+ + "$I jt" + joins.length + " on %"
+ + (joinIdentifiers.length - 2) + "$I.%"
+ + (joinIdentifiers.length - 1) + "$I = jt" + joins.length + ".%" + joinIdentifiers.length + "$I");
}
}
orm = prevOrm;
plv8.elog(ERROR, 'Attribute not found in map: ' + orderBy[i].attribute);
}
orderByIdentifiers.push(orderBy[i].attribute);
+ orderByColumnIdentifiers.push("t1");
+ orderByColumnIdentifiers.push(prop.attr.column);
orderByParams.push("%" + orderByIdentifiers.length + "$I");
+ orderByColumnParams.push("%" + (orderByColumnIdentifiers.length - 1) + "$I.%" + orderByColumnIdentifiers.length + "$I");
+ groupByColumnParams.push("%" + (orderByColumnIdentifiers.length - 1) + "$I.%" + orderByColumnIdentifiers.length + "$I");
pcount = orderByParams.length - 1;
}
if (orderBy[i].isEmpty) {
orderByParams[pcount] = "length(" + orderByParams[pcount] + ")=0";
+ orderByColumnParams[pcount] = "length(" + orderByColumnParams[pcount] + ")=0";
}
if (orderBy[i].descending) {
orderByParams[pcount] += " desc";
+ orderByColumnParams[pcount] += " desc";
}
- list.push(orderByParams[pcount])
+ orderByList.push(orderByParams[pcount])
+ orderByColumnList.push(orderByColumnParams[pcount])
}
}
- ret.orderBy = list.length ? XT.format('order by ' + list.join(','), orderByIdentifiers) : '';
+ ret.orderBy = orderByList.length ? XT.format('order by ' + orderByList.join(','), orderByIdentifiers) : '';
+ ret.orderByColumns = orderByColumnList.length ? XT.format('order by ' + orderByColumnList.join(','), orderByColumnIdentifiers) : '';
+ ret.groupByColumns = groupByColumnParams.length ? XT.format(', ' + groupByColumnParams.join(','), orderByColumnIdentifiers) : '';
+ ret.joins = joins.length ? XT.format(joins.join(' '), joinIdentifiers) : '';
return ret;
},
* @returns {Number}
*/
getTableOid: function (table) {
- var name = table.toLowerCase(), /* be generous */
- namespace = "public", /* default assumed if no dot in name */
+ var tableName = this.getTableFromNamespacedTable(table).toLowerCase(), /* be generous */
+ namespace = this.getNamespaceFromNamespacedTable(table),
ret,
sql = "select pg_class.oid::integer as oid " +
"from pg_class join pg_namespace on relnamespace = pg_namespace.oid " +
"where relname = $1 and nspname = $2";
- if (table.indexOf(".") > 0) {
- namespace = table.beforeDot();
- table = table.afterDot();
- }
-
if (DEBUG) {
XT.debug('getTableOid sql =', sql);
- XT.debug('getTableOid values =', [table, namespace]);
+ XT.debug('getTableOid values =', [tableName, namespace]);
}
- ret = plv8.execute(sql, [table, namespace])[0].oid - 0;
+ ret = plv8.execute(sql, [tableName, namespace])[0].oid - 0;
// TODO - Handle not found error.
}
},
+ getNamespaceFromNamespacedTable: function (fullName) {
+ return fullName.indexOf(".") > 0 ? fullName.beforeDot() : "public";
+ },
+
+ getTableFromNamespacedTable: function (fullName) {
+ return fullName.indexOf(".") > 0 ? fullName.afterDot() : fullName;
+ },
+
+ getPgTypeFromOrmType: function (schema, table, column) {
+ var sql = "select data_type from information_schema.columns " +
+ "where true " +
+ "and table_schema = $1 " +
+ "and table_name = $2 " +
+ "and column_name = $3;",
+ pgType,
+ values = [schema, table, column];
+
+ if (DEBUG) {
+ XT.debug('getPgTypeFromOrmType sql =', sql);
+ XT.debug('getPgTypeFromOrmType values =', values);
+ }
+
+ pgType = plv8.execute(sql, values);
+ pgType = pgType ? pgType[0].data_type : false;
+
+ return pgType;
+ },
+
/**
* Get the natural key id for an object based on a passed in primary key.
*
encryptionKey = options.encryptionKey,
orderBy = query.orderBy,
orm = this.fetchOrm(nameSpace, type),
+ table,
+ tableNamespace,
parameters = query.parameters,
- clause = this.buildClause(nameSpace, type, parameters, orderBy),
+ clause = this.buildClauseOptimized(nameSpace, type, parameters, orderBy),
i,
pkey = XT.Orm.primaryKey(orm),
+ pkeyColumn = XT.Orm.primaryKey(orm, true),
nkey = XT.Orm.naturalKey(orm),
limit = query.rowLimit ? XT.format('limit %1$L', [query.rowLimit]) : '',
offset = query.rowOffset ? XT.format('offset %1$L', [query.rowOffset]) : '',
sqlCount,
etags,
sql_etags,
- etag_namespace,
- etag_table,
- sql1 = 'select %3$I as id from %1$I.%2$I where {conditions} {orderBy} {limit} {offset};',
+ 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};',
sql2 = 'select * from %1$I.%2$I where %3$I in ({ids}) {orderBy}';
/* Validate - don't bother running the query if the user has no privileges. */
if (!this.checkPrivileges(nameSpace, type)) { return []; }
+ tableNamespace = this.getNamespaceFromNamespacedTable(orm.table);
+ table = this.getTableFromNamespacedTable(orm.table);
+
if (query.count) {
/* Just get the count of rows that match the conditions */
- sqlCount = 'select count(*) as count from %1$I.%2$I where {conditions};';
- sqlCount = XT.format(sqlCount, [nameSpace.decamelize(), type.decamelize()]);
- sqlCount = sqlCount.replace('{conditions}', clause.conditions);
+ sqlCount = 'select count(distinct t1.%3$I) as count from %1$I.%2$I t1 {joins} where {conditions};';
+ sqlCount = XT.format(sqlCount, [tableNamespace.decamelize(), table.decamelize(), pkeyColumn]);
+ sqlCount = sqlCount.replace('{joins}', clause.joins)
+ .replace('{conditions}', clause.conditions);
if (DEBUG) {
XT.debug('fetch sqlCount = ', sqlCount);
}
/* Query the model. */
- sql1 = XT.format(sql1, [nameSpace.decamelize(), type.decamelize(), pkey]);
- sql1 = sql1.replace('{conditions}', clause.conditions)
- .replace(/{orderBy}/g, clause.orderBy)
+ sql1 = XT.format(sql1, [tableNamespace.decamelize(), table.decamelize(), pkeyColumn]);
+ sql1 = sql1.replace('{joins}', clause.joins)
+ .replace('{conditions}', clause.conditions)
+ .replace(/{groupBy}/g, clause.groupByColumns)
+ .replace(/{orderBy}/g, clause.orderByColumns)
.replace('{limit}', limit)
.replace('{offset}', offset);
});
if (orm.lockable) {
- if (orm.table.indexOf(".") > 0) {
- etag_namespace = orm.table.beforeDot();
- etag_table = orm.table.afterDot();
- } else {
- etag_namespace = 'public';
- etag_table = orm.table;
- }
-
sql_etags = "select ver_etag as etag, ver_record_id as id " +
"from xt.ver " +
"where ver_table_oid = ( " +
"where nspname = %1$L and relname = %2$L " +
") " +
"and ver_record_id in ({ids})";
- sql_etags = XT.format(sql_etags, [etag_namespace, etag_table]);
+ sql_etags = XT.format(sql_etags, [tableNamespace, table]);
sql_etags = sql_etags.replace('{ids}', idParams.join());
if (DEBUG) {
context.prop = XT.Orm.getProperty(context.map, context.relation);
context.pertinentExtension = XT.Orm.getProperty(context.map, context.relation, true);
context.underlyingTable = context.pertinentExtension.table,
- context.underlyingNameSpace = context.underlyingTable.indexOf(".") > 0 ?
- context.underlyingTable.beforeDot() :
- "public";
- context.underlyingType = context.underlyingTable.indexOf(".") > 0 ?
- context.underlyingTable.afterDot() :
- context.underlyingTable;
+ context.underlyingNameSpace = this.getNamespaceFromNamespacedTable(context.underlyingTable);
+ context.underlyingType = this.getTableFromNamespacedTable(context.underlyingTable);
context.fkey = context.prop.toMany.inverse;
context.fkeyColumn = context.prop.toMany.column;
context.pkey = XT.Orm.naturalKey(context.map) || XT.Orm.primaryKey(context.map);
return true;
},
+ /* This deprecated function is still used by three dispatch functions. We should delete
+ this as soon as we refactor those, and then rename buildClauseOptimized to buildClause */
+ buildClause: function (nameSpace, type, parameters, orderBy) {
+ parameters = parameters || [];
+
+ var arrayIdentifiers = [],
+ arrayParams,
+ charSql,
+ childOrm,
+ clauses = [],
+ count = 1,
+ identifiers = [],
+ list = [],
+ isArray = false,
+ op,
+ orClause,
+ orderByIdentifiers = [],
+ orderByParams = [],
+ orm = this.fetchOrm(nameSpace, type),
+ param,
+ params = [],
+ parts,
+ pcount,
+ prevOrm,
+ privileges = orm.privileges,
+ prop,
+ ret = {};
+
+ ret.conditions = "";
+ ret.parameters = [];
+
+ /* Handle privileges. */
+ if (orm.isNestedOnly) { plv8.elog(ERROR, 'Access Denied'); }
+ if (privileges &&
+ (!privileges.all ||
+ (privileges.all &&
+ (!this.checkPrivilege(privileges.all.read) &&
+ !this.checkPrivilege(privileges.all.update)))
+ ) &&
+ privileges.personal &&
+ (this.checkPrivilege(privileges.personal.read) ||
+ this.checkPrivilege(privileges.personal.update))
+ ) {
+
+ parameters.push({
+ attribute: privileges.personal.properties,
+ isLower: true,
+ isUsernamePrivFilter: true,
+ value: XT.username
+ });
+ }
+
+ /* Handle parameters. */
+ if (parameters.length) {
+ for (var i = 0; i < parameters.length; i++) {
+ orClause = [];
+ param = parameters[i];
+ op = param.operator || '=';
+ switch (op) {
+ case '=':
+ case '>':
+ case '<':
+ case '>=':
+ case '<=':
+ case '!=':
+ break;
+ case 'BEGINS_WITH':
+ op = '~^';
+ break;
+ case 'ENDS_WITH':
+ op = '~?';
+ break;
+ case 'MATCHES':
+ op = '~*';
+ break;
+ case 'ANY':
+ op = '<@';
+ for (var c = 0; c < param.value.length; c++) {
+ ret.parameters.push(param.value[c]);
+ param.value[c] = '$' + count;
+ count++;
+ }
+ break;
+ case 'NOT ANY':
+ op = '!<@';
+ for (var c = 0; c < param.value.length; c++) {
+ ret.parameters.push(param.value[c]);
+ param.value[c] = '$' + count;
+ count++;
+ }
+ break;
+ default:
+ plv8.elog(ERROR, 'Invalid operator: ' + op);
+ }
+
+ /* Handle characteristics. This is very specific to xTuple,
+ and highly dependant on certain table structures and naming conventions,
+ but otherwise way too much work to refactor in an abstract manner right now. */
+ if (param.isCharacteristic) {
+ /* Handle array. */
+ if (op === '<@') {
+ param.value = ' ARRAY[' + param.value.join(',') + ']';
+ }
+
+ /* Booleans are stored as strings. */
+ if (param.value === true) {
+ param.value = 't';
+ } else if (param.value === false) {
+ param.value = 'f';
+ }
+
+ /* Yeah, it depends on a property called 'characteristics'... */
+ prop = XT.Orm.getProperty(orm, 'characteristics');
+
+ /* Build the characteristics query clause. */
+ identifiers.push(prop.toMany.inverse);
+ identifiers.push(orm.nameSpace.toLowerCase());
+ identifiers.push(prop.toMany.type.decamelize());
+ identifiers.push(param.attribute);
+ identifiers.push(param.value);
+
+ charSql = 'id in (' +
+ ' select %' + (identifiers.length - 4) + '$I '+
+ ' from %' + (identifiers.length - 3) + '$I.%' + (identifiers.length - 2) + '$I ' +
+ ' join char on (char_name = characteristic)' +
+ ' where 1=1 ' +
+ /* Note: Not using $i for these. L = literal here. These is not identifiers. */
+ ' and char_name = %' + (identifiers.length - 1) + '$L ' +
+ ' and value ' + op + ' %' + (identifiers.length) + '$L ' +
+ ')';
+
+ clauses.push(charSql);
+
+ /* Array comparisons handle another way. e.g. %1$I !<@ ARRAY[$1,$2] */
+ } else if (op === '<@' || op === '!<@') {
+ /* Handle paths if applicable. */
+ if (param.attribute.indexOf('.') > -1) {
+ parts = param.attribute.split('.');
+ childOrm = this.fetchOrm(nameSpace, type);
+ params.push("");
+ pcount = params.length - 1;
+
+ for (var n = 0; n < parts.length; n++) {
+ /* Validate attribute. */
+ prop = XT.Orm.getProperty(childOrm, parts[n]);
+ if (!prop) {
+ plv8.elog(ERROR, 'Attribute not found in object map: ' + parts[n]);
+ }
+
+ /* Build path. e.g. ((%1$I).%2$I).%3$I */
+ identifiers.push(parts[n]);
+ params[pcount] += "%" + identifiers.length + "$I";
+ if (n < parts.length - 1) {
+ params[pcount] = "(" + params[pcount] + ").";
+ childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
+ } else {
+ params[pcount] += op + ' ARRAY[' + param.value.join(',') + ']';
+ }
+ }
+ } else {
+ identifiers.push(param.attribute);
+ params.push("%" + identifiers.length + "$I " + op + ' ARRAY[' + param.value.join(',') + ']');
+ pcount = params.length - 1;
+ }
+ clauses.push(params[pcount]);
+
+ /* Everything else handle another. */
+ } else {
+ if (XT.typeOf(param.attribute) !== 'array') {
+ param.attribute = [param.attribute];
+ }
+
+ for (var c = 0; c < param.attribute.length; c++) {
+ /* Handle paths if applicable. */
+ if (param.attribute[c].indexOf('.') > -1) {
+ parts = param.attribute[c].split('.');
+ childOrm = this.fetchOrm(nameSpace, type);
+ params.push("");
+ pcount = params.length - 1;
+ isArray = false;
+
+ /* Check if last part is an Array. */
+ for (var m = 0; m < parts.length; m++) {
+ /* Validate attribute. */
+ prop = XT.Orm.getProperty(childOrm, parts[m]);
+ if (!prop) {
+ plv8.elog(ERROR, 'Attribute not found in object map: ' + parts[m]);
+ }
+
+ if (m < parts.length - 1) {
+ childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
+ } else if (prop.attr && prop.attr.type === 'Array') {
+ /* The last property in the path is an array. */
+ isArray = true;
+ params[pcount] = '$' + count;
+ }
+ }
+
+ /* Reset the childOrm to parent. */
+ childOrm = this.fetchOrm(nameSpace, type);
+
+ for (var n = 0; n < parts.length; n++) {
+ /* Validate attribute. */
+ prop = XT.Orm.getProperty(childOrm, parts[n]);
+ if (!prop) {
+ plv8.elog(ERROR, 'Attribute not found in object map: ' + parts[n]);
+ }
+
+ /* Do a persional privs array search e.g. 'admin' = ANY (usernames_array). */
+ if (param.isUsernamePrivFilter && isArray) {
+ identifiers.push(parts[n]);
+ arrayIdentifiers.push(identifiers.length);
+
+ if (n < parts.length - 1) {
+ childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
+ }
+ } else {
+ /* Build path. e.g. ((%1$I).%2$I).%3$I */
+ identifiers.push(parts[n]);
+ params[pcount] += "%" + identifiers.length + "$I";
+
+ if (n < parts.length - 1) {
+ params[pcount] = "(" + params[pcount] + ").";
+ childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
+ } else if (param.isLower) {
+ params[pcount] = "lower(" + params[pcount] + ")";
+ }
+ }
+ }
+ } else {
+ /* Validate attribute. */
+ prop = XT.Orm.getProperty(orm, param.attribute[c]);
+ if (!prop) {
+ plv8.elog(ERROR, 'Attribute not found in object map: ' + param.attribute[c]);
+ }
+
+ identifiers.push(param.attribute[c]);
+
+ /* Do a persional privs array search e.g. 'admin' = ANY (usernames_array). */
+ if (param.isUsernamePrivFilter && ((prop.toMany && !prop.isNested) ||
+ (prop.attr && prop.attr.type === 'Array'))) {
+
+ params.push('$' + count);
+ pcount = params.length - 1;
+ arrayIdentifiers.push(identifiers.length);
+ } else {
+ params.push("%" + identifiers.length + "$I");
+ pcount = params.length - 1;
+ }
+ }
+
+ /* Add persional privs array search. */
+ if (param.isUsernamePrivFilter && ((prop.toMany && !prop.isNested)
+ || (prop.attr && prop.attr.type === 'Array') || isArray)) {
+
+ /* e.g. 'admin' = ANY (usernames_array) */
+ arrayParams = "";
+ params[pcount] += ' ' + op + ' ANY (';
+
+ /* Build path. e.g. ((%1$I).%2$I).%3$I */
+ for (var f =0; f < arrayIdentifiers.length; f++) {
+ arrayParams += '%' + arrayIdentifiers[f] + '$I';
+ if (f < arrayIdentifiers.length - 1) {
+ arrayParams = "(" + arrayParams + ").";
+ }
+ }
+ params[pcount] += arrayParams + ')';
+
+ /* Add optional is null clause. */
+ } else if (parameters[i].includeNull) {
+ /* e.g. %1$I = $1 or %1$I is null */
+ params[pcount] = params[pcount] + " " + op + ' $' + count + ' or ' + params[pcount] + ' is null';
+ } else {
+ /* e.g. %1$I = $1 */
+ params[pcount] += " " + op + ' $' + count;
+ }
+
+ orClause.push(params[pcount]);
+ }
+
+ /* If more than one clause we'll get: (%1$I = $1 or %1$I = $2 or %1$I = $3) */
+ clauses.push('(' + orClause.join(' or ') + ')');
+ count++;
+ ret.parameters.push(param.value);
+ }
+ }
+ }
+
+ ret.conditions = (clauses.length ? '(' + XT.format(clauses.join(' and '), identifiers) + ')' : ret.conditions) || true;
+
+ /* Massage ordeBy with quoted identifiers. */
+ if (orderBy) {
+ for (var i = 0; i < orderBy.length; i++) {
+ /* Handle path case. */
+ if (orderBy[i].attribute.indexOf('.') > -1) {
+ parts = orderBy[i].attribute.split('.');
+ prevOrm = orm;
+ orderByParams.push("");
+ pcount = orderByParams.length - 1;
+
+ for (var n = 0; n < parts.length; n++) {
+ prop = XT.Orm.getProperty(orm, parts[n]);
+ if (!prop) {
+ plv8.elog(ERROR, 'Attribute not found in map: ' + parts[n]);
+ }
+ orderByIdentifiers.push(parts[n]);
+ orderByParams[pcount] += "%" + orderByIdentifiers.length + "$I";
+
+ if (n < parts.length - 1) {
+ orderByParams[pcount] = "(" + orderByParams[pcount] + ").";
+ orm = this.fetchOrm(nameSpace, prop.toOne.type);
+ }
+ }
+ orm = prevOrm;
+ /* Normal case. */
+ } else {
+ prop = XT.Orm.getProperty(orm, orderBy[i].attribute);
+ if (!prop) {
+ plv8.elog(ERROR, 'Attribute not found in map: ' + orderBy[i].attribute);
+ }
+ orderByIdentifiers.push(orderBy[i].attribute);
+ orderByParams.push("%" + orderByIdentifiers.length + "$I");
+ pcount = orderByParams.length - 1;
+ }
+
+ if (orderBy[i].isEmpty) {
+ orderByParams[pcount] = "length(" + orderByParams[pcount] + ")=0";
+ }
+ if (orderBy[i].descending) {
+ orderByParams[pcount] += " desc";
+ }
+
+ list.push(orderByParams[pcount])
+ }
+ }
+
+ ret.orderBy = list.length ? XT.format('order by ' + list.join(','), orderByIdentifiers) : '';
+
+ return ret;
+ },
/**
* Renew a lock. Defaults to rewing the lock for 30 seconds.
*
listItemOrms = [],
org = plv8.execute("select current_database()"),
ormAuth = {},
- orms = [],
+ orms,
schemas = {},
services,
version = "v1alpha1";
/* Build up ORMs from array. */
for (var i = 0; i < orm.length; i++) {
gotOrms = XT.Discovery.getIsRestORMs(orm[i]);
- orms = orms.concat(gotOrms).unique();
+ if (gotOrms) {
+ if (!(orms instanceof Array)) {
+ orms = [];
+ }
+ orms = orms.concat(gotOrms).unique();
+ }
}
} else {
orms = XT.Discovery.getIsRestORMs();
* Auth section.
*/
discovery.auth = XT.Discovery.getAuth(orm, rootUrl);
+ discovery.auth = XT.Discovery.getServicesAuth(orm, discovery.auth, rootUrl);
/*
* Schema section.
*/
XT.Discovery.getORMSchemas(orms, schemas);
- if (!schemas) {
- return false;
- }
-
- /* Get parent ListItem ORMs */
- for (var i = 0; i < orms.length; i++) {
- listItemOrms[i] = {"orm_namespace": orms[i].orm_namespace, "orm_type": orms[i].orm_type + "ListItem"};
- }
-
- if (listItemOrms.length > 0) {
- XT.Discovery.getORMSchemas(listItemOrms, schemas);
- }
-
/* Sanitize the JSON-Schema. */
XT.Discovery.sanitize(schemas);
XT.Discovery.getServicesSchema(null, schemas);
}
+ if (!schemas) {
+ return false;
+ }
+
+ /* Get parent ListItem ORMs */
+ if (orms && orms instanceof Array && orms.length) {
+ for (var i = 0; i < orms.length; i++) {
+ listItemOrms[i] = {"orm_namespace": orms[i].orm_namespace, "orm_type": orms[i].orm_type + "ListItem"};
+ }
+ }
+
+ if (listItemOrms.length > 0) {
+ XT.Discovery.getORMSchemas(listItemOrms, schemas);
+ }
+
/* Sort schema properties alphabetically. */
discovery.schemas = XT.Discovery.sortObject(schemas);
-
/*
* Resources section.
*/
discovery.resources = XT.Discovery.getResources(orm, rootUrl);
/* Loop through resources and add JSON-Schema primKeyProp for methods that need it. */
- for (var i = 0; i < orms.length; i++) {
- var ormType = orms[i].orm_type,
- ormNamespace = orms[i].orm_namespace,
- thisOrm = XT.Orm.fetch(ormNamespace, ormType, {"superUser": true}),
- key = XT.Discovery.getKeyProps(discovery.schemas[ormType]);
-
- if (!key) {
- /* This should never happen. */
- plv8.elog(ERROR, "No key found for ormType: ", ormType);
- }
+ if (orms && orms instanceof Array && orms.length) {
+ for (var i = 0; i < orms.length; i++) {
+ var ormType = orms[i].orm_type,
+ ormNamespace = orms[i].orm_namespace,
+ thisOrm = XT.Orm.fetch(ormNamespace, ormType, {"superUser": true}),
+ key = XT.Discovery.getKeyProps(discovery.schemas[ormType]);
+
+ if (!key) {
+ /* This should never happen. */
+ plv8.elog(ERROR, "No key found for ormType: ", ormType);
+ }
- if (thisOrm.privileges.all.delete) {
- discovery.resources[ormType].methods.delete.path = discovery.resources[ormType].methods.delete.path + key.name + "}";
- discovery.resources[ormType].methods.delete.parameters = {};
- discovery.resources[ormType].methods.delete.parameters[key.name] = key.props;
- discovery.resources[ormType].methods.delete.parameters[key.name].location = 'path';
- discovery.resources[ormType].methods.delete.parameterOrder = [key.name];
- }
+ if (thisOrm.privileges.all.delete) {
+ discovery.resources[ormType].methods.delete.path = discovery.resources[ormType].methods.delete.path + key.name + "}";
+ discovery.resources[ormType].methods.delete.parameters = {};
+ discovery.resources[ormType].methods.delete.parameters[key.name] = key.props;
+ discovery.resources[ormType].methods.delete.parameters[key.name].location = 'path';
+ discovery.resources[ormType].methods.delete.parameterOrder = [key.name];
+ }
- if (thisOrm.privileges.all.read) {
- discovery.resources[ormType].methods.get.path = discovery.resources[ormType].methods.get.path + key.name + "}";
- discovery.resources[ormType].methods.get.parameters = {};
- discovery.resources[ormType].methods.get.parameters[key.name] = key.props;
- discovery.resources[ormType].methods.get.parameters[key.name].location = 'path';
- discovery.resources[ormType].methods.get.parameterOrder = [key.name];
- }
+ if (thisOrm.privileges.all.read) {
+ discovery.resources[ormType].methods.get.path = discovery.resources[ormType].methods.get.path + key.name + "}";
+ discovery.resources[ormType].methods.get.parameters = {};
+ discovery.resources[ormType].methods.get.parameters[key.name] = key.props;
+ discovery.resources[ormType].methods.get.parameters[key.name].location = 'path';
+ discovery.resources[ormType].methods.get.parameterOrder = [key.name];
+ }
- if (thisOrm.privileges.all.read) {
- discovery.resources[ormType].methods.head.path = discovery.resources[ormType].methods.head.path + key.name + "}";
- discovery.resources[ormType].methods.head.parameters = {};
- discovery.resources[ormType].methods.head.parameters[key.name] = key.props;
- discovery.resources[ormType].methods.head.parameters[key.name].location = 'path';
- discovery.resources[ormType].methods.head.parameterOrder = [key.name];
- }
+ if (thisOrm.privileges.all.read) {
+ discovery.resources[ormType].methods.head.path = discovery.resources[ormType].methods.head.path + key.name + "}";
+ discovery.resources[ormType].methods.head.parameters = {};
+ discovery.resources[ormType].methods.head.parameters[key.name] = key.props;
+ discovery.resources[ormType].methods.head.parameters[key.name].location = 'path';
+ discovery.resources[ormType].methods.head.parameterOrder = [key.name];
+ }
- if (thisOrm.privileges.all.update) {
- discovery.resources[ormType].methods.patch.path = discovery.resources[ormType].methods.patch.path + key.name + "}";
- discovery.resources[ormType].methods.patch.parameters = {};
- discovery.resources[ormType].methods.patch.parameters[key.name] = key.props;
- discovery.resources[ormType].methods.patch.parameters[key.name].location = 'path';
- discovery.resources[ormType].methods.patch.parameterOrder = [key.name];
+ if (thisOrm.privileges.all.update) {
+ discovery.resources[ormType].methods.patch.path = discovery.resources[ormType].methods.patch.path + key.name + "}";
+ discovery.resources[ormType].methods.patch.parameters = {};
+ discovery.resources[ormType].methods.patch.parameters[key.name] = key.props;
+ discovery.resources[ormType].methods.patch.parameters[key.name].location = 'path';
+ discovery.resources[ormType].methods.patch.parameterOrder = [key.name];
+ }
}
}
var auth = {},
gotOrms,
org = plv8.execute("select current_database()"),
- orms = [];
+ orms;
rootUrl = rootUrl || "{rootUrl}";
/* Build up ORMs from array. */
for (var i = 0; i < orm.length; i++) {
gotOrms = XT.Discovery.getIsRestORMs(orm[i]);
- orms = orms.concat(gotOrms).unique();
+ if (gotOrms) {
+ if (!(orms instanceof Array)) {
+ orms = [];
+ }
+ orms = orms.concat(gotOrms).unique();
+ }
}
} else {
orms = XT.Discovery.getIsRestORMs();
/* Build up ORMs from array. */
for (var i = 0; i < orm.length; i++) {
gotOrms = XT.Discovery.getIsRestORMs(orm[i]);
- orms = orms.concat(gotOrms).unique();
+ if (gotOrms) {
+ if (!(orms instanceof Array)) {
+ orms = [];
+ }
+ orms = orms.concat(gotOrms).unique();
+ }
}
} else {
orms = XT.Discovery.getIsRestORMs();
*/
XT.Discovery.getServicesSchema = function (orm, schemas) {
"use strict";
+
schemas = schemas || {};
- var dispatchableObjects = XT.Discovery.getDispatchableObjects(orm),
+ var dispatchableObjects = [],
+ gotOrms,
i,
businessObject,
businessObjectName,
methodName,
objectServices;
+ if (orm && typeof orm === 'string') {
+ dispatchableObjects = XT.Discovery.getDispatchableObjects(orm);
+ } else if (orm instanceof Array && orm.length) {
+ /* Build up ORMs from array. */
+ for (var i = 0; i < orm.length; i++) {
+ gotOrms = XT.Discovery.getDispatchableObjects(orm[i]);
+ dispatchableObjects = dispatchableObjects.concat(gotOrms).unique();
+ }
+ } else {
+ dispatchableObjects = XT.Discovery.getDispatchableObjects(null);
+ }
+
for (i = 0; i < dispatchableObjects.length; i++) {
businessObjectName = dispatchableObjects[i];
businessObject = XM[businessObjectName];
return allServices;
};
+ /**
+ * Return an API Discovery document's Services JSON-Schema.
+ *
+ * @param {String} Optional. An orm_type name like "Contact".
+ * @param {Object} Optional. A schema object to add schemas too.
+ * @returns {Object}
+ */
+ XT.Discovery.getServicesAuth = function (orm, auth, rootUrl) {
+ "use strict";
+
+ auth = auth || {oauth2: {scopes: {}}};
+ rootUrl = rootUrl || "{rootUrl}";
+
+ var dispatchableObjects = [],
+ gotOrms,
+ org = plv8.execute("select current_database()"),
+ i,
+ businessObject,
+ businessObjectName,
+ method,
+ methodName,
+ objectServices;
+
+ if (org.length !== 1) {
+ return false;
+ } else {
+ org = org[0].current_database;
+ }
+
+ if (orm && typeof orm === 'string') {
+ dispatchableObjects = XT.Discovery.getDispatchableObjects(orm);
+ } else if (orm instanceof Array && orm.length) {
+ /* Build up ORMs from array. */
+ for (var i = 0; i < orm.length; i++) {
+ gotOrms = XT.Discovery.getDispatchableObjects(orm[i]);
+ dispatchableObjects = dispatchableObjects.concat(gotOrms).unique();
+ }
+ } else {
+ dispatchableObjects = XT.Discovery.getDispatchableObjects(null);
+ }
+
+ for (i = 0; i < dispatchableObjects.length; i++) {
+ businessObjectName = dispatchableObjects[i];
+ businessObject = XM[businessObjectName];
+ objectServices = {};
+ for (methodName in businessObject) {
+ method = businessObject[methodName];
+ /*
+ Report only on documented dispatch methods. We document the methods by
+ tacking description and params attributes onto the function.
+ */
+ if (typeof method === 'function' && method.description && method.scope) {
+ auth.oauth2.scopes[rootUrl + org + "/auth/" + method.scope.camelToHyphen()] = {
+ description: "Use " + method.scope + " services"
+ }
+ }
+ }
+ }
+
+ return auth;
+ };
/*
* Helper function to convert date to string in yyyyMMdd format.
schemas = schemas || {};
- if (!orms.length) {
+ if (!orms || (orms instanceof Array && !orms.length)) {
return false;
}
--- /dev/null
+drop operator if exists <@ (
+ uuid,
+ uuid[]
+);
+
+create operator <@ (
+ leftarg = uuid,
+ rightarg = uuid[],
+ procedure = xt.any_uuid,
+ hashes, merges
+);
value: {
'(?)attributes': {
'(+)': _.or(
+ { ANY: _.isDefined },
+ { NOT_ANY: _.isDefined },
{ EQUALS: _.isDefined },
{ NOT_EQUALS: _.isDefined },
{ MATCHES: _.isString },
*/
operators: {
value: {
+ ANY: 'ANY',
+ NOT_ANY: 'NOT ANY',
EQUALS: '=',
NOT_EQUALS: '!=',
LESS_THAN: '<',
--- /dev/null
+/*jshint trailing:true, white:true, indent:2, strict:true, curly:true,
+ immed:true, eqeqeq:true, forin:true, latedef:true,
+ newcap:true, noarg:true, undef:true */
+/*global XT:true, describe:true, it:true, require:true, __dirname:true, before:true */
+
+var _ = require("underscore"),
+ assert = require('chai').assert,
+ datasource = require('../../node-datasource/lib/ext/datasource').dataSource,
+ path = require('path');
+
+(function () {
+ "use strict";
+ describe('The database', function () {
+ this.timeout(10 * 1000);
+
+ var loginData = require(path.join(__dirname, "../lib/login_data.js")).data,
+ datasource = require('../../../xtuple/node-datasource/lib/ext/datasource').dataSource,
+ config = require(path.join(__dirname, "../../node-datasource/config.js")),
+ creds = config.databaseServer,
+ databaseName = loginData.org,
+ isCommercial = false; // this is awkward #refactor
+
+
+ before(function (done) {
+ var sql = "select metric_value from public.metric where metric_name = 'MultiWhs';";
+ creds.database = databaseName;
+ datasource.query(sql, creds, function (err, res) {
+ isCommercial = res.rows[0].metric_value === 't';
+ done();
+ });
+
+ });
+
+ // these tests are pretty fragile to the exact numbers in the database, but have been invaluable to
+ // make sure I'm not breaking anything in the fetch refactor
+ it('should execute a query with a join filter', function (done) {
+ var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"ContactListItem","query":{"orderBy":[{"attribute":"lastName"}],"rowOffset":0,"rowLimit":50,"parameters":[{"attribute":"owner.username","operator":"","isCharacteristic":false,"value":"admin"}]},"username":"admin","encryptionKey":"foo"}$$);';
+
+ datasource.query(sql, creds, function (err, res) {
+ var results;
+ assert.isNull(err);
+ assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+ results = JSON.parse(res.rows[1].get);
+ assert.equal(results.data.length, 5);
+ done();
+ });
+ });
+
+ it('should execute a query with an array', function (done) {
+ var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"ActivityListItem","query":{"orderBy":[{"attribute":"dueDate"},{"attribute":"name"},{"attribute":"uuid"}],"rowOffset":0,"rowLimit":50,"parameters":[{"attribute":"isActive","operator":"=","value":true},{"attribute":["owner.username","assignedTo.username"],"operator":"","isCharacteristic":false,"value":"admin"},{"attribute":"activityType","operator":"ANY","value":["Incident","Opportunity","ToDo","SalesOrder","SalesOrderWorkflow","PurchaseOrder","PurchaseOrderWorkflow","Project","ProjectTask","ProjectWorkflow"]}]},"username":"admin","encryptionKey":"this is any content"}$$);';
+
+ datasource.query(sql, creds, function (err, res) {
+ var results;
+ assert.isNull(err);
+ assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+ results = JSON.parse(res.rows[1].get);
+ assert.equal(results.data.length, isCommercial ? 25 : 20);
+ done();
+ });
+ });
+
+ it('should execute a query with an array with a path', function (done) {
+ var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"ContactListItem","query":{"orderBy":[{"attribute":"lastName"}],"rowOffset":0,"rowLimit":50,"parameters":[{"attribute":"owner.username","operator":"ANY","isCharacteristic":false,"value":["admin","foo"]}]},"username":"admin","encryptionKey":"foo"}$$);';
+
+ datasource.query(sql, creds, function (err, res) {
+ var results;
+ assert.isNull(err);
+ assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+ results = JSON.parse(res.rows[1].get);
+ assert.equal(results.data.length, 5);
+ done();
+ });
+ });
+
+ it('should execute a query with a simple filter', function (done) {
+ var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"ContactListItem","query":{"orderBy":[{"attribute":"lastName"}],"rowOffset":0,"rowLimit":50,"parameters":[{"attribute":"isActive","operator":"=","value":true}]},"username":"admin","encryptionKey":"foo"}$$);';
+
+ datasource.query(sql, creds, function (err, res) {
+ var results;
+ assert.isNull(err);
+ assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+ results = JSON.parse(res.rows[1].get);
+ assert.equal(results.data.length, isCommercial ? 30 : 29);
+ done();
+ });
+ });
+
+ it('should execute a query with a simple filter and a join filter', function (done) {
+ var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"ContactListItem","query":{"orderBy":[{"attribute":"lastName"}],"rowOffset":0,"rowLimit":50,"parameters":[{"attribute":"isActive","operator":"=","value":true},{"attribute":"owner.username","operator":"","isCharacteristic":false,"value":"admin"}]},"username":"admin","encryptionKey":"foo"}$$);';
+
+ datasource.query(sql, creds, function (err, res) {
+ var results;
+ assert.isNull(err);
+ assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+ results = JSON.parse(res.rows[1].get);
+ assert.equal(results.data.length, 5);
+ done();
+ });
+ });
+
+ it('should execute a query with a simple filter and two join filters', function (done) {
+ var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"ContactListItem","query":{"orderBy":[{"attribute":"lastName"}],"rowOffset":0,"rowLimit":50,"parameters":[{"attribute":"isActive","operator":"=","value":true},{"attribute":"account.number","operator":"","isCharacteristic":false,"value":"admin"},{"attribute":"owner.username","operator":"","isCharacteristic":false,"value":"admin"}]},"username":"admin","encryptionKey":"foo"}$$);';
+
+ datasource.query(sql, creds, function (err, res) {
+ var results;
+ assert.isNull(err);
+ assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+ results = JSON.parse(res.rows[1].get);
+ assert.equal(results.length, 0);
+ done();
+ });
+ });
+
+ it('should execute a query with an array of attributes', function (done) {
+ var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"ContactListItem","query":{"orderBy":[{"attribute":"lastName"},{"attribute":"firstName"},{"attribute":"primaryEmail"}],"rowOffset":0,"rowLimit":50,"parameters":[{"attribute":["number","name","firstName","lastName","jobTitle","phone","alternate","fax","primaryEmail","webAddress","accountParent"],"operator":"MATCHES","value":"coltraine"},{"attribute":"isActive","operator":"=","value":true}]},"username":"admin","encryptionKey":"this is any content"}$$);';
+
+ datasource.query(sql, creds, function (err, res) {
+ var results;
+ assert.isNull(err);
+ assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+ results = JSON.parse(res.rows[1].get);
+ assert.equal(results.data.length, 1);
+ done();
+ });
+ });
+
+ it('should execute a query with two join filters on the same table', function (done) {
+ var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"IncidentListItem","query":{"orderBy":[{"attribute":"priorityOrder"},{"attribute":"updated","descending":true},{"attribute":"number","descending":true,"numeric":true}],"rowOffset":0,"rowLimit":50,"parameters":[{"attribute":["owner.username","assignedTo.username"],"operator":"","isCharacteristic":false,"value":"admin"}]},"username":"admin","encryptionKey":"this is any content"}$$);';
+
+ datasource.query(sql, creds, function (err, res) {
+ var results;
+ assert.isNull(err);
+ assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+ results = JSON.parse(res.rows[1].get);
+ assert.equal(results.data.length, isCommercial ? 7 : 4);
+ done();
+ });
+ });
+
+ it('should execute a query with ambiguous column filters', function (done) {
+ var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"ToDoListItem","query":{"orderBy":[{"attribute":"priorityOrder"},{"attribute":"dueDate"},{"attribute":"name"}],"parameters":[{"attribute":"isActive","operator":"=","value":true},{"attribute":["owner.username","assignedTo.username"],"operator":"","isCharacteristic":false,"value":"admin"},{"attribute":"uuid","operator":"=","value":"23eef809-2f7c-4289-9eab-a72d621a6adb"}],"rowOffset":0,"rowLimit":50},"username":"admin","encryptionKey":"this is any content"}$$);';
+
+ datasource.query(sql, creds, function (err, res) {
+ var results;
+ assert.isNull(err);
+ assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+ results = JSON.parse(res.rows[1].get);
+ done();
+ });
+ });
+
+ it('should execute a query filtering on an orm-extended field', function (done) {
+ var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"ProspectRelation","query":{"orderBy":[{"attribute":"number"}],"parameters":[{"attribute":"isActive","operator":"=","value":true}],"rowOffset":0,"rowLimit":50},"username":"admin","encryptionKey":"this is any content"}$$);';
+
+ datasource.query(sql, creds, function (err, res) {
+ var results;
+ assert.isNull(err);
+ assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+ results = JSON.parse(res.rows[1].get);
+ assert.equal(results.data.length, 1);
+ done();
+ });
+ });
+
+ it('should execute an item-site fetch', function (done) {
+ var sql = 'select xt.js_init(true);select xt.post($${"nameSpace":"XM","type":"ItemSiteRelation","dispatch":{"functionName":"fetch","parameters":{"parameters":[{"attribute":"item.number","value":"BTRUCK1"},{"attribute":"site.code","value":"WH1"}]}},"username":"admin","encryptionKey":"this is any content"}$$);';
+
+ datasource.query(sql, creds, function (err, res) {
+ var results;
+ assert.isNull(err);
+ assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+ results = JSON.parse(res.rows[1].post);
+ assert.equal(results.length, 1);
+ done();
+ });
+ });
+
+ it('should execute a complicated item-site fetch', function (done) {
+ var sql = 'select xt.js_init(true);select xt.post($${"nameSpace":"XM","type":"ItemSiteRelation","dispatch":{"functionName":"fetch","parameters":{"parameters":[{"attribute":"item.isSold","value":true},{"attribute":"item.isActive","value":true},{"attribute":"isSold","value":true},{"attribute":"isActive","value":true},{"attribute":"site.code","value":"WH1"},{"attribute":"customer","value":"TTOYS"},{"attribute":["number","barcode"],"operator":"BEGINS_WITH","value":"bt","keySearch":true}],"orderBy":[{"attribute":"number"},{"attribute":"barcode"}],"rowLimit":1}},"username":"admin","encryptionKey":"this is any content"}$$)';
+
+ datasource.query(sql, creds, function (err, res) {
+ var results;
+ assert.isNull(err);
+ assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+ results = JSON.parse(res.rows[1].post);
+ assert.equal(results.length, 1);
+ done();
+ });
+ });
+
+ it('should supported a nested order-by', function (done) {
+ var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"ItemSource","query":{"orderBy":[{"attribute":"vendorItemNumber"},{"attribute":"vendor.name"}],"parameters":[{"attribute":"isActive","value":true},{"attribute":"effective","operator":"<=","value":"2014-03-20T04:00:00.000Z"},{"attribute":"expires","operator":">=","value":"2014-03-22T01:18:09.202Z"}],"rowOffset":0,"rowLimit":50},"username":"admin","encryptionKey":"this is any content"}$$);';
+
+ datasource.query(sql, creds, function (err, res) {
+ var results;
+ assert.isNull(err);
+ assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+ results = JSON.parse(res.rows[1].get);
+ assert.equal(results.data.length, isCommercial ? 21 : 20);
+ done();
+ });
+ });
+
+ it('should supported an ambiguous primary key', function (done) {
+ var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"IssueToShipping","query":{"orderBy":[{"attribute":"lineNumber"},{"attribute":"subNumber"}],"parameters":[{"attribute":"order.uuid","operator":"=","value":"d3538bbd-826a-4351-b35c-795d7db99ba0"}],"rowOffset":0,"rowLimit":50},"username":"admin","encryptionKey":"this is any content"}$$);';
+
+ if (!isCommercial) {
+ // forget about it
+ done();
+ return;
+ }
+
+ datasource.query(sql, creds, function (err, res) {
+ var results;
+ assert.isNull(err);
+ assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+ results = JSON.parse(res.rows[1].get);
+ assert.equal(results.data.length, 1);
+ done();
+ });
+ });
+
+ it('should allow the shortcut of querying a toOne directly by its natural key', function (done) {
+ var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"IncidentListItem","query":{"orderBy":[{"attribute":"priorityOrder"},{"attribute":"updated","descending":true},{"attribute":"number","descending":true,"numeric":true}],"rowOffset":0,"rowLimit":50,"parameters":[{"attribute":"category","operator":"","isCharacteristic":false,"value":"Customer"},{"attribute":["owner.username","assignedTo.username"],"operator":"","isCharacteristic":false,"value":"admin"}]},"username":"admin","encryptionKey":"this is any content"}$$);';
+
+ datasource.query(sql, creds, function (err, res) {
+ var results;
+ assert.isNull(err);
+ assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+ results = JSON.parse(res.rows[1].get);
+ assert.equal(results.data.length, 1);
+ done();
+ });
+ });
+
+ it('should allow the shortcut of querying a toOne directly by its natural key with an attribute array', function (done) {
+ var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"SalesOrderListItem","query":{"orderBy":[{"attribute":"number"}],"rowOffset":0,"rowLimit":50,"parameters":[{"attribute":["number","customerPurchaseOrderNumber","status","orderNotes","currency","billtoName","billtoCity","billtoState","billtoCountry","shiptoName","shiptoCity","shiptoState","shiptoCountry"],"operator":"MATCHES","value":"trem"},{"attribute":"status","value":"O"}]},"username":"admin","encryptionKey":"this is any content"}$$);';
+
+ datasource.query(sql, creds, function (err, res) {
+ var results;
+ assert.isNull(err);
+ assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+ results = JSON.parse(res.rows[1].get);
+ assert.equal(results.data.length, 1);
+ done();
+ });
+ });
+
+ it('should allow querying by characteristics', function (done) {
+ var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"ContactListItem","query":{"orderBy":[{"attribute":"lastName"},{"attribute":"firstName"},{"attribute":"primaryEmail"}],"rowOffset":0,"rowLimit":50,"parameters":[{"attribute":"isActive","operator":"=","value":true},{"attribute":"CONTACT-BIRTHDAY","operator":"MATCHES","isCharacteristic":true,"value":"foo"}]},"username":"admin","encryptionKey":"this is any content"}$$);';
+
+ datasource.query(sql, creds, function (err, res) {
+ var results;
+ assert.isNull(err);
+ assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+ results = JSON.parse(res.rows[1].get);
+ assert.equal(results.length, 0);
+ done();
+ });
+ });
+
+ it('should work with an empty parameters list', function (done) {
+ var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"IncidentListItem","query":{"parameters":[],"orderBy":[],"rowLimit":100},"username":"admin","encryptionKey":"xq5j2"}$$);';
+
+ datasource.query(sql, creds, function (err, res) {
+ var results;
+ assert.isNull(err);
+ assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+ results = JSON.parse(res.rows[1].get);
+ assert.equal(results.data.length, 4);
+ done();
+ });
+ });
+
+ it('should facilitate the count query', function (done) {
+ var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"ContactListItem","query":{"count":true,"orderBy":[{"attribute":"lastName"}],"rowOffset":0,"rowLimit":50,"parameters":[{"attribute":"isActive","operator":"=","value":true},{"attribute":"owner.username","operator":"","isCharacteristic":false,"value":"admin"}]},"username":"admin","encryptionKey":"foo"}$$);';
+ datasource.query(sql, creds, function (err, res) {
+ var results;
+ assert.isNull(err);
+ assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+ results = JSON.parse(res.rows[1].get);
+ assert.equal(results.data[0].count, 5);
+ done();
+ });
+ });
+
+
+ });
+}());
+
+