diff --git a/src/__tests__/utils.parseFilter.test.js b/src/__tests__/utils.parseFilter.test.js index d926796..c4ac2ad 100644 --- a/src/__tests__/utils.parseFilter.test.js +++ b/src/__tests__/utils.parseFilter.test.js @@ -38,4 +38,16 @@ describe('parseFilter number fields', () => { const result = await parseFilter('_reference', 'JOB*', jobModel); expect(result._reference).toEqual({ $regex: '^JOB.*$', $options: 'i' }); }); + + it('strips model prefix from equality expressions', async () => { + await expect(parseFilter('_reference', 'GCF:EJX3EXUZYW2U', jobModel)).resolves.toEqual({ + _reference: { $regex: '^EJX3EXUZYW2U$', $options: 'i' }, + }); + }); + + it('strips model prefix from not-equal expressions', async () => { + await expect(parseFilter('_reference', '<>GCF:EJX3EXUZYW2U', jobModel)).resolves.toEqual({ + _reference: { $ne: 'EJX3EXUZYW2U' }, + }); + }); }); diff --git a/src/utils.js b/src/utils.js index b7cbbcd..d054547 100644 --- a/src/utils.js +++ b/src/utils.js @@ -198,6 +198,26 @@ function parsePrefixedValue(value) { return { prefix: null, suffix: trimmed, hadPrefix: false }; } +const FILTER_OPERATOR_PREFIXES = ['<>', '>=', '<=', '>', '<', '=']; + +function splitLeadingOperator(str) { + const text = String(str).trim(); + for (const op of FILTER_OPERATOR_PREFIXES) { + if (text.startsWith(op)) { + return { operator: op, operand: text.slice(op.length).trim() }; + } + } + return { operator: '', operand: text }; +} + +// Strips a model prefix (e.g. GCF:) from an expression, including after a leading operator. +function stripModelPrefixFromExpression(expression) { + const { operator, operand } = splitLeadingOperator(expression); + const { suffix, hadPrefix } = parsePrefixedValue(operand); + if (!hadPrefix) return expression; + return operator + suffix; +} + function buildRegexOp(pattern, useOptions = true) { const op = { $regex: pattern }; if (useOptions) op.$options = 'i'; @@ -242,12 +262,12 @@ function getRefModelEntryFromPrefix(prefix) { } function stripPrefixFromOperand(operand) { - const { suffix, hadPrefix } = parsePrefixedValue(String(operand).trim()); - return hadPrefix ? suffix : String(operand).trim(); + return stripModelPrefixFromExpression(operand); } function getRefModelEntryForToken(token, fallbackRefName) { - const { prefix, hadPrefix } = parsePrefixedValue(String(token).trim()); + const { operand } = splitLeadingOperator(token); + const { prefix, hadPrefix } = parsePrefixedValue(operand); if (hadPrefix) { const entry = getRefModelEntryFromPrefix(prefix); if (entry) return entry; @@ -741,9 +761,15 @@ function combineAndRefIds(children) { } async function resolveRefOperand(operand, refModelEntry, operator = 'eq') { + const lookupOperand = String(operand).trim(); + const { prefix, suffix, hadPrefix } = parsePrefixedValue(lookupOperand); + if (hadPrefix) { + const prefixEntry = getRefModelEntryFromPrefix(prefix); + if (prefixEntry) refModelEntry = prefixEntry; + } + if (!refModelEntry?.model) return NO_MATCH_CONDITION; - const lookupOperand = String(operand).trim(); const objectId = extractObjectIdFromOperand(lookupOperand); if (objectId) { @@ -752,14 +778,16 @@ async function resolveRefOperand(operand, refModelEntry, operator = 'eq') { return NO_MATCH_CONDITION; } - const expression = buildRefListExpression(operator, lookupOperand); - const ids = await listRefModelIds(refModelEntry, expression); + const expressionOperand = hadPrefix ? suffix : lookupOperand; if (operator === 'ne') { + const ids = await listRefModelIds(refModelEntry, expressionOperand); if (ids.length === 0) return { query: {} }; return { op: { $nin: ids } }; } + const expression = buildRefListExpression(operator, expressionOperand); + const ids = await listRefModelIds(refModelEntry, expression); return idsToCondition(ids); } @@ -850,9 +878,8 @@ async function parseFilter(property, value, model = null) { } let expression = trimmed; - if (fieldKind.kind === 'default' && expression.charAt(3) === ':') { - const afterColon = value.split(':')[1]; - expression = afterColon != null ? afterColon.trim() : ''; + if (fieldKind.kind === 'default') { + expression = stripModelPrefixFromExpression(expression); } const isDateField = looksLikeDateField(property);