import { asc, desc } from './basefilter.js';
/* example use
// create a couple filter expressions
customerFirstName = eq("FirstName","A*")
customerCountry = eq("Country","Brazil")
q1 = new DataDiscoveryFilter(customerFirstName) // create the filter object for the first resource, with an initial filter expression if desired
q1 = q1.addFilter(customerCountry) // add the second filter expression, previously stored in a variable
.addFilter(any("City","Brasília","São Paulo")) // add another filter expression in-line
.join("Invoice") // join a related resource
.addFilter( and(gte("InvoiceDate","2011-01-01"), eq("BillingCountry","Brazil") )) // add a nested filter expression in-line
.join("InvoiceLine") // join a related resource
.join("Track") // join a related resource - last context
.sort("Name","UnitPrice") // sort results by these columns
qstring = q1.str() // retrieve the constructed query string
*/
// output:
// FirstName/A*/Country/Brazil/City/Brasília|São Paulo/Invoice/InvoiceDate/2011-01-01../BillingCountry/Brazil/InvoiceLine/Track?sort=Name,UnitPrice
// *** expression builders - any/eq/between/gte/lte build expressions
const SDB_FILTER_ERR_INVALID_COL_NAME = "Invalid column name parameter: must be non-empty string; cannot contain spaces; cannot begin with a number; cannot contain '/'; cannot contain '..'";
const SDB_FILTER_ERR_INVALID_NUM_ARGS = 'Invalid number of filter parameters';
const SDB_FILTER_ERR_INVALID_TYPE = 'Invalid data type for value, must be string or number';
const SDB_FILTER_ERR_INVALID_TYPE_NULL = 'Invalid data type for value, must be string, number, or null';
const SDB_FILTER_ERR_INVALID_VALUE_EMPTY_STRING = 'String values cannot be empty';
const SDB_FILTER_ERR_INVALID_VALUE_SLASH = "String values cannot contain '/' (use __ or set placeholder query parameter for values that contain '/') ";
const SDB_FILTER_ERR_INVALID_COMPARE_TYPE ='Range value data types must match';
const SDB_FILTER_ERR_INVALID_RANGE = 'Invalid range values';
const SDB_FILTER_ERR_INVALID_RANGE_VALUE_DOTS = "Range values cannot contain '..'";
const SDB_FILTER_ERR_NO_COL_FOUND = 'No column detected in parameter (string must contain at least one of: {col/}';
// SDB_SEPARATOR is used to separate values in the column/values string returned by any().
// SDB_SEPARATOR is imported into the DataDiscoveryFilter module, where a filter string
// that contains it is automatically detected, and it is replaced with the DataDiscoveryFilter
// separator string value. The advantage of this approach is that a user only has to
// set a unique a separator in the DataDiscoveryFilter module when they are passing values
// that contain ',' characters to the any() function. SDB_SEPARATOR is a very unique value
// by default, but can be changed using chgPlaceHolder(',',value) if desired.
// default placeholder values
const PLACEHOLDER_DEFAULTS = {
valueSeparator : '|SDBSEP|',
nullStr : '<null>'
}
let SDB_SEPARATOR = PLACEHOLDER_DEFAULTS['valueSeparator'];
let SDB_NULLSTR = PLACEHOLDER_DEFAULTS['nullStr'];
/**
* Change placeholder values for `null` and `,` in filter functions.
* eg: `chgPlaceHolder(null,'*nullValue*')` - updates the null placeholder to `*nullValue*`
*
* Calling without parameters will reset all placeholders to their default values.
* A single placeholder parameter w/o a value will reset only that parameter to its default value.
*
* Note that changing the `null` placeholder with this function will automatically set the `nullStr`
* query string parameter for any `BaseFilter`, `DataDiscoveryFilter` and `SQLPassThruFilter`
* objects created afte invoking the function.
*
* *It is not recommended to change the `,` separator unless you are encountering issues creating filters
* with the default value `|SDBSEP|`*
* @param {any} placeHolder - placeholder to update
* @param {string} value - new value for the placeholder
* @returns {any} updated placeholder value
*/
function chgPlaceHolder(placeHolder, value) {
function setPlaceHolders(placeHolder, value) {
switch(placeHolder) {
case ',':
return SDB_SEPARATOR = value === undefined ? PLACEHOLDER_DEFAULTS['valueSeparator'] : value;
case null:
return SDB_NULLSTR = value === undefined ? PLACEHOLDER_DEFAULTS['nullStr'] : value;
default:
SDB_SEPARATOR = PLACEHOLDER_DEFAULTS['valueSeparator'];
SDB_NULLSTR = PLACEHOLDER_DEFAULTS['nullStr'];
return [[',', SDB_SEPARATOR], [null, SDB_NULLSTR]];
}
}
if (arguments.length === 0) {
return setPlaceHolders();
}
if (arguments.length === 1) {
return setPlaceHolders(placeHolder);
}
if (typeof(value) !== 'string') {
throw TypeError('Placeholder value must be a string')
}
if (!isNaN(parseInt(value[0])) || value.indexOf(' ') > -1 || value.indexOf('/') > -1 || value.indexOf('..') > -1) {
throw SyntaxError("Placeholder value cannot contain spaces/slash or '..' sequences")
}
return setPlaceHolders(placeHolder, value);
};
// sanity checks for column names
function validateColumnName(col) {
if (typeof(col) !== 'string') {
throw TypeError(SDB_FILTER_ERR_INVALID_COL_NAME);
}
if (!isNaN(parseInt(col[0])) || col.indexOf(' ') > -1 || col.trim().length < 1) {
throw SyntaxError(SDB_FILTER_ERR_INVALID_COL_NAME);
}
if (col.indexOf('/') > -1) {
throw SyntaxError(SDB_FILTER_ERR_INVALID_COL_NAME);
}
if (col.indexOf('..') > -1) {
throw SyntaxError(SDB_FILTER_ERR_INVALID_COL_NAME);
}
}
/**
* Single value equality filter - column value equals parameter value
*
* eg: `eq("CustomerId",1)`
*
* @param {string} col - name of column to apply filter to
* @param {string | number} value - parameter containing the value to filter by
* @returns {string} constructed URL segment for filtering the given column
*/
function eq(col, value) {
if (arguments.length !== 2) {
throw ReferenceError(SDB_FILTER_ERR_INVALID_NUM_ARGS);
}
validateColumnName(col);
value = value === null ? SDB_NULLSTR : value;
if (typeof(value) !== 'number' && typeof(value) !== 'string') {
throw TypeError(SDB_FILTER_ERR_INVALID_TYPE_NULL);
}
if (typeof(value) === 'string' && value.indexOf('/') > -1) {
throw SyntaxError(SDB_FILTER_ERR_INVALID_VALUE_SLASH);
}
return any(col, value);
}
/**
* Multi-value equality filter - column value equals any of the parameter values
*
* eg: `any("CustomerId",1,2,3)`
*
* @param {string} col - name of column to apply filter to
* @param {...string | ...number} values - comma delimited list of values to filter by (can be mixed strings/numbers)
* @returns {string} constructed URL segment for filtering the given column
*/
function any(col, ...values) {
if (arguments.length < 2) {
throw ReferenceError(SDB_FILTER_ERR_INVALID_NUM_ARGS);
}
validateColumnName(col);
let s = `${col}/`;
for (let [i, v] of values.entries()) {
v = v === null ? SDB_NULLSTR : v;
if (typeof(v) !== 'number' && typeof(v) !== 'string') {
throw TypeError(SDB_FILTER_ERR_INVALID_TYPE_NULL + ` (parameter ${i+2})`);
}
if (typeof(v) === 'string' && v.indexOf('/') > -1) {
throw SyntaxError(SDB_FILTER_ERR_INVALID_VALUE_SLASH + ` (parameter ${i+2})`);
}
/** uncomment to disable empty strings for values **/
// if (typeof(v) === 'string' && v.trim().length === 0) {
// throw TypeError(SDB_FILTER_ERR_INVALID_VALUE_EMPTY_STRING + ` (parameter ${i+2})`);
// }
else {
// URL encode commas if desired
// if (typeof(v) === 'string') {
// v = v.replaceAll(',' , '%2C');
// }
// separate values with the unique separator string
s += `${v}${SDB_SEPARATOR}`
}
}
return s.slice(0, (0 - SDB_SEPARATOR.length) )
}
/**
* Range filter - column value is between the parameter values (inclusive)
*
* eg:
* `between("CustomerId",1,10)` - returns range string with column name
* `between(4,8)` - returns range string w/o column name
*
* @param {string} arg1 - name of column to apply filter to, or lower bound
* @param {string | number | null} lb - lower bound value of the range, or upper bound
* @param {string | number | null} ub - upper bound value of the range
* @returns {string} constructed URL segment for filtering the given column, or range string
*/
function between(arg1, lb = null, ub = null) {
const nullValues = [null,undefined];
let hasColumnName = false;
if (arguments.length < 2 || arguments.length > 3 ) {
throw ReferenceError(SDB_FILTER_ERR_INVALID_NUM_ARGS);
}
if (arguments.length == 2) {
ub = lb;
lb = arg1;
}
else if (arguments.length == 3) {
validateColumnName(arg1);
hasColumnName = true;
}
const v = [lb, ub];
// if both values are null/undefined - error
if ( nullValues.includes(v[0]) && nullValues.includes(v[1]) ) {
throw ReferenceError(SDB_FILTER_ERR_INVALID_RANGE);
}
// if both values are present but are of different types - error
if ( !nullValues.includes(v[0]) && !nullValues.includes(v[1]) ) {
if ( typeof(v[0]) !== typeof(v[1]) ) {
throw TypeError(SDB_FILTER_ERR_INVALID_COMPARE_TYPE);
}
}
const ranges = []
for (let value of v)
{
// if either value is an empty string - error
if (typeof(value) === 'string' && value.trim().length < 1) {
throw SyntaxError(SDB_FILTER_ERR_INVALID_VALUE_EMPTY_STRING);
}
// if either value contains '/' - error
if (typeof(value) === 'string' && value.indexOf('/') > -1) {
throw SyntaxError(SDB_FILTER_ERR_INVALID_VALUE_SLASH);
}
// if either value contains .. - error
if (typeof(value) === 'string' && value.indexOf('..') > -1) {
throw SyntaxError(SDB_FILTER_ERR_INVALID_RANGE_VALUE_DOTS);
}
value = nullValues.includes(value) ? '' : value;
if (typeof(value) !== 'number' && typeof(value) !== 'string') {
throw TypeError(SDB_FILTER_ERR_INVALID_TYPE);
}
ranges.push(value);
}
[lb, ub] = ranges;
return hasColumnName ? `${arg1}/${lb}..${ub}` : `${lb}..${ub}`
}
/**
* Greater-than-equal filter - column value is greater than or equal to the parameter value
* Wrapper for `between()`
*
* eg:
* `gte("CustomerId",10)` - returns range string with column name
* `gte(10)` - returns range string w/o column
*
* @param {string} arg1 - name of column to apply filter to, or lower bound
* @param {string | number } lb - lower bound value of the filter when column name given
* @returns {string} constructed URL segment for filtering the given column, or bare range string
*/
function gte(arg1, lb) {
if (arguments.length === 0 || arguments.length > 2) {
throw ReferenceError(SDB_FILTER_ERR_INVALID_NUM_ARGS);
}
if (arguments.length == 1) {
return between(arg1, null);
}
return between(arg1, lb, null);
}
/**
* Less-than-equal filter - column value is less than or equal to the parameter value
* Wrapper for `between()`
*
* eg:
* `lte("CustomerId",10)` - returns range string with column name
* `lte(10)` - returns range string w/o column
*
* @param {string} arg1 - name of column to apply filter to, or upper bound
* @param {string | number } ub - upper bound value of the filter when column name given
* @returns {string} constructed URL segment for filtering the given column, or bare range string
*/
function lte(arg1, ub) {
if (arguments.length === 0 || arguments.length > 2) {
throw ReferenceError(SDB_FILTER_ERR_INVALID_NUM_ARGS);
}
if (arguments.length == 1) {
return between(null, arg1);
}
return between(arg1, null, ub);
}
// *** expression modifiers - operate on expressions created by expression builders
/**
* Negates a filter expression - can nest `any, eq, and, between, gte, lte` expressions
*
* eg: `not(eq("CustomerId",1))`
*
* @param {string} colFilter - a string containing column filter (can contain concatenated filters)
* @returns {string} negated version of URL segment for filtering the given column(s)
*/
function not(colFilter) {
if (arguments.length !== 1) {
throw ReferenceError(SDB_FILTER_ERR_INVALID_NUM_ARGS);
}
if (typeof(colFilter) !== 'string' || colFilter.trim().length < 1) {
throw TypeError(SDB_FILTER_ERR_INVALID_TYPE);
}
if (colFilter.indexOf('/') === -1) {
throw SyntaxError(SDB_FILTER_ERR_NO_COL_FOUND);
}
return `~${colFilter}`;
}
/**
* Concatenates filter expressions - can nest `any, eq, between, gte, lte` expressions
*
* eg: `and( eq("FirstName","David")`, `any("City","Vancouver","Edmonton") )`
*
* @param {...string} colFilters - comma delimited list of strings containing column filters
* @returns {string} concatenated URL segment containing multiple column filters
*/
function and(...colFilters) {
if (colFilters.length === 0) {
throw ReferenceError(SDB_FILTER_ERR_INVALID_NUM_ARGS);
}
let s = '';
for (const [i,v] of colFilters.entries()) {
if (typeof(v) !== 'string' || v.trim().length < 1) {
throw TypeError(SDB_FILTER_ERR_INVALID_TYPE + ` (parameter ${i+1})`);
}
if (v.indexOf('/') === -1) {
throw SyntaxError(SDB_FILTER_ERR_NO_COL_FOUND);
}
s += `${v}/`;
}
return s.slice(0,-1)
}
export { eq, any, between, gte, lte, not, and }
export { chgPlaceHolder }
export { asc, desc }
export { SDB_SEPARATOR, SDB_NULLSTR }
// for testing only
export { SDB_FILTER_ERR_INVALID_COL_NAME, SDB_FILTER_ERR_INVALID_NUM_ARGS, SDB_FILTER_ERR_INVALID_TYPE, SDB_FILTER_ERR_INVALID_VALUE_EMPTY_STRING,
SDB_FILTER_ERR_INVALID_VALUE_SLASH, SDB_FILTER_ERR_INVALID_COMPARE_TYPE, SDB_FILTER_ERR_INVALID_RANGE, SDB_FILTER_ERR_NO_COL_FOUND,
SDB_FILTER_ERR_INVALID_RANGE_VALUE_DOTS, SDB_FILTER_ERR_INVALID_TYPE_NULL }