Skip to content

Commit

Permalink
handle comparisons in lucene queries
Browse files Browse the repository at this point in the history
  • Loading branch information
markusahlstrand committed Aug 5, 2024
1 parent e84dbfa commit f75adf8
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 14 deletions.
7 changes: 7 additions & 0 deletions packages/kysely/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# @authhero/kysely-adapter

## 0.6.11

### Patch Changes

- Trim the logs description when writing a new entry
- Update the lucene filters to handle comparisons

## 0.6.10

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/kysely/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"type": "git",
"url": "https://github.com/markusahlstrand/authhero"
},
"version": "0.6.10",
"version": "0.6.11",
"files": [
"dist"
],
Expand Down
56 changes: 43 additions & 13 deletions packages/kysely/src/helpers/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ export function luceneFilter<TB extends keyof Database>(
) {
const filters = query
.split(/\s+/)
// TODO - no .replaceAll? is this our typing rather than reality? Is this hack safe?
.map((q) => q.replace("=", ":"))
// This handles queries that incorrectly are using a = instead of :
.map((q) => q.replace(/^([^:]+)=/g, "$1:"))
.map((filter) => {
let isNegation = filter.startsWith("-");
let key, value, isExistsQuery;
let key, value, isExistsQuery, operator;

if (filter.startsWith("-_exists_:")) {
key = filter.substring(10); // Remove '-_exists_:' part
key = filter.substring(10);
isExistsQuery = true;
isNegation = true;
} else if (filter.startsWith("_exists_:")) {
key = filter.substring(9); // Remove '_exists_:' part
key = filter.substring(9);
isExistsQuery = true;
isNegation = false;
} else if (filter.includes(":")) {
Expand All @@ -29,31 +29,61 @@ export function luceneFilter<TB extends keyof Database>(
? filter.substring(1).split(":")
: filter.split(":");
isExistsQuery = false;

if (value.startsWith(">=")) {
operator = ">=";
value = value.substring(2);
} else if (value.startsWith(">")) {
operator = ">";
value = value.substring(1);
} else if (value.startsWith("<=")) {
console.log("value", value);
operator = "<=";
value = value.substring(2);
} else if (value.startsWith("<")) {
operator = "<";
value = value.substring(1);
} else {
operator = "=";
}
} else {
// Single word search case
key = null;
value = filter;
isExistsQuery = false;
}

return { key, value, isNegation, isExistsQuery };
return { key, value, isNegation, isExistsQuery, operator };
});

// Apply filters to the query builder
filters.forEach(({ key, value, isNegation, isExistsQuery }) => {
filters.forEach(({ key, value, isNegation, isExistsQuery, operator }) => {
if (key) {
if (isExistsQuery) {
if (isNegation) {
// I'm not following how this ever worked...
qb = qb.where(key as any, "is", null);
qb = qb.where(key, "is", null);
} else {
qb = qb.where(key as any, "is not", null);
qb = qb.where(key, "is not", null);
}
} else {
if (isNegation) {
qb = qb.where(key as any, "!=", value);
switch (operator) {
case ">":
qb = qb.where(key, "<=", value);
break;
case ">=":
qb = qb.where(key, "<", value);
break;
case "<":
qb = qb.where(key, ">=", value);
break;
case "<=":
qb = qb.where(key, ">", value);
break;
default:
qb = qb.where(key, "!=", value);
}
} else {
qb = qb.where(key as any, "=", value);
qb = qb.where(key, operator, value);
}
}
} else {
Expand Down
2 changes: 2 additions & 0 deletions packages/kysely/src/logs/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export function createLog(db: Kysely<Database>) {
.insertInto("logs")
.values({
...createdLog,
// Truncate long strings to avoid database errors
description: createdLog.description?.substring(0, 256),
isMobile: log.isMobile ? 1 : 0,
tenant_id,
scope: log.scope?.join(","),
Expand Down
119 changes: 119 additions & 0 deletions packages/kysely/test/helpers/filter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { Kysely } from "kysely";
import { luceneFilter } from "../../src/helpers/filter";

describe("luceneFilter", () => {
// Mock Kysely instance
const mockDb = {
dynamic: {
ref: vi.fn((col) => col),
},
} as unknown as Kysely<any>;

// Mock query builder
const mockQb = {
where: vi.fn().mockReturnThis(),
};

const searchableColumns = ["title", "description"];

beforeEach(() => {
vi.clearAllMocks();
});

it("handles single word search", () => {
luceneFilter(mockDb, mockQb as any, "test", searchableColumns);
expect(mockQb.where).toHaveBeenCalledWith(expect.any(Function));
});

it("handles exact match query", () => {
luceneFilter(mockDb, mockQb as any, "field:value", searchableColumns);
expect(mockQb.where).toHaveBeenCalledWith("field", "=", "value");
});

it("handles negation query", () => {
luceneFilter(mockDb, mockQb as any, "-field:value", searchableColumns);
expect(mockQb.where).toHaveBeenCalledWith("field", "!=", "value");
});

it("handles greater than query", () => {
luceneFilter(mockDb, mockQb as any, "field:>value", searchableColumns);
expect(mockQb.where).toHaveBeenCalledWith("field", ">", "value");
});

it("handles greater than or equal to query", () => {
luceneFilter(mockDb, mockQb as any, "field:>=value", searchableColumns);
expect(mockQb.where).toHaveBeenCalledWith("field", ">=", "value");
});

it("handles less than query", () => {
luceneFilter(mockDb, mockQb as any, "field:<value", searchableColumns);
expect(mockQb.where).toHaveBeenCalledWith("field", "<", "value");
});

it("handles less than or equal to query", () => {
luceneFilter(mockDb, mockQb as any, "field:<=value", searchableColumns);
expect(mockQb.where).toHaveBeenCalledWith("field", "<=", "value");
});

it("handles negated greater than query", () => {
luceneFilter(mockDb, mockQb as any, "-field:>value", searchableColumns);
expect(mockQb.where).toHaveBeenCalledWith("field", "<=", "value");
});

it("handles negated greater than or equal to query", () => {
luceneFilter(mockDb, mockQb as any, "-field:>=value", searchableColumns);
expect(mockQb.where).toHaveBeenCalledWith("field", "<", "value");
});

it("handles negated less than query", () => {
luceneFilter(mockDb, mockQb as any, "-field:<value", searchableColumns);
expect(mockQb.where).toHaveBeenCalledWith("field", ">=", "value");
});

it("handles negated less than or equal to query", () => {
luceneFilter(mockDb, mockQb as any, "-field:<=value", searchableColumns);
expect(mockQb.where).toHaveBeenCalledWith("field", ">", "value");
});

it("handles exists query", () => {
luceneFilter(mockDb, mockQb as any, "_exists_:field", searchableColumns);
expect(mockQb.where).toHaveBeenCalledWith("field", "is not", null);
});

it("handles not exists query", () => {
luceneFilter(mockDb, mockQb as any, "-_exists_:field", searchableColumns);
expect(mockQb.where).toHaveBeenCalledWith("field", "is", null);
});

it("handles multiple conditions", () => {
luceneFilter(
mockDb,
mockQb as any,
"field1:value1 field2:>value2 field3:<value3",
searchableColumns,
);
expect(mockQb.where).toHaveBeenCalledTimes(3);
expect(mockQb.where).toHaveBeenCalledWith("field1", "=", "value1");
expect(mockQb.where).toHaveBeenCalledWith("field2", ">", "value2");
expect(mockQb.where).toHaveBeenCalledWith("field3", "<", "value3");
});

it("handles mixed conditions", () => {
luceneFilter(
mockDb,
mockQb as any,
"searchword field:>=value _exists_:field2",
searchableColumns,
);
expect(mockQb.where).toHaveBeenCalledTimes(3);
expect(mockQb.where).toHaveBeenCalledWith(expect.any(Function));
expect(mockQb.where).toHaveBeenCalledWith("field", ">=", "value");
expect(mockQb.where).toHaveBeenCalledWith("field2", "is not", null);
});

it('handles query with "=" instead of ":"', () => {
luceneFilter(mockDb, mockQb as any, "field=value", searchableColumns);
expect(mockQb.where).toHaveBeenCalledWith("field", "=", "value");
});
});

0 comments on commit f75adf8

Please sign in to comment.