Source: datadiscoveryfilter.js

import { SDB_SEPARATOR } from "./filterfunctions.js";
import { BaseFilter } from "./basefilter.js";

const SDB_DDF_INVALID_RESOURCE = 'Resource must be a non-empty string/cannot contain spaces/cannot begin with a number/cannot contain "/" character';
const SDB_DDF_INVALID_FILTER = 'Filter must be a non-empty string, must contain at least one "/" character';
const SDB_DDF_INVALID_WILDCARD = 'Wildcard must be a string, cannot contain slash (/)';
const SDB_DDF_INVALID_SEPARATOR = 'Separator must be a string, cannot contain slash (/)';
const SDB_DDF_DEPTH_TYPE = 'Depth must be a positive integer value';
const SDB_DDF_XSDCARD_TYPE = 'xsdCardinality must be a string or positive integer'

/** 
 * Class for creating URL strings for SlashDB Data Discovery functionality
 */
class DataDiscoveryFilter extends BaseFilter {

   /**
   * Create a `DataDiscoveryFilter` object for making SlashDB-compatible URL strings
   * @extends BaseFilter
   * @param {string} [filter] - optional filter string to instantiate object with; accepts strings created using filter expression functions
   * @param {string} [wildcard] - set if using a special wildcard character(s) in URL strings (default is `*`)
   * @param {string} [separator] - set if using a special separator character(s) in URL strings (default is `,`)
   * @param {string} [urlPlaceholder] -  a string that contains a character(s) to set for the placeholder query parameter (used to indicate what character(s)
   * was used to replace '/' character in values contained in the URL that may contain the '/' character);  default is '__'
   * @throws {TypeError} if `wildcard` or `separator` parameters are not strings, are empty strings, or contain '/' character
   * @throws {TypeError} if `filter` parameter  is not a string or an empty string
   * @throws {SyntaxError} if `filter` parameter does not contain '/' character
   */    	
	constructor(filter = null, wildcard = '*', separator = ',', urlPlaceholder = '__') {
		super(urlPlaceholder);

		if (wildcard !== undefined) {
			if (typeof(wildcard) !== 'string' || wildcard.indexOf('/') > -1  || wildcard.trim().length < 1) {
				throw TypeError(SDB_DDF_INVALID_WILDCARD);
			}
		}

		if (separator !== undefined || separator !== null) {
			if (typeof(separator) !== 'string' || separator.indexOf('/') > -1  || separator.trim().length < 1) {
				throw TypeError(SDB_DDF_INVALID_SEPARATOR);
			}

		}		
		
		this.wildcard = wildcard;
		this.separator = separator;
		
		if (filter !== null) {
			if (typeof(filter) !== 'string' || filter.trim().length < 1) {
				throw TypeError(SDB_DDF_INVALID_FILTER);
			}
			if (filter.indexOf('/') === -1) {
				throw SyntaxError(SDB_DDF_INVALID_FILTER);
			}
			filter = filter.replaceAll(SDB_SEPARATOR,this.separator);
		}
		
		this.resources = new Set(['rootResource']);	// track all resources added to the filter
		this.filters = (filter === undefined || filter === null) ? {rootResource : [] } : {rootResource : [filter] };	// track the filter strings for each resource
		this.lastContext = 'rootResource';

		this.urlStringParams = {
			...this.urlStringParams,
			stream: { default: false, value: false },
			depth: { default: undefined, value: undefined },
			headers: { default: true, value: true },
			csvNullStr: { default: false, value: false },
			href: { default: true, value: true },
			cardinality: { default: undefined, value: undefined },
			wantarray: { default: false, value: false },
		}
		
		// the path as it is built up
		this.pathString = '';
		this.pathString += filter == null ? '' : `/${filter}`;

		this.build();
	}

	/**
	* Add a filter string to this object and stores info about filter; accepts strings created using filter expression functions
	* @param {string} filterString - a filter string; can contain multiple filters (e.g. `'FirstName/Tim/LastName/Smith'`) or values 
	* created using filter expression functions 
	*
	* e.g. `and(eq('FirstName','Tim'),eq('LastName','Smith'))`
	*
	* @returns {DataDiscoveryFilter} this object
	* @throws {TypeError} - if `filterString` parameter is not a string or is an empty string
	* @throws {SyntaxError} - if `filterString` parameter does not contain '/' character
	*/ 	
	addFilter(filterString) {

		if (typeof(filterString) !== 'string' || filterString.trim().length < 1) {
			throw TypeError(SDB_DDF_INVALID_FILTER);
		}
		if (filterString.indexOf('/') === -1) {
			throw SyntaxError(SDB_DDF_INVALID_FILTER);
		}

		filterString = filterString.replaceAll(SDB_SEPARATOR,this.separator);
		
		this.pathString += `/${filterString}`; 
		if (this.filters[this.lastContext] === undefined) {
			this.filters[this.lastContext] = [];
		}
		this.filters[this.lastContext].push(`${filterString}`);
			
		return this.build();
	}

	/**
	* Adds a resource to this object and stores info about it 
	* @param {string} resource - resource name to add to object
	* @returns {DataDiscoveryFilter} this object
	* @throws {TypeError} - if `resource` parameter is not a string or is an empty string
	* @throws {SyntaxError} - if `resource` parameter contains '/' character
	* @throws {SyntaxError} - if `resource` parameter parses to a number or contains a space
	*/ 		
	join(resource) { 
		if (typeof(resource) !== 'string' || resource.trim().length < 1) {
			throw TypeError(SDB_DDF_INVALID_RESOURCE);
		}
		if (resource.indexOf('/') > -1) {
			throw SyntaxError(SDB_DDF_INVALID_RESOURCE);
		}

		if (!isNaN(parseInt(resource)) || resource.indexOf(' ') > -1) {
			throw SyntaxError(SDB_DDF_INVALID_RESOURCE);
		}

		this.pathString = `${this.pathString}/${resource}`; 
		this.resources.add(resource);
		this.lastContext = resource;
		return this.build();
	}

	/**
	* Sets the `stream` query string parameter
	* @param {boolean} [toggle] - sets the parameter if not provided; removes the parameter if set to false
	* @returns {DataDiscoveryFilter} this object
	*/ 		 	
	stream(toggle = true) {
		if (toggle === true || toggle === false) {
			this.urlStringParams['stream']['value'] = toggle === true;
		}
		return this.build();
	}

	/**
	* Sets the `depth` query string parameter
	* @param {number | boolean} [level] - sets the parameter with the value provided; removes the
	* parameter if not provided or set to false
	* @returns {DataDiscoveryFilter} this object
	* @throws {TypeError} if value provided is not an integer or < 1
	*/ 	
	depth(level = false) {
		if (level) {
			if ( !Number.isInteger(level) || level < 1) {
				throw TypeError(SDB_DDF_DEPTH_TYPE);
			}
		}
		this.urlStringParams['depth']['value'] = level !== false ? level : this.urlStringParams['depth']['default'];
		return this.build();
	}
		

	/**
	* Sets the `wantarray` query string parameter
	* @param {boolean} [toggle] - sets the parameter if not provided; removes the parameter if set to false
	* @returns {DataDiscoveryFilter} this object
	*/ 	
	wantarray(toggle = true) {
		if (toggle === true || toggle === false) {
			this.urlStringParams['wantarray']['value'] = toggle === true;
		}
		return this.build();
	}

	/**
	* Sets the `headers` query string parameter; applies only to CSV formatted data
	* @param {boolean} [toggle] - sets the parameter if not provided; removes the parameter if set to false.
	* @returns {DataDiscoveryFilter} this object
	*/ 	
	csvHeader(toggle = true) {
		if (toggle === true || toggle === false) {
			this.urlStringParams['headers']['value'] = toggle === true;
		}
		return this.build();
	}

	/**
	* Sets the `csvNullStr` query string parameter; applies only to CSV formatted data
	* @param {boolean} [toggle] - sets the parameter if not provided; removes the parameter if set to false.
	* @returns {DataDiscoveryFilter} this object
	*/	
	csvNullStr(toggle = true) {
		if (toggle === true || toggle === false) {
			this.urlStringParams['csvNullStr']['value'] = toggle === true;
		}
		return this.build();
	}

	/**
	* Sets the `href` query string parameter; applies only to JSON/XML formatted data
	* @param {boolean} [toggle] - sets the parameter if not provided; removes the parameter if set to false.
	* @returns {DataDiscoveryFilter} this object
	*/		
	jsonHref(toggle = true) {
		if (toggle === true || toggle === false) {
			this.urlStringParams['href']['value'] = toggle === true;
		}
		return this.build();
	}	

	/**
	* Sets the `cardinality` query string parameter; applies only to XSD formatted data
	* @param {string} [value] - the value for the parameter (default is `'unbounded'`); removes the parameter if set to false
	* @returns {DataDiscoveryFilter} this object	
	* @throws {TypeError} if `value` parameter is an empty string
	* @throws {TypeError} if `value` parameter is not an integer or < 0
	*/		
	xsdCardinality(value = 'unbounded') {

		if (value === false) {
			value =  this.urlStringParams['cardinality']['default'];
		}
		
		else if (value !== 'unbounded') {
			if (typeof(value) === 'string' && value.trim().length < 1) {
				throw TypeError(SDB_DDF_XSDCARD_TYPE);
			}

			else if ( !Number.isInteger(value) || value < 0) {
				throw TypeError(SDB_DDF_XSDCARD_TYPE);
			}
		}
		
		this.urlStringParams['cardinality']['value'] = value;
		return this.build();
	}	

	// add wildcard to query string - only used internally
	_setWildcard() {
		return this.wildcard !== '*' ? `&wildcard=${this.wildcard}` : '';
	}	

	// add separator to query string - only used internally
	_setSeparator() {
		return this.separator !== ',' ? `&separator=${this.separator}` : '' ;
	}

	/**
	* Builds the URL endpoint string from the filter strings provided to the class and the query string parameters that have been set;
	* called at the end of most filter string methods and query string parameter methods
	* @returns {DataDiscoveryFilter} this object
	*/ 
	build() {
		let columns = this.returnColumns ? `/${this.returnColumns}` : '';

		let paramString = '';
		for (const p in this.urlStringParams) {
			if (typeof this.urlStringParams[p] === 'object' && this.urlStringParams[p] !== null ) {
				  	if (this.urlStringParams[p]['default'] !== this.urlStringParams[p]['value']) {
			  			paramString += `${p}=${this.urlStringParams[p]['value']}&` ;	
			  	}
			 }
		}

		paramString = paramString.slice(0,paramString.length-1);	// chop trailing &
		paramString += this._setSeparator() + this._setWildcard() + this._urlPlaceholderFn();
		paramString = paramString.at(0) === '&' ? paramString.slice(1) : paramString; // chop leading & if exists

		this.endpoint = paramString.length > 0 ? `${this.pathString}${columns}?${paramString}` : `${this.pathString}${columns}`;
		
		return this;
	}
}

export { DataDiscoveryFilter }

// for testing only
export { SDB_DDF_INVALID_WILDCARD, SDB_DDF_INVALID_SEPARATOR,
		SDB_DDF_DEPTH_TYPE, SDB_DDF_XSDCARD_TYPE,
		SDB_DDF_INVALID_RESOURCE, SDB_DDF_INVALID_FILTER }