From a7b613c88baf94996f0a446464893e596866e2f7 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Tue, 1 Sep 2026 18:44:38 +0100 Subject: [PATCH] Implement model prefix stripping and leading operator handling in utility functions This commit introduces new functions to handle model prefix stripping and leading operator detection in the `utils.js` file. The `stripModelPrefixFromExpression` and `splitLeadingOperator` functions are added to enhance expression parsing. Additionally, existing functions are updated to utilize these new utilities, improving the handling of expressions in filters. Corresponding tests are added to ensure the correctness of the new functionality, particularly for equality and not-equal expressions. --- src/__tests__/utils.parseFilter.test.js | 12 +++++++ src/utils.js | 45 ++++++++++++++++++++----- 2 files changed, 48 insertions(+), 9 deletions(-) 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);