UNPKG

@nymphjs/query-parser

Version:

Nymph.js - Query Parser

788 lines 27 kB
import { splitn } from '@sciactive/splitn'; export default function queryParser({ query, entityClass, defaultFields = ['name'], qrefMap = {}, bareHandler = (input, _class, defaultFields = ['name']) => { if (!input.match(/[_%]/)) { input = `%${input}%`; } if (defaultFields.length) { return { type: '|', ilike: defaultFields.map((field) => [field, input]), }; } return {}; }, }) { const options = { class: entityClass }; return [ options, ...selectorsParser({ query, entityClass, type: '&', defaultFields, qrefMap, options, bareHandler, }), ]; } function selectorsParser({ query, entityClass, type, defaultFields, qrefMap, options, bareHandler, }) { const selector = { type }; let curQuery = query; // Look for top level selectors inside parens. const subSelectorPairs = []; let inQuote = false; let nesting = 0; let currentStart = null; for (let i = 0; i < curQuery.length; i++) { if (curQuery[i] === '"') { if (!inQuote) { inQuote = true; } else if (curQuery[i - 1] !== '\\') { inQuote = false; } } else if (inQuote) { continue; } else if (curQuery[i] === '(') { const searchClause = i !== 0 && !curQuery[i - 1].match(/\s/); if (currentStart == null && !searchClause) { currentStart = i; } else { nesting++; } } else if (curQuery[i] === ')') { if (nesting === 0) { if (currentStart == null) { // mismatched parens } else { subSelectorPairs.push([currentStart, i + 1]); currentStart = null; } } else { nesting--; } } } if (subSelectorPairs.length) { selector.selector = []; // Reverse order so we can take them out back to front. subSelectorPairs.reverse(); for (let pair of subSelectorPairs) { // Slice out in between the parens. let selectorQuery = curQuery.slice(pair[0] + 1, pair[1] - 1); // Cut the selector out of the query. curQuery = curQuery.slice(0, pair[0]) + curQuery.slice(pair[1]); // First char inside parens determines type of selector. let type = '&'; if (selectorQuery.startsWith('&')) { selectorQuery = selectorQuery.slice(1); } else if (selectorQuery.startsWith('!&')) { type = '!&'; selectorQuery = selectorQuery.slice(2); } else if (selectorQuery.startsWith('|')) { type = '|'; selectorQuery = selectorQuery.slice(1); } else if (selectorQuery.startsWith('!|')) { type = '!|'; selectorQuery = selectorQuery.slice(2); } else if (selectorQuery.startsWith('!')) { type = '!&'; selectorQuery = selectorQuery.slice(1); } selector.selector.push(...selectorsParser({ query: selectorQuery, entityClass, type, defaultFields, qrefMap, bareHandler, })); } } curQuery = selectorParser({ query: curQuery, selector, qrefMap, bareHandler, }); if (options) { const limitRegex = /(?: |^)limit:(\d+)(?= |$)/; const limitMatch = curQuery.match(limitRegex); if (limitMatch) { options.limit = Number(limitMatch[1]); } curQuery = curQuery.replace(limitRegex, ''); const offsetRegex = /(?: |^)offset:(\d+)(?= |$)/; const offsetMatch = curQuery.match(offsetRegex); if (offsetMatch) { options.offset = Number(offsetMatch[1]); } curQuery = curQuery.replace(offsetRegex, ''); // JavaScript variable names are ridiculously infeasable to check // thoroughly, so this is a "best attempt". const sortRegex = /(?: |^)sort:(-|[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*)(?= |$)/; const sortMatch = curQuery.match(sortRegex); if (sortMatch) { options.sort = sortMatch[1] === '-' ? null : sortMatch[1]; } curQuery = curQuery.replace(sortRegex, ''); const reverseRegex = /(?: |^)reverse:(true|false|1|0)(?= |$)/; const reverseMatch = curQuery.match(reverseRegex); if (reverseMatch) { options.reverse = reverseMatch[1] === 'true' || reverseMatch[1] === '1'; } curQuery = curQuery.replace(reverseRegex, ''); } curQuery = curQuery.trim(); if (curQuery.length) { const bareSelector = bareHandler(curQuery, entityClass, defaultFields); if (Object.keys(bareSelector).length) { return [ ...(Object.keys(selector).length > 1 ? [selector] : []), { type: '|', ...bareSelector, }, ]; } } if ('selector' in selector && selector.selector.length === 1 && Object.keys(selector).length === 2 && (selector.type === '&' || selector.type === '|')) { // There is only one subselector, and this selector is a positive match // type, so just return it as the selector. return selector.selector; } return [selector]; } function selectorParser({ query, selector, qrefMap, bareHandler, }) { let curQuery = query; // eg. prop(some search string) or prop!(some search string) const searchRegex = /(?: |^)([^\s=\[\]<>{}]+?)!?\([^\)]+\)(?= |$)/g; const searchMatch = curQuery.match(searchRegex); if (searchMatch) { selector.search = []; selector['!search'] = []; for (let match of searchMatch) { try { let [name, value] = splitn(match.trim().slice(0, -1), '(', 2); if (name.endsWith('!')) { selector['!search'].push([name.slice(0, -1), value]); } else { selector.search.push([name, value]); } } catch (e) { continue; } } if (!selector.search.length) { delete selector.search; } if (!selector['!search'].length) { delete selector['!search']; } } curQuery = curQuery.replace(searchRegex, ''); // eg. user<{User name="Hunter"}> or user!<{User name="Hunter"}> const qrefRegex = /(?: |^)([^\s=\[\]<>{}]+?)!?<\{(\w+) (.*?[^\\])\}>(?= |$)/g; const qrefMatch = curQuery.match(qrefRegex); if (qrefMatch) { selector.qref = []; selector['!qref'] = []; for (let match of qrefMatch) { try { let [name, value] = splitn(match.trim().slice(0, -1), '<', 2); value = unQuoteCurlies(value.slice(1, -1)); let [className, qrefQuery] = splitn(value, ' ', 2); const EntityClass = qrefMap[className].class; if (EntityClass == null) { continue; } const qref = queryParser({ query: qrefQuery, entityClass: EntityClass, defaultFields: qrefMap[className].defaultFields, qrefMap, bareHandler, }); if (name.endsWith('!')) { selector['!qref'].push([name.slice(0, -1), qref]); } else { selector.qref.push([name, qref]); } } catch (e) { continue; } } if (!selector.qref.length) { delete selector.qref; } if (!selector['!qref'].length) { delete selector['!qref']; } } curQuery = curQuery.replace(qrefRegex, ''); // eg. someArray=[1,2] or someObject={"prop":"some value"} const equalJsonRegex = /(?: |^)([^\s=\[\]<>{}]+?)!?=(\{|\[)/g; const equalJsonMatch = [...curQuery.matchAll(equalJsonRegex)]; if (equalJsonMatch) { if (!('equal' in selector)) { selector.equal = []; } if (!('!equal' in selector)) { selector['!equal'] = []; } // Work backward to find all long JSON values. for (let i = equalJsonMatch.length - 1; i >= 0; i--) { const match = equalJsonMatch[i]; let [name, opener] = splitn(match[0].trim(), '=', 2); let start = match.index + match[0].length - 1; let nextEndToken = curQuery.indexOf(opener === '{' ? '}' : ']', start); while (nextEndToken !== -1) { try { if (name.endsWith('!')) { selector['!equal'].unshift([ name.slice(0, -1), JSON.parse(curQuery.substring(start, nextEndToken + 1)), ]); } else { selector.equal.unshift([ name, JSON.parse(curQuery.substring(start, nextEndToken + 1)), ]); } curQuery = curQuery.substring(0, match.index) + curQuery.substring(nextEndToken + 1); break; } catch (e) { nextEndToken = curQuery.indexOf(opener === '{' ? '}' : ']', nextEndToken + 1); } } } if (selector.equal == null || !selector.equal.length) { delete selector.equal; } if (selector['!equal'] == null || !selector['!equal'].length) { delete selector['!equal']; } } // eg. name=Marty or name="Marty McFly" or enabled=true const equalRegex = /(?: |^)([^\s=\[\]<>{}]+?)!?=(""|".*?[^\\]"|[^ ]+)(?= |$)/g; const equalMatch = curQuery.match(equalRegex); if (equalMatch) { if (!('equal' in selector)) { selector.equal = []; } if (!('!equal' in selector)) { selector['!equal'] = []; } for (let match of equalMatch) { try { let [name, value] = splitn(match.trim(), '=', 2); try { if (name.endsWith('!')) { selector['!equal'].push([ name.slice(0, -1), JSON.parse(value), ]); } else { selector.equal.push([name, JSON.parse(value)]); } } catch (e) { if (name.endsWith('!')) { selector['!equal'].push([ name.slice(0, -1), unQuoteString(value), ]); } else { selector.equal.push([ name, unQuoteString(value), ]); } } } catch (e) { continue; } } if (selector.equal == null || !selector.equal.length) { delete selector.equal; } if (selector['!equal'] == null || !selector['!equal'].length) { delete selector['!equal']; } } curQuery = curQuery.replace(equalRegex, ''); // eg. user<{790274347f9b3a018c2cedee}> or user!<{790274347f9b3a018c2cedee}> const refRegex = /(?: |^)([^\s=\[\]<>{}]+?)!?<\{([0-9a-f]{24})\}>(?= |$)/g; const refMatch = curQuery.match(refRegex); if (refMatch) { selector.ref = []; selector['!ref'] = []; for (let match of refMatch) { try { let [name, value] = splitn(match.trim().slice(0, -1), '<', 2); if (name.endsWith('!')) { selector['!ref'].push([name.slice(0, -1), value.slice(1, -1)]); } else { selector.ref.push([name, value.slice(1, -1)]); } } catch (e) { continue; } } if (!selector.ref.length) { delete selector.ref; } if (!selector['!ref'].length) { delete selector['!ref']; } } curQuery = curQuery.replace(refRegex, ''); // eg. someArrayOfNumbers<10> or someObject!<"some string"> const containRegex = /(?: |^)([^\s=\[\]<>{}]+?)!?(<(?:[^"][^>]*?|".*?[^\\]"))>(?= |$)/g; const containMatch = curQuery.match(containRegex); if (containMatch) { selector.contain = []; selector['!contain'] = []; for (let match of containMatch) { try { let [name, value] = splitn(match.trim().slice(0, -1), '<', 2); try { if (name.endsWith('!')) { selector['!contain'].push([ name.slice(0, -1), JSON.parse(unQuoteString(value)), ]); } else { selector.contain.push([name, JSON.parse(unQuoteString(value))]); } } catch (e) { if (name.endsWith('!')) { selector['!contain'].push([ name.slice(0, -1), unQuoteString(value), ]); } else { selector.contain.push([name, unQuoteString(value)]); } } } catch (e) { continue; } } if (!selector.contain.length) { delete selector.contain; } if (!selector['!contain'].length) { delete selector['!contain']; } } curQuery = curQuery.replace(containRegex, ''); // eg. name~/Hunter/ or name!~/hunter/i const posixRegex = /(?: |^)([^\s=\[\]<>{}]+?)!?~(\/\/|\/.*?[^\\]\/)i?(?= |$)/g; const posixMatch = curQuery.match(posixRegex); if (posixMatch) { selector.match = []; selector['!match'] = []; selector.imatch = []; selector['!imatch'] = []; for (let match of posixMatch) { try { let [name, value] = splitn(match.trim(), '~', 2); if (name.endsWith('!')) { if (value.endsWith('i')) { selector['!imatch'].push([ name.slice(0, -1), value.replace(/^\/|\/i$/g, ''), ]); } else { selector['!match'].push([ name.slice(0, -1), value.replace(/^\/|\/$/g, ''), ]); } } else { if (value.endsWith('i')) { selector.imatch.push([name, value.replace(/^\/|\/i$/g, '')]); } else { selector.match.push([name, value.replace(/^\/|\/$/g, '')]); } } } catch (e) { continue; } } if (!selector.match.length) { delete selector.match; } if (!selector['!match'].length) { delete selector['!match']; } if (!selector.imatch.length) { delete selector.imatch; } if (!selector['!imatch'].length) { delete selector['!imatch']; } } curQuery = curQuery.replace(posixRegex, ''); // eg. name~Hunter or name!~"hunter"i const likeRegex = /(?: |^)([^\s=\[\]<>{}]+?)!?~(""i?|".*?[^\\]"i?|[^ ]+)(?= |$)/g; const likeMatch = curQuery.match(likeRegex); if (likeMatch) { selector.like = []; selector['!like'] = []; selector.ilike = []; selector['!ilike'] = []; for (let match of likeMatch) { try { let [name, value] = splitn(match.trim(), '~', 2); if (name.endsWith('!')) { if (value.endsWith('"i')) { selector['!ilike'].push([ name.slice(0, -1), unQuoteString(value.slice(0, -1)), ]); } else { selector['!like'].push([name.slice(0, -1), unQuoteString(value)]); } } else { if (value.endsWith('"i')) { selector.ilike.push([name, unQuoteString(value.slice(0, -1))]); } else { selector.like.push([name, unQuoteString(value)]); } } } catch (e) { continue; } } if (!selector.like.length) { delete selector.like; } if (!selector['!like'].length) { delete selector['!like']; } if (!selector.ilike.length) { delete selector.ilike; } if (!selector['!ilike'].length) { delete selector['!ilike']; } } curQuery = curQuery.replace(likeRegex, ''); // eg. {790274347f9b3a018c2cedee} or {!790274347f9b3a018c2cedee} const guidRegex = /(?: |^)\{!?([0-9a-f]{24})\}(?= |$)/g; const guidMatch = curQuery.match(guidRegex); if (guidMatch) { selector.guid = []; selector['!guid'] = []; for (let match of guidMatch) { try { let guid = match.trim().replace(/^\{|\}$/g, ''); if (guid.startsWith('!')) { selector['!guid'].push(guid.slice(1)); } else { selector.guid.push(guid); } } catch (e) { continue; } } if (!selector.guid.length) { delete selector.guid; } if (!selector['!guid'].length) { delete selector['!guid']; } } curQuery = curQuery.replace(guidRegex, ''); // eg. [enabled] or [!defaultPrimaryGroup] const truthyRegex = /(?: |^)\[(!?[^\s=\[\]<>{}]+?)\](?= |$)/g; const truthyMatch = curQuery.match(truthyRegex); if (truthyMatch) { selector.truthy = []; selector['!truthy'] = []; for (let match of truthyMatch) { try { let name = match.trim().replace(/^\[|\]$/g, ''); if (name.startsWith('!')) { selector['!truthy'].push(name.slice(1)); } else { selector.truthy.push(name); } } catch (e) { continue; } } if (!selector.truthy.length) { delete selector.truthy; } if (!selector['!truthy'].length) { delete selector['!truthy']; } } curQuery = curQuery.replace(truthyRegex, ''); // eg. <archived> or <!archived> const tagRegex = /(?: |^)<(!?\w+)>(?= |$)/g; const tagMatch = curQuery.match(tagRegex); if (tagMatch) { selector.tag = []; selector['!tag'] = []; for (let match of tagMatch) { try { let name = match.trim().replace(/^<|>$/g, ''); if (name.startsWith('!')) { selector['!tag'].push(name.slice(1)); } else { selector.tag.push(name); } } catch (e) { continue; } } if (!selector.tag.length) { delete selector.tag; } if (!selector['!tag'].length) { delete selector['!tag']; } } curQuery = curQuery.replace(tagRegex, ''); // eg. cdate>15 const gtRegex = /(?: |^)([^\s=\[\]<>{}]+?)>(-?\d+(?:\.\d+)?)(?= |$)/g; const gtMatch = curQuery.match(gtRegex); if (gtMatch) { selector.gt = []; for (let match of gtMatch) { try { let [name, value] = splitn(match.trim(), '>', 2); selector.gt.push([name, Number(value)]); } catch (e) { continue; } } if (!selector.gt.length) { delete selector.gt; } } curQuery = curQuery.replace(gtRegex, ''); // eg. cdate>yesterday or cdate>"2 days ago" const gtRelativeRegex = /(?: |^)([^\s=\[\]<>{}]+?)>(\w+|"[^"]+")(?= |$)/g; const gtRelativeMatch = curQuery.match(gtRelativeRegex); if (gtRelativeMatch) { if (selector.gt == null) { selector.gt = []; } for (let match of gtRelativeMatch) { try { let [name, value] = splitn(match.trim(), '>', 2); selector.gt.push([ name, null, value.replace(/"/g, ''), ]); } catch (e) { continue; } } if (!selector.gt.length) { delete selector.gt; } } curQuery = curQuery.replace(gtRelativeRegex, ''); // eg. cdate>=15 const gteRegex = /(?: |^)([^\s=\[\]<>{}]+?)>=(-?\d+(?:\.\d+)?)(?= |$)/g; const gteMatch = curQuery.match(gteRegex); if (gteMatch) { selector.gte = []; for (let match of gteMatch) { try { let [name, value] = splitn(match.trim(), '>=', 2); selector.gte.push([name, Number(value)]); } catch (e) { continue; } } if (!selector.gte.length) { delete selector.gte; } } curQuery = curQuery.replace(gteRegex, ''); // eg. cdate>=yesterday or cdate>="2 days ago" const gteRelativeRegex = /(?: |^)([^\s=\[\]<>{}]+?)>=(\w+|"[^"]+")(?= |$)/g; const gteRelativeMatch = curQuery.match(gteRelativeRegex); if (gteRelativeMatch) { if (selector.gte == null) { selector.gte = []; } for (let match of gteRelativeMatch) { try { let [name, value] = splitn(match.trim(), '>=', 2); selector.gte.push([ name, null, value.replace(/"/g, ''), ]); } catch (e) { continue; } } if (!selector.gte.length) { delete selector.gte; } } curQuery = curQuery.replace(gteRelativeRegex, ''); // eg. cdate<15 const ltRegex = /(?: |^)([^\s=\[\]<>{}]+?)<(-?\d+(?:\.\d+)?)(?= |$)/g; const ltMatch = curQuery.match(ltRegex); if (ltMatch) { selector.lt = []; for (let match of ltMatch) { try { let [name, value] = splitn(match.trim(), '<', 2); selector.lt.push([name, Number(value)]); } catch (e) { continue; } } if (!selector.lt.length) { delete selector.lt; } } curQuery = curQuery.replace(ltRegex, ''); // eg. cdate<yesterday or cdate<"2 days ago" const ltRelativeRegex = /(?: |^)([^\s=\[\]<>{}]+?)<(\w+|"[^"]+")(?= |$)/g; const ltRelativeMatch = curQuery.match(ltRelativeRegex); if (ltRelativeMatch) { if (selector.lt == null) { selector.lt = []; } for (let match of ltRelativeMatch) { try { let [name, value] = splitn(match.trim(), '<', 2); selector.lt.push([ name, null, value.replace(/"/g, ''), ]); } catch (e) { continue; } } if (!selector.lt.length) { delete selector.lt; } } curQuery = curQuery.replace(ltRelativeRegex, ''); // eg. cdate<=15 const lteRegex = /(?: |^)([^\s=\[\]<>{}]+?)<=(-?\d+(?:\.\d+)?)(?= |$)/g; const lteMatch = curQuery.match(lteRegex); if (lteMatch) { selector.lte = []; for (let match of lteMatch) { try { let [name, value] = splitn(match.trim(), '<=', 2); selector.lte.push([name, Number(value)]); } catch (e) { continue; } } if (!selector.lte.length) { delete selector.lte; } } curQuery = curQuery.replace(lteRegex, ''); // eg. cdate<=yesterday or cdate<="2 days ago" const lteRelativeRegex = /(?: |^)([^\s=\[\]<>{}]+?)<=(\w+|"[^"]+")(?= |$)/g; const lteRelativeMatch = curQuery.match(lteRelativeRegex); if (lteRelativeMatch) { if (selector.lte == null) { selector.lte = []; } for (let match of lteRelativeMatch) { try { let [name, value] = splitn(match.trim(), '<=', 2); selector.lte.push([ name, null, value.replace(/"/g, ''), ]); } catch (e) { continue; } } if (!selector.lte.length) { delete selector.lte; } } curQuery = curQuery.replace(lteRelativeRegex, ''); return curQuery.trim(); } function unQuoteString(input) { if (input.match(/^".*?[^\\]"$/)) { return input.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\'); } return input; } function unQuoteAngles(input) { return input.replace(/\\</g, '<').replace(/\\>/g, '>').replace(/\\\\/g, '\\'); } function unQuoteCurlies(input) { return input .replace(/\\\{/g, '{') .replace(/\\\}/g, '}') .replace(/\\\\/g, '\\'); } //# sourceMappingURL=queryParser.js.map