objection
Version:
An SQL-friendly ORM for Node.js
137 lines (120 loc) • 5.43 kB
JavaScript
import _ from 'lodash';
import jsonFieldExpressionParser from '../../parsers/jsonFieldExpressionParser';
/**
* @typedef {String} FieldExpression
*
* Field expressions allow one to refer to separate JSONB fields inside columns.
*
* Syntax: <column reference>[:<json field reference>]
*
* e.g. `Person.jsonColumnName:details.names[1]` would refer to value `'Second'`
* in column `Person.jsonColumnName` which has
* `{ details: { names: ['First', 'Second', 'Last'] } }` object stored in it.
*
* First part `<column reference>` is compatible with column references used in
* knex e.g. `MyFancyTable.tributeToThBestColumnNameEver`.
*
* Second part describes a path to an attribute inside the referred column.
* It is optional and it always starts with colon which follows directly with
* first path element. e.g. `Table.jsonObjectColumnName:jsonFieldName` or
* `Table.jsonArrayColumn:[321]`.
*
* Syntax supports `[<key or index>]` and `.<key or index>` flavors of reference
* to json keys / array indexes:
*
* e.g. both `Table.myColumn:[1][3]` and `Table.myColumn:1.3` would access correctly
* both of the following objects `[null, [null,null,null, "I was accessed"]]` and
* `{ "1": { "3" : "I was accessed" } }`
*
* Caveats when using special characters in keys:
*
* 1. `objectColumn.key` This is the most common syntax, good if you are
* not using dots or square brackets `[]` in your json object key name.
* 2. Keys containing dots `objectColumn:[keywith.dots]` Column `{ "keywith.dots" : "I was referred" }`
* 3. Keys containing square brackets `column['[]']` `{ "[]" : "This is getting ridiculous..." }`
* 4. Keys containing square brackets and quotes
* `objectColumn:['Double."Quote".[]']` and `objectColumn:["Sinlge.'Quote'.[]"]`
* Column `{ "Double.\"Quote\".[]" : "I was referred", "Single.'Quote'.[]" : "Mee too!" }`
* 99. Keys containing dots, square brackets, single quotes and double quotes in one json key is
* not currently supported
*/
export default {
parseFieldExpression,
whereJsonbRefOnLeftJsonbValOrRefOnRight,
whereJsonbRefOnLeftJsonbValOrRefOnRightRawQueryParams,
whereJsonFieldRightStringArrayOnLeftQuery,
whereJsonFieldQuery
}
function parseFieldExpression(expression, extractAsText) {
let parsed = jsonFieldExpressionParser.parse(expression);
let jsonRefs = _(parsed.access).map('ref').value().join(",");
let extractor = extractAsText ? '#>>' : '#>';
let middleQuotedColumnName = parsed.columnName.split('.').join('"."');
return `"${middleQuotedColumnName}"${extractor}'{${jsonRefs}}'`;
}
function whereJsonbRefOnLeftJsonbValOrRefOnRight(builder, fieldExpression, operator, jsonObjectOrFieldExpression, queryPrefix) {
let queryParams = whereJsonbRefOnLeftJsonbValOrRefOnRightRawQueryParams(fieldExpression, operator, jsonObjectOrFieldExpression, queryPrefix);
return builder.whereRaw.apply(builder, queryParams);
}
function whereJsonbRefOnLeftJsonbValOrRefOnRightRawQueryParams(fieldExpression, operator, jsonObjectOrFieldExpression, queryPrefix) {
let fieldReference = parseFieldExpression(fieldExpression);
if (_.isString(jsonObjectOrFieldExpression)) {
let rightHandReference = parseFieldExpression(jsonObjectOrFieldExpression);
let refRefQuery = ["(", fieldReference, ")::jsonb", operator, "(", rightHandReference, ")::jsonb"];
if (queryPrefix) {
refRefQuery.unshift(queryPrefix);
}
return [refRefQuery.join(" ")];
} else if (_.isObject(jsonObjectOrFieldExpression)) {
let refValQuery = ["(", fieldReference, ")::jsonb", operator, "?::jsonb"];
if (queryPrefix) {
refValQuery.unshift(queryPrefix);
}
return [refValQuery.join(" "), JSON.stringify(jsonObjectOrFieldExpression)];
}
throw new Error("Invalid right hand expression.");
}
function whereJsonFieldRightStringArrayOnLeftQuery(builder, fieldExpression, operator, keys) {
let knex = builder._knex;
let fieldReference = parseFieldExpression(fieldExpression);
keys = _.isArray(keys) ? keys : [keys];
let questionMarksArray = _.map(keys, function (key) {
if (!_.isString(key)) {
throw new Error("All keys to find must be strings.");
}
return "?";
});
let rawSqlTemplateString = "array[" + questionMarksArray.join(",") + "]";
let rightHandExpression = knex.raw(rawSqlTemplateString, keys);
return `${fieldReference} ${operator.replace('?', '\\?')} ${rightHandExpression}`;
}
function whereJsonFieldQuery(knex, fieldExpression, operator, value) {
let fieldReference = parseFieldExpression(fieldExpression, true);
let normalizedOperator = normalizeOperator(knex, operator);
// json type comparison takes json type in string format
let cast;
let escapedValue = knex.raw(" ?", [value]);
if (_.isNumber(value)) {
cast = "::NUMERIC";
} else if (_.isBoolean(value)) {
cast = "::BOOLEAN";
} else if (_.isString(value)) {
cast = "::TEXT";
} else if (_.isNull(value)) {
cast = "::TEXT";
escapedValue = 'NULL';
} else {
throw new Error("Value must be string, number, boolean or null.");
}
return `(${fieldReference})${cast} ${normalizedOperator} ${escapedValue}`;
}
function normalizeOperator(knex, operator) {
let trimmedLowerCase = operator.trim().toLowerCase();
switch (trimmedLowerCase) {
case "is":
case "is not":
return trimmedLowerCase;
default:
return knex.client.formatter().operator(operator);
}
}