Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sw operator to _filter search #5666

Merged
merged 1 commit into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Add sw operator to _filter search
  • Loading branch information
mattwiller committed Dec 12, 2024
commit d62f17f0db086a429e7a4ed6031cd6dd73560076
22 changes: 10 additions & 12 deletions packages/core/src/filter/parse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 5,6 @@ import { FhirFilterComparison, FhirFilterConnective, FhirFilterNegation } from '
describe('_filter Parameter parser', () => {
test('Simple comparison', () => {
const result = parseFilterParameter('name co "pet"');
expect(result).toBeDefined();
expect(result).toBeInstanceOf(FhirFilterComparison);

const comp = result as FhirFilterComparison;
Expand All @@ -16,7 15,6 @@ describe('_filter Parameter parser', () => {

test('Negation', () => {
const result = parseFilterParameter('not (name co "pet")');
expect(result).toBeDefined();
expect(result).toBeInstanceOf(FhirFilterNegation);

const negation = result as FhirFilterNegation;
Expand All @@ -30,7 28,6 @@ describe('_filter Parameter parser', () => {

test('And connective', () => {
const result = parseFilterParameter('given eq "peter" and birthdate ge 2014-10-10');
expect(result).toBeDefined();
expect(result).toBeInstanceOf(FhirFilterConnective);

const connective = result as FhirFilterConnective;
Expand All @@ -51,7 48,6 @@ describe('_filter Parameter parser', () => {

test('Or connective', () => {
const result = parseFilterParameter('given eq "peter" or birthdate ge 2014-10-10');
expect(result).toBeDefined();
expect(result).toBeInstanceOf(FhirFilterConnective);

const connective = result as FhirFilterConnective;
Expand All @@ -72,7 68,6 @@ describe('_filter Parameter parser', () => {

test('Top level parentheses', () => {
const result = parseFilterParameter('(given ne "alice" and given ne "bob")');
expect(result).toBeDefined();
expect(result).toBeInstanceOf(FhirFilterConnective);

const connective = result as FhirFilterConnective;
Expand All @@ -92,7 87,6 @@ describe('_filter Parameter parser', () => {

test('Nested expressions', () => {
const result = parseFilterParameter('given eq "alice" or (given eq "peter" and birthdate ge 2014-10-10)');
expect(result).toBeDefined();
expect(result).toBeInstanceOf(FhirFilterConnective);

const connective1 = result as FhirFilterConnective;
Expand Down Expand Up @@ -123,7 117,6 @@ describe('_filter Parameter parser', () => {

test('Nested connectives', () => {
const result = parseFilterParameter('(status eq preliminary and code eq 123) or (status eq final and code eq 456)');
expect(result).toBeDefined();
expect(result).toBeInstanceOf(FhirFilterConnective);

const connective1 = result as FhirFilterConnective;
Expand Down Expand Up @@ -166,7 159,6 @@ describe('_filter Parameter parser', () => {
const result = parseFilterParameter(
'(status eq preliminary and code eq 123) or (not (status eq preliminary) and code eq 456)'
);
expect(result).toBeDefined();
expect(result).toBeInstanceOf(FhirFilterConnective);

const connective1 = result as FhirFilterConnective;
Expand Down Expand Up @@ -208,7 200,6 @@ describe('_filter Parameter parser', () => {

test('Observation with system and code', () => {
const result = parseFilterParameter('code eq http://loinc.org|1234-5');
expect(result).toBeDefined();
expect(result).toBeInstanceOf(FhirFilterComparison);

const comp = result as FhirFilterComparison;
Expand All @@ -219,7 210,6 @@ describe('_filter Parameter parser', () => {

test('Identifier search', () => {
const result = parseFilterParameter('performer identifier https://example.com/1234');
expect(result).toBeDefined();
expect(result).toBeInstanceOf(FhirFilterComparison);

const comp = result as FhirFilterComparison;
Expand All @@ -228,12 218,20 @@ describe('_filter Parameter parser', () => {
expect(comp.value).toBe('https://example.com/1234');
});

test('Starts with', () => {
const result = parseFilterParameter('name sw ali');
expect(result).toBeInstanceOf(FhirFilterComparison);

const comp = result as FhirFilterComparison;
expect(comp.operator).toEqual(Operator.STARTS_WITH);
});

test('Unsupported search operator', () => {
try {
parseFilterParameter('name sw ali');
parseFilterParameter('name ew ali');
throw new Error('Expected error');
} catch (err: any) {
expect(err.message).toBe('Invalid operator: sw');
expect(err.message).toBe('Invalid operator: ew');
}
});
});
2 changes: 1 addition & 1 deletion packages/core/src/filter/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 17,7 @@ const operatorMap: Record<string, Operator | undefined> = {
// co - An item in the set contains this value
co: Operator.CONTAINS,
// sw - An item in the set starts with this value
sw: undefined,
sw: Operator.STARTS_WITH,
// ew - An item in the set ends with this value
ew: undefined,
// gt / lt / ge / le - A value in the set is (greater than, less than, greater or equal, less or equal) the given value
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/search/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 66,7 @@ export enum Operator {

// String
CONTAINS = 'contains',
STARTS_WITH = 'sw',
EXACT = 'exact',

// Token
Expand Down Expand Up @@ -124,6 125,7 @@ const PREFIX_OPERATORS: Record<string, Operator> = {
sa: Operator.STARTS_AFTER,
eb: Operator.ENDS_BEFORE,
ap: Operator.APPROXIMATELY,
sw: Operator.STARTS_WITH,
};

/**
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/SearchControl/SearchUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 68,7 @@ const operatorNames: Record<Operator, string> = {
sa: 'starts after',
eb: 'ends before',
ap: 'approximately',
sw: 'starts with',
contains: 'contains',
exact: 'exact',
text: 'text',
Expand Down
36 changes: 36 additions & 0 deletions packages/server/src/fhir/search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3319,6 3319,42 @@ describe('FHIR Search', () => {
expect(result.entry?.[0]?.resource?.id).toStrictEqual(patient.id);
}));

test('_filter sw', () =>
withTestContext(async () => {
const patient = await repo.createResource<Patient>({
resourceType: 'Patient',
name: [{ given: ['Evelyn', 'Dierdre'], family: 'Arachnae' }],
});

// NOTE: This incorrect behavior is currently kept for backwards compatibility,
// and should be changed to exact matching in Medplum v4
const result = await repo.search({
resourceType: 'Patient',
filters: [
{
code: '_filter',
operator: Operator.EQUALS,
value: 'name eq Evel',
},
],
});
expect(result.entry).toHaveLength(1);
expect(result.entry?.[0]?.resource?.id).toStrictEqual(patient.id);

const result2 = await repo.search({
resourceType: 'Patient',
filters: [
{
code: '_filter',
operator: Operator.EQUALS,
value: 'name sw Evel',
},
],
});
expect(result2.entry).toHaveLength(1);
expect(result2.entry?.[0]?.resource?.id).toStrictEqual(patient.id);
}));

test.each([true, false])('_filter with chained search', (ff) =>
withTestContext(async () => {
config.chainedSearchWithReferenceTables = ff;
Expand Down
5 changes: 4 additions & 1 deletion packages/server/src/fhir/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1108,14 1108,17 @@ function buildStringSearchFilter(
return expression;
}

const prefixMatchOperators = [Operator.EQUALS, Operator.STARTS_WITH];
function buildStringFilterExpression(column: Column, operator: Operator, values: string[]): Expression {
const conditions = values.map((v) => {
if (operator === Operator.EXACT) {
return new Condition(column, '=', v);
} else if (operator === Operator.CONTAINS) {
return new Condition(column, 'LIKE', `%${escapeLikeString(v)}%`);
} else {
} else if (prefixMatchOperators.includes(operator)) {
return new Condition(column, 'LIKE', `${escapeLikeString(v)}%`);
} else {
throw new OperationOutcomeError(badRequest('Unsupported string search operator: ' operator));
}
});
return new Disjunction(conditions);
Expand Down
Loading