Skip to content
Open
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
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# Changelog

## [0.2.0]
## [0.2.1]
### Bug
- Addressing issues with single and double quote characters inside filtered values.

## [0.2.0]
### Added
- Adding ability to compare with variables.
- Adding condition priority.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "js-array-filter",
"version": "0.2.0",
"version": "0.2.1",
"description": "Apply filter to an array",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
27 changes: 21 additions & 6 deletions src/utils/filterExpression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,10 @@ const parseLiteralValue = (rawValue: string, columnType: ItemTypeParsed, isMulti
if (match === null) {
throw new Error(`Invalid string value ${rawValue}`);
}
return match[2];
const quote = match[1];
const value = match[2];
const escapedQuote = quote === '"' ? '\\\\"' : "\\\\'";
return value.replace(new RegExp(escapedQuote, "g"), quote);
Comment on lines +108 to +111
}

const items = rawValue.trim().replace(/^\((.*)\)$/s, "$1");
Expand All @@ -118,7 +121,10 @@ const parseLiteralValue = (rawValue: string, columnType: ItemTypeParsed, isMulti
if (match === null) {
throw new Error(`Invalid string value ${item}`);
}
return match[2];
const quote = match[1];
const value = match[2];
const escapedQuote = quote === '"' ? '\\\\"' : "\\\\'";
return value.replace(new RegExp(escapedQuote, "g"), quote);
Comment on lines +124 to +127
});
};

Expand Down Expand Up @@ -199,17 +205,18 @@ const readConditionText = (state: ParserState): string => {
let nestedDepth = 0;
let inSingle = false;
let inDouble = false;
let previousCharWasEscape = false;

while (state.position < state.input.length) {
const character = state.input[state.position];

if (character === '"' && !inSingle) {
if (character === '"' && !inSingle && !previousCharWasEscape) {
inDouble = !inDouble;
state.position += 1;
continue;
}

if (character === "'" && !inDouble) {
if (character === "'" && !inDouble && !previousCharWasEscape) {
inSingle = !inSingle;
state.position += 1;
continue;
Expand Down Expand Up @@ -247,6 +254,11 @@ const readConditionText = (state: ParserState): string => {
}
}

if (character === "\\" && !previousCharWasEscape) {
previousCharWasEscape = true;
} else {
previousCharWasEscape = false;
}
state.position += 1;
}

Expand Down Expand Up @@ -439,12 +451,15 @@ export const conditionToString = (condition: FilterCondition): string => {
if (value.length === 0) {
valueString = "()";
} else if (typeof value[0] === "string") {
valueString = `("${value.join('", "')}")`;
const escapedValues = value.map((v) => (v as string).replace(/"/g, '\\"'));
valueString = `("${escapedValues.join('", "')}")`;
Comment on lines +454 to +455
} else {
valueString = `(${value.join(", ")})`;
}
} else if (typeof value === "string") {
valueString = `"${value}"`;
// Escape double quotes in the string value
const escapedValue = value.replace(/"/g, '\\"');
valueString = `"${escapedValue}"`;
Comment on lines +460 to +462
} else {
valueString = String(value);
}
Expand Down
2 changes: 1 addition & 1 deletion src/utils/filterRegex.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export const filterRegex = {
variable: /\w+/,
variableParse: /(\w+)/,
itemString: /["][^"]*?["]|['][^']*?[']|null/i,
itemString: /"(?:\\"|[^"])*"|'(?:\\'|[^'])*'|null/i,
itemNumber: /[^'",][^,\s]*|null/i,
itemBoolean: /True|False|null/i,
item: / /,
Expand Down
15 changes: 15 additions & 0 deletions test/filterToString.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,4 +413,19 @@ describe("filterToString", () => {
'(age > 50 or age in (20, 30, 40) or trt01p != trt01a) and (name in ("John", "Dave") or trt01p = "Placebo")',
);
});
it("should escape double quotes correctly", () => {
const filter: BasicFilter = {
conditions: [
{ variable: "name", operator: "eq", value: 'John "Doe"' },
{ variable: "age", operator: "gt", value: 30 },
],
connectors: ["and"],
};
const columns = filter.conditions.map((condition) => ({
name: condition.variable,
dataType: typeof condition.value === "string" ? "string" : ("integer" as ItemTypeDatasetJson),
})) as ColumnMetadataDatasetJson[];
const expectedString = 'name = "John \\"Doe\\"" and age > 30';
expect(new Filter("dataset-json1.1", columns, filter).toString()).toBe(expectedString);
});
});
93 changes: 93 additions & 0 deletions test/stringToFilter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,4 +326,97 @@ describe("stringToFilter", () => {

expect(new Filter("parsed", columns, filterString).toBasicFilter()).toEqual(expectedFilter);
});
it("should create filter with a value having multiple double quotes", () => {
const filterString = 'name = "John \\"Doe\\"" and age > 30';
const expectedFilter: BasicFilter = {
conditions: [
{ variable: "name", operator: "eq", value: 'John "Doe"' },
{ variable: "age", operator: "gt", value: 30 },
],
connectors: ["and"],
};

const filter = new Filter("parsed", columns, filterString).toBasicFilter();

expect(filter).toEqual(expectedFilter);
});
it("should create filter with a value having single double quote", () => {
const filterString = 'name = "John \\"Doe" and age > 30';
const expectedFilter: BasicFilter = {
conditions: [
{ variable: "name", operator: "eq", value: 'John "Doe' },
{ variable: "age", operator: "gt", value: 30 },
],
connectors: ["and"],
};

const filter = new Filter("parsed", columns, filterString).toBasicFilter();

expect(filter).toEqual(expectedFilter);
});
it("should create filter with a value having multiple single quotes", () => {
const filterString = "name = 'John \\'Doe\\'' and age > 30";
const expectedFilter: BasicFilter = {
conditions: [
{ variable: "name", operator: "eq", value: "John 'Doe'" },
{ variable: "age", operator: "gt", value: 30 },
],
connectors: ["and"],
};

const filter = new Filter("parsed", columns, filterString).toBasicFilter();

expect(filter).toEqual(expectedFilter);
});
it("should create filter with a value having one single quote", () => {
const filterString = "name = 'John \\'Doe' and age > 30";
const expectedFilter: BasicFilter = {
conditions: [
{ variable: "name", operator: "eq", value: "John 'Doe" },
{ variable: "age", operator: "gt", value: 30 },
],
connectors: ["and"],
};

const filter = new Filter("parsed", columns, filterString).toBasicFilter();

expect(filter).toEqual(expectedFilter);
});
it("should pass conversion round trip for a value with escaped double quotes", () => {
const filterString = 'name = "John \\"Doe\\"" and age > 30';
const basicFilter = new Filter("parsed", columns, filterString).toBasicFilter();
const newFilter = new Filter("parsed", columns, basicFilter).toString();

expect(newFilter).toEqual(filterString);
});
it("should pass conversion round trip for a value with escaped single quotes", () => {
const filterString = "name = 'John \\'Doe' and age > 30";
const basicFilter = new Filter("parsed", columns, filterString).toBasicFilter();
const newFilter = new Filter("parsed", columns, basicFilter).toString();
const expectedFilterString = 'name = "John \'Doe" and age > 30';

expect(newFilter).toEqual(expectedFilterString);
});
it("should save options in basic filter", () => {
const sourceFilter: BasicFilter = {
conditions: [
{ variable: "name", operator: "eq", value: "John" },
{ variable: "age", operator: "gt", value: 30 },
],
connectors: ["and"],
options: { caseInsensitive: true },
};
const basicFilter = new Filter("parsed", columns, sourceFilter).toBasicFilter();

const expectedFilter: BasicFilter = {
conditions: [
{ variable: "name", operator: "eq", value: "John" },
{ variable: "age", operator: "gt", value: 30 },
],
connectors: ["and"],
options: { caseInsensitive: true },
};

expect(basicFilter).toEqual(expectedFilter);
});
});
8 changes: 8 additions & 0 deletions test/validateFilterString.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,12 @@ describe("validateFilterString", () => {
const filterString = "name = age";
expect(new Filter("parsed", columns, "").validateFilterString(filterString)).toBe(false);
});
it("should validate a filter string with escaped single quotes", () => {
const filterString = "name = 'John \\'Doe'";
expect(new Filter("parsed", columns, "").validateFilterString(filterString)).toBe(true);
});
it("should validate a filter string with escaped double quotes", () => {
const filterString = 'name = "John \\"Doe\\""';
expect(new Filter("parsed", columns, "").validateFilterString(filterString)).toBe(true);
});
});