mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat(core): Add support for data table row comparison filters (no-changelog) (#18863)
This commit is contained in:
@@ -10,6 +10,10 @@ const FilterConditionSchema = z.union([
|
||||
z.literal('neq'),
|
||||
z.literal('like'),
|
||||
z.literal('ilike'),
|
||||
z.literal('gt'),
|
||||
z.literal('gte'),
|
||||
z.literal('lt'),
|
||||
z.literal('lte'),
|
||||
]);
|
||||
export type ListDataStoreContentFilterConditionType = z.infer<typeof FilterConditionSchema>;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ import type { Project, User } from '@n8n/db';
|
||||
import { ProjectRepository, QueryFailedError } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { DateTime } from 'luxon';
|
||||
import type { DataStoreRow } from 'n8n-workflow';
|
||||
|
||||
import { createDataStore } from '@test-integration/db/data-stores';
|
||||
import { createOwner, createMember, createAdmin } from '@test-integration/db/users';
|
||||
@@ -1797,6 +1798,64 @@ describe('GET /projects/:projectId/data-stores/:dataStoreId/rows', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.each([
|
||||
['gt', '>', 25, ['Bob'], ['Alice', 'Carol']],
|
||||
['gte', '>=', 25, ['Bob', 'Carol'], ['Alice']],
|
||||
['lt', '<', 25, ['Alice'], ['Bob', 'Carol']],
|
||||
['lte', '<=', 25, ['Alice', 'Carol'], ['Bob']],
|
||||
])(
|
||||
'should filter rows using %s (%s) condition correctly',
|
||||
async (condition, _operator, value, expectedNames, excludedNames) => {
|
||||
const dataStore = await createDataStore(memberProject, {
|
||||
columns: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'age',
|
||||
type: 'number',
|
||||
},
|
||||
],
|
||||
data: [
|
||||
{
|
||||
name: 'Alice',
|
||||
age: 20,
|
||||
},
|
||||
{
|
||||
name: 'Bob',
|
||||
age: 30,
|
||||
},
|
||||
{
|
||||
name: 'Carol',
|
||||
age: 25,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const filterParam = encodeURIComponent(
|
||||
JSON.stringify({
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'age', value, condition }],
|
||||
}),
|
||||
);
|
||||
const response = await authMemberAgent
|
||||
.get(`/projects/${memberProject.id}/data-stores/${dataStore.id}/rows?filter=${filterParam}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.data.count).toBe(expectedNames.length);
|
||||
const returnedNames = (response.body.data.data as DataStoreRow[]).map((row) => row.name);
|
||||
|
||||
for (const expectedName of expectedNames) {
|
||||
expect(returnedNames).toContain(expectedName);
|
||||
}
|
||||
|
||||
for (const excludedName of excludedNames) {
|
||||
expect(returnedNames).not.toContain(excludedName);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test.each(['like', 'ilike'])(
|
||||
'should auto-wrap %s filters if no wildcard is present',
|
||||
async (condition) => {
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import type {
|
||||
AddDataStoreColumnDto,
|
||||
CreateDataStoreColumnDto,
|
||||
ListDataStoreContentFilterConditionType,
|
||||
} from '@n8n/api-types';
|
||||
import type { AddDataStoreColumnDto, CreateDataStoreColumnDto } from '@n8n/api-types';
|
||||
import { createTeamProject, testDb, testModules } from '@n8n/backend-test-utils';
|
||||
import { Project } from '@n8n/db';
|
||||
import type { Project } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
import { DataStoreRowsRepository } from '../data-store-rows.repository';
|
||||
@@ -638,140 +634,6 @@ describe('dataStore', () => {
|
||||
});
|
||||
|
||||
describe('getManyAndCount', () => {
|
||||
it('should retrieve by name', async () => {
|
||||
// ARRANGE
|
||||
const dataStore = await dataStoreService.createDataStore(project1.id, {
|
||||
name: 'dataStore',
|
||||
columns: [],
|
||||
});
|
||||
|
||||
// ACT
|
||||
const result = await dataStoreService.getManyAndCount({
|
||||
filter: { projectId: project1.id, name: dataStore.name },
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0]).toEqual({
|
||||
...dataStore,
|
||||
project: expect.any(Project),
|
||||
});
|
||||
expect(result.data[0].project).toEqual({
|
||||
icon: null,
|
||||
id: project1.id,
|
||||
name: project1.name,
|
||||
type: project1.type,
|
||||
});
|
||||
expect(result.count).toEqual(1);
|
||||
});
|
||||
|
||||
it('should retrieve by ids', async () => {
|
||||
// ARRANGE
|
||||
const dataStore1 = await dataStoreService.createDataStore(project1.id, {
|
||||
name: 'myDataStore1',
|
||||
columns: [],
|
||||
});
|
||||
|
||||
const dataStore2 = await dataStoreService.createDataStore(project1.id, {
|
||||
name: 'myDataStore2',
|
||||
columns: [],
|
||||
});
|
||||
|
||||
// ACT
|
||||
const result = await dataStoreService.getManyAndCount({
|
||||
filter: { projectId: project1.id, id: [dataStore1.id, dataStore2.id] },
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(result.data).toHaveLength(2);
|
||||
expect(result.data).toContainEqual({
|
||||
...dataStore2,
|
||||
project: expect.any(Project),
|
||||
});
|
||||
expect(result.data).toContainEqual({
|
||||
...dataStore1,
|
||||
project: expect.any(Project),
|
||||
});
|
||||
expect(result.count).toEqual(2);
|
||||
});
|
||||
|
||||
it('should retrieve by projectId', async () => {
|
||||
// ARRANGE
|
||||
const dataStore = await dataStoreService.createDataStore(project1.id, {
|
||||
name: 'myDataStore',
|
||||
columns: [],
|
||||
});
|
||||
const names = [dataStore.name];
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
const ds = await dataStoreService.createDataStore(project1.id, {
|
||||
name: `anotherDataStore${i}`,
|
||||
columns: [],
|
||||
});
|
||||
names.push(ds.name);
|
||||
}
|
||||
|
||||
// ACT
|
||||
const result = await dataStoreService.getManyAndCount({
|
||||
filter: { projectId: project1.id },
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(result.data.map((x) => x.name).sort()).toEqual(names.sort());
|
||||
expect(result.count).toEqual(11);
|
||||
});
|
||||
|
||||
it('should retrieve by id with pagination', async () => {
|
||||
// ARRANGE
|
||||
const dataStore = await dataStoreService.createDataStore(project1.id, {
|
||||
name: 'myDataStore',
|
||||
columns: [],
|
||||
});
|
||||
const names = [dataStore.name];
|
||||
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
const ds = await dataStoreService.createDataStore(project1.id, {
|
||||
name: `anotherDataStore${i}`,
|
||||
columns: [],
|
||||
});
|
||||
names.push(ds.name);
|
||||
}
|
||||
|
||||
// ACT
|
||||
const p0 = await dataStoreService.getManyAndCount({
|
||||
filter: { projectId: project1.id },
|
||||
skip: 0,
|
||||
take: 3,
|
||||
});
|
||||
const p1 = await dataStoreService.getManyAndCount({
|
||||
filter: { projectId: project1.id },
|
||||
skip: 3,
|
||||
take: 3,
|
||||
});
|
||||
const rest = await dataStoreService.getManyAndCount({
|
||||
filter: { projectId: project1.id },
|
||||
skip: 6,
|
||||
take: 10,
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(p0.count).toBe(11);
|
||||
expect(p0.data).toHaveLength(3);
|
||||
|
||||
expect(p1.count).toBe(11);
|
||||
expect(p1.data).toHaveLength(3);
|
||||
|
||||
expect(rest.count).toBe(11);
|
||||
expect(rest.data).toHaveLength(5);
|
||||
|
||||
expect(
|
||||
p0.data
|
||||
.concat(p1.data)
|
||||
.concat(rest.data)
|
||||
.map((x) => x.name)
|
||||
.sort(),
|
||||
).toEqual(names.sort());
|
||||
});
|
||||
|
||||
it('correctly joins columns', async () => {
|
||||
// ARRANGE
|
||||
const columns: CreateDataStoreColumnDto[] = [
|
||||
@@ -2337,677 +2199,5 @@ describe('dataStore', () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("retrieves rows with 'equals' filter correctly", async () => {
|
||||
// ARRANGE
|
||||
const { id: dataStoreId } = await dataStoreService.createDataStore(project1.id, {
|
||||
name: 'dataStore',
|
||||
columns: [
|
||||
{ name: 'name', type: 'string' },
|
||||
{ name: 'age', type: 'number' },
|
||||
],
|
||||
});
|
||||
|
||||
const rows = [
|
||||
{ name: 'John', age: 30 },
|
||||
{ name: 'Mary', age: 25 },
|
||||
{ name: 'Jack', age: 35 },
|
||||
];
|
||||
|
||||
await dataStoreService.insertRows(dataStoreId, project1.id, rows);
|
||||
|
||||
// ACT
|
||||
const result = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {
|
||||
filter: { type: 'and', filters: [{ columnName: 'name', value: 'Mary', condition: 'eq' }] },
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(result.count).toEqual(1);
|
||||
expect(result.data).toEqual([expect.objectContaining({ name: 'Mary', age: 25 })]);
|
||||
});
|
||||
|
||||
it("retrieves rows with 'not equals' filter correctly", async () => {
|
||||
// ARRANGE
|
||||
const { id: dataStoreId } = await dataStoreService.createDataStore(project1.id, {
|
||||
name: 'dataStore',
|
||||
columns: [
|
||||
{ name: 'name', type: 'string' },
|
||||
{ name: 'age', type: 'number' },
|
||||
],
|
||||
});
|
||||
|
||||
const rows = [
|
||||
{ name: 'John', age: 30 },
|
||||
{ name: 'Mary', age: 25 },
|
||||
{ name: 'Jack', age: 35 },
|
||||
];
|
||||
|
||||
await dataStoreService.insertRows(dataStoreId, project1.id, rows);
|
||||
|
||||
// ACT
|
||||
const result = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {
|
||||
filter: { type: 'and', filters: [{ columnName: 'name', value: 'Mary', condition: 'neq' }] },
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(result.count).toEqual(2);
|
||||
expect(result.data).toEqual([
|
||||
expect.objectContaining({ name: 'John', age: 30 }),
|
||||
expect.objectContaining({ name: 'Jack', age: 35 }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("retrieves rows with 'contains sensitive' filter correctly", async () => {
|
||||
// ARRANGE
|
||||
const { id: dataStoreId } = await dataStoreService.createDataStore(project1.id, {
|
||||
name: 'dataStore',
|
||||
columns: [
|
||||
{ name: 'name', type: 'string' },
|
||||
{ name: 'age', type: 'number' },
|
||||
],
|
||||
});
|
||||
|
||||
const rows = [
|
||||
{ name: 'Arnold', age: 30 },
|
||||
{ name: 'Mary', age: 25 },
|
||||
{ name: 'Charlie', age: 35 },
|
||||
];
|
||||
|
||||
await dataStoreService.insertRows(dataStoreId, project1.id, rows);
|
||||
|
||||
// ACT
|
||||
const result = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'name', value: '%ar%', condition: 'like' }],
|
||||
},
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(result.count).toEqual(2);
|
||||
expect(result.data).toEqual([
|
||||
expect.objectContaining({ name: 'Mary', age: 25 }),
|
||||
expect.objectContaining({ name: 'Charlie', age: 35 }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("retrieves rows with 'contains insensitive' filter correctly", async () => {
|
||||
// ARRANGE
|
||||
const { id: dataStoreId } = await dataStoreService.createDataStore(project1.id, {
|
||||
name: 'dataStore',
|
||||
columns: [
|
||||
{ name: 'name', type: 'string' },
|
||||
{ name: 'age', type: 'number' },
|
||||
],
|
||||
});
|
||||
|
||||
const rows = [
|
||||
{ name: 'John', age: 30 },
|
||||
{ name: 'Mary', age: 20 },
|
||||
{ name: 'Benjamin', age: 25 },
|
||||
{ name: 'Taj', age: 35 },
|
||||
];
|
||||
|
||||
await dataStoreService.insertRows(dataStoreId, project1.id, rows);
|
||||
|
||||
// ACT
|
||||
const result = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'name', value: '%J%', condition: 'ilike' }],
|
||||
},
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(result.count).toEqual(3);
|
||||
expect(result.data).toEqual([
|
||||
expect.objectContaining({ name: 'John', age: 30 }),
|
||||
expect.objectContaining({ name: 'Benjamin', age: 25 }),
|
||||
expect.objectContaining({ name: 'Taj', age: 35 }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("retrieves rows with 'starts with' filter correctly", async () => {
|
||||
// ARRANGE
|
||||
const { id: dataStoreId } = await dataStoreService.createDataStore(project1.id, {
|
||||
name: 'dataStore',
|
||||
columns: [
|
||||
{ name: 'name', type: 'string' },
|
||||
{ name: 'age', type: 'number' },
|
||||
],
|
||||
});
|
||||
|
||||
const rows = [
|
||||
{ name: 'Arnold', age: 30 },
|
||||
{ name: 'Mary', age: 25 },
|
||||
{ name: 'Charlie', age: 35 },
|
||||
];
|
||||
|
||||
await dataStoreService.insertRows(dataStoreId, project1.id, rows);
|
||||
|
||||
// ACT
|
||||
const result = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'name', value: 'Ar%', condition: 'ilike' }],
|
||||
},
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(result.count).toEqual(1);
|
||||
expect(result.data).toEqual([expect.objectContaining({ name: 'Arnold', age: 30 })]);
|
||||
});
|
||||
|
||||
it("retrieves rows with 'ends with' filter correctly", async () => {
|
||||
// ARRANGE
|
||||
const { id: dataStoreId } = await dataStoreService.createDataStore(project1.id, {
|
||||
name: 'dataStore',
|
||||
columns: [
|
||||
{ name: 'name', type: 'string' },
|
||||
{ name: 'age', type: 'number' },
|
||||
],
|
||||
});
|
||||
|
||||
const rows = [
|
||||
{ name: 'Arnold', age: 30 },
|
||||
{ name: 'Mary', age: 25 },
|
||||
{ name: 'Charlie', age: 35 },
|
||||
{ name: 'Harold', age: 40 },
|
||||
];
|
||||
|
||||
await dataStoreService.insertRows(dataStoreId, project1.id, rows);
|
||||
|
||||
// ACT
|
||||
const result = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'name', value: '%old', condition: 'ilike' }],
|
||||
},
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(result.count).toEqual(2);
|
||||
expect(result.data).toEqual([
|
||||
expect.objectContaining({ name: 'Arnold', age: 30 }),
|
||||
expect.objectContaining({ name: 'Harold', age: 40 }),
|
||||
]);
|
||||
});
|
||||
|
||||
describe.each(['like', 'ilike'] as ListDataStoreContentFilterConditionType[])(
|
||||
'%s filter validation',
|
||||
(condition) => {
|
||||
it(`throws error when '${condition}' filter value is null`, async () => {
|
||||
// ARRANGE
|
||||
const { id: dataStoreId } = await dataStoreService.createDataStore(project1.id, {
|
||||
name: 'dataStore',
|
||||
columns: [
|
||||
{ name: 'name', type: 'string' },
|
||||
{ name: 'age', type: 'number' },
|
||||
],
|
||||
});
|
||||
|
||||
const rows = [
|
||||
{ name: 'John', age: 30 },
|
||||
{ name: 'Mary', age: 25 },
|
||||
];
|
||||
|
||||
await dataStoreService.insertRows(dataStoreId, project1.id, rows);
|
||||
|
||||
// ACT
|
||||
const result = dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'name', value: null, condition }],
|
||||
},
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
await expect(result).rejects.toThrow(
|
||||
new DataStoreValidationError(
|
||||
`${condition.toUpperCase()} filter value cannot be null or undefined`,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it(`throws error when '${condition}' filter value is not a string`, async () => {
|
||||
// ARRANGE
|
||||
const { id: dataStoreId } = await dataStoreService.createDataStore(project1.id, {
|
||||
name: 'dataStore',
|
||||
columns: [
|
||||
{ name: 'name', type: 'string' },
|
||||
{ name: 'age', type: 'number' },
|
||||
],
|
||||
});
|
||||
|
||||
const rows = [
|
||||
{ name: 'John', age: 30 },
|
||||
{ name: 'Mary', age: 25 },
|
||||
];
|
||||
|
||||
await dataStoreService.insertRows(dataStoreId, project1.id, rows);
|
||||
|
||||
// ACT
|
||||
const result = dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'age', value: 123, condition }],
|
||||
},
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
await expect(result).rejects.toThrow(
|
||||
new DataStoreValidationError(
|
||||
`${condition.toUpperCase()} filter value must be a string`,
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
describe('like filter with special characters', () => {
|
||||
let dataStoreId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const { id } = await dataStoreService.createDataStore(project1.id, {
|
||||
name: 'dataStore',
|
||||
columns: [{ name: 'text', type: 'string' }],
|
||||
});
|
||||
dataStoreId = id;
|
||||
});
|
||||
|
||||
it('should treat square brackets literally in like patterns', async () => {
|
||||
// ARRANGE
|
||||
await dataStoreService.insertRows(dataStoreId, project1.id, [
|
||||
{ text: 'test[abc]data' },
|
||||
{ text: 'Test[abc]Data' },
|
||||
{ text: 'testAdata' },
|
||||
{ text: 'testBdata' },
|
||||
]);
|
||||
|
||||
// ACT
|
||||
const result = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'text', value: 'test%[abc]%', condition: 'like' }],
|
||||
},
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(result.count).toEqual(1);
|
||||
expect(result.data).toEqual([expect.objectContaining({ text: 'test[abc]data' })]);
|
||||
});
|
||||
|
||||
it('should treat asterisk literally in like patterns', async () => {
|
||||
// ARRANGE
|
||||
await dataStoreService.insertRows(dataStoreId, project1.id, [
|
||||
{ text: 'test*data' },
|
||||
{ text: 'Test*Data' },
|
||||
{ text: 'testAdata' },
|
||||
{ text: 'testABCdata' },
|
||||
]);
|
||||
|
||||
// ACT
|
||||
const result = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'text', value: 'test%*%', condition: 'like' }],
|
||||
},
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(result.count).toEqual(1);
|
||||
expect(result.data).toEqual([expect.objectContaining({ text: 'test*data' })]);
|
||||
});
|
||||
|
||||
it('should treat question mark literally in like patterns', async () => {
|
||||
// ARRANGE
|
||||
await dataStoreService.insertRows(dataStoreId, project1.id, [
|
||||
{ text: 'test?data' },
|
||||
{ text: 'Test?Data' },
|
||||
{ text: 'testAdata' },
|
||||
{ text: 'testXdata' },
|
||||
]);
|
||||
|
||||
// ACT
|
||||
const result = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'text', value: 'test%?%', condition: 'like' }],
|
||||
},
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(result.count).toEqual(1);
|
||||
expect(result.data).toEqual([expect.objectContaining({ text: 'test?data' })]);
|
||||
});
|
||||
|
||||
it('should convert LIKE % wildcard to match zero or more characters', async () => {
|
||||
// ARRANGE
|
||||
await dataStoreService.insertRows(dataStoreId, project1.id, [
|
||||
{ text: 'data%more' },
|
||||
{ text: 'Data%More' },
|
||||
{ text: 'datamore' },
|
||||
{ text: 'dataABCmore' },
|
||||
{ text: 'different' },
|
||||
]);
|
||||
|
||||
// ACT
|
||||
const result = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'text', value: 'data%more', condition: 'like' }],
|
||||
},
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(result.count).toEqual(3);
|
||||
expect(result.data).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ text: 'data%more' }),
|
||||
expect.objectContaining({ text: 'datamore' }),
|
||||
expect.objectContaining({ text: 'dataABCmore' }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should treat underscore literally in like patterns', async () => {
|
||||
// ARRANGE
|
||||
await dataStoreService.insertRows(dataStoreId, project1.id, [
|
||||
{ text: 'prefix_suffix' },
|
||||
{ text: 'Prefix_Suffix' },
|
||||
{ text: 'prefix\\_suffix' },
|
||||
{ text: 'prefixAsuffix' },
|
||||
{ text: 'prefixsuffix' },
|
||||
]);
|
||||
|
||||
// ACT
|
||||
const result = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'text', value: 'prefix_suffix', condition: 'like' }],
|
||||
},
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(result.count).toEqual(1);
|
||||
expect(result.data).toEqual([expect.objectContaining({ text: 'prefix_suffix' })]);
|
||||
});
|
||||
|
||||
it('should handle multiple special characters', async () => {
|
||||
// ARRANGE
|
||||
await dataStoreService.insertRows(dataStoreId, project1.id, [
|
||||
{ text: 'test[*?]data' },
|
||||
{ text: 'Test[*?]Data' },
|
||||
{ text: 'testOtherData' },
|
||||
{ text: 'test123data' },
|
||||
]);
|
||||
|
||||
// ACT
|
||||
const result = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'text', value: 'test%[*?]%', condition: 'like' }],
|
||||
},
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(result.count).toEqual(1);
|
||||
expect(result.data).toEqual([expect.objectContaining({ text: 'test[*?]data' })]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ilike filter with special characters (case-insensitive)', () => {
|
||||
let dataStoreId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const { id } = await dataStoreService.createDataStore(project1.id, {
|
||||
name: 'dataStore',
|
||||
columns: [{ name: 'text', type: 'string' }],
|
||||
});
|
||||
dataStoreId = id;
|
||||
});
|
||||
|
||||
it('should treat square brackets literally', async () => {
|
||||
// ARRANGE
|
||||
await dataStoreService.insertRows(dataStoreId, project1.id, [
|
||||
{ text: 'test[abc]data' },
|
||||
{ text: 'Test[ABC]Data' },
|
||||
{ text: 'testAdata' },
|
||||
{ text: 'testBdata' },
|
||||
]);
|
||||
|
||||
// ACT
|
||||
const result = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'text', value: '%[abc]%', condition: 'ilike' }],
|
||||
},
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(result.count).toEqual(2);
|
||||
expect(result.data).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ text: 'test[abc]data' }),
|
||||
expect.objectContaining({ text: 'Test[ABC]Data' }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should treat asterisk literally', async () => {
|
||||
// ARRANGE
|
||||
await dataStoreService.insertRows(dataStoreId, project1.id, [
|
||||
{ text: 'test*data' },
|
||||
{ text: 'Test*Data' },
|
||||
{ text: 'testOtherData' },
|
||||
{ text: 'testABCdata' },
|
||||
]);
|
||||
|
||||
// ACT
|
||||
const result = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'text', value: '%*%', condition: 'ilike' }],
|
||||
},
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(result.count).toEqual(2);
|
||||
expect(result.data).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ text: 'test*data' }),
|
||||
expect.objectContaining({ text: 'Test*Data' }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should treat question mark literally', async () => {
|
||||
// ARRANGE
|
||||
await dataStoreService.insertRows(dataStoreId, project1.id, [
|
||||
{ text: 'test?data' },
|
||||
{ text: 'Test?Data' },
|
||||
{ text: 'testSingleChar' },
|
||||
{ text: 'testMultiChar' },
|
||||
]);
|
||||
|
||||
// ACT
|
||||
const result = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'text', value: '%?%', condition: 'ilike' }],
|
||||
},
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(result.count).toEqual(2);
|
||||
expect(result.data).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ text: 'test?data' }),
|
||||
expect.objectContaining({ text: 'Test?Data' }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should convert % wildcard to match zero or more characters', async () => {
|
||||
// ARRANGE
|
||||
await dataStoreService.insertRows(dataStoreId, project1.id, [
|
||||
{ text: 'data%more' },
|
||||
{ text: 'Data%More' },
|
||||
{ text: 'datamore' },
|
||||
{ text: 'DataMore' },
|
||||
{ text: 'dataABCmore' },
|
||||
{ text: 'DataABCMore' },
|
||||
{ text: 'different' },
|
||||
]);
|
||||
|
||||
// ACT
|
||||
const result = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'text', value: 'data%more', condition: 'ilike' }],
|
||||
},
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(result.count).toEqual(6);
|
||||
expect(result.data).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ text: 'data%more' }),
|
||||
expect.objectContaining({ text: 'Data%More' }),
|
||||
expect.objectContaining({ text: 'datamore' }),
|
||||
expect.objectContaining({ text: 'DataMore' }),
|
||||
expect.objectContaining({ text: 'dataABCmore' }),
|
||||
expect.objectContaining({ text: 'DataABCMore' }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should treat underscore literally', async () => {
|
||||
// ARRANGE
|
||||
await dataStoreService.insertRows(dataStoreId, project1.id, [
|
||||
{ text: 'prefix_suffix' },
|
||||
{ text: 'Prefix_Suffix' },
|
||||
{ text: 'Prefix\\_Suffix' },
|
||||
{ text: 'prefixASuffix' },
|
||||
{ text: 'prefixsuffix' },
|
||||
]);
|
||||
|
||||
// ACT
|
||||
const result = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'text', value: 'prefix_suffix', condition: 'ilike' }],
|
||||
},
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(result.count).toEqual(2);
|
||||
expect(result.data).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ text: 'prefix_suffix' }),
|
||||
expect.objectContaining({ text: 'Prefix_Suffix' }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle multiple special characters', async () => {
|
||||
// ARRANGE
|
||||
await dataStoreService.insertRows(dataStoreId, project1.id, [
|
||||
{ text: 'test[*?]data' },
|
||||
{ text: 'Test[*?]Data' },
|
||||
{ text: 'testOtherData' },
|
||||
{ text: 'test123data' },
|
||||
]);
|
||||
|
||||
// ACT
|
||||
const result = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'text', value: '%[*?]%', condition: 'ilike' }],
|
||||
},
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(result.count).toEqual(2);
|
||||
expect(result.data).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ text: 'test[*?]data' }),
|
||||
expect.objectContaining({ text: 'Test[*?]Data' }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
it('retrieves supports filter by null', async () => {
|
||||
// ARRANGE
|
||||
const { id: dataStoreId } = await dataStoreService.createDataStore(project1.id, {
|
||||
name: 'dataStore',
|
||||
columns: [
|
||||
{ name: 'c1', type: 'string' },
|
||||
{ name: 'c2', type: 'boolean' },
|
||||
],
|
||||
});
|
||||
|
||||
const rows = [
|
||||
{ c1: null, c2: true },
|
||||
{ c1: 'Marco', c2: true },
|
||||
{ c1: null, c2: false },
|
||||
{ c1: 'Polo', c2: false },
|
||||
];
|
||||
|
||||
const ids = await dataStoreService.insertRows(dataStoreId, project1.id, rows);
|
||||
expect(ids).toEqual([1, 2, 3, 4].map((id) => ({ id })));
|
||||
|
||||
// ACT
|
||||
const result = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'c1', condition: 'eq', value: null }],
|
||||
},
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(result.count).toEqual(2);
|
||||
// Assuming IDs are auto-incremented starting from 1
|
||||
expect(result.data).toMatchObject([
|
||||
{ c1: null, c2: true, id: 1 },
|
||||
{ c1: null, c2: false, id: 3 },
|
||||
]);
|
||||
});
|
||||
it('retrieves supports filter by not null', async () => {
|
||||
// ARRANGE
|
||||
const { id: dataStoreId } = await dataStoreService.createDataStore(project1.id, {
|
||||
name: 'dataStore',
|
||||
columns: [
|
||||
{ name: 'c1', type: 'string' },
|
||||
{ name: 'c2', type: 'boolean' },
|
||||
],
|
||||
});
|
||||
|
||||
const rows = [
|
||||
{ c1: null, c2: true },
|
||||
{ c1: 'Marco', c2: true },
|
||||
{ c1: null, c2: false },
|
||||
{ c1: 'Polo', c2: false },
|
||||
];
|
||||
|
||||
const ids = await dataStoreService.insertRows(dataStoreId, project1.id, rows);
|
||||
expect(ids).toEqual([1, 2, 3, 4].map((id) => ({ id })));
|
||||
|
||||
// ACT
|
||||
const result = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'c1', condition: 'neq', value: null }],
|
||||
},
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(result.count).toEqual(2);
|
||||
// Assuming IDs are auto-incremented starting from 1
|
||||
expect(result.data).toMatchObject([
|
||||
{ c1: 'Marco', c2: true, id: 2 },
|
||||
{ c1: 'Polo', c2: false, id: 4 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,13 +60,24 @@ function getConditionAndParams(
|
||||
}
|
||||
}
|
||||
|
||||
// Handle operators that map directly to SQL operators
|
||||
const operators: Record<string, string> = {
|
||||
eq: '=',
|
||||
neq: '!=',
|
||||
gt: '>',
|
||||
gte: '>=',
|
||||
lt: '<',
|
||||
lte: '<=',
|
||||
};
|
||||
|
||||
if (operators[filter.condition]) {
|
||||
return [
|
||||
`${column} ${operators[filter.condition]} :${paramName}`,
|
||||
{ [paramName]: filter.value },
|
||||
];
|
||||
}
|
||||
|
||||
switch (filter.condition) {
|
||||
case 'eq':
|
||||
return [`${column} = :${paramName}`, { [paramName]: filter.value }];
|
||||
|
||||
case 'neq':
|
||||
return [`${column} != :${paramName}`, { [paramName]: filter.value }];
|
||||
|
||||
// case-sensitive
|
||||
case 'like':
|
||||
if (['sqlite', 'sqlite-pooled'].includes(dbType)) {
|
||||
@@ -113,6 +124,9 @@ function getConditionAndParams(
|
||||
|
||||
return [`UPPER(${column}) LIKE UPPER(:${paramName})`, { [paramName]: filter.value }];
|
||||
}
|
||||
|
||||
// This should never happen as all valid conditions are handled above
|
||||
throw new Error(`Unsupported filter condition: ${filter.condition}`);
|
||||
}
|
||||
|
||||
@Service()
|
||||
|
||||
@@ -357,6 +357,14 @@ export class DataStoreService {
|
||||
filter.value = `%${filter.value}%`;
|
||||
}
|
||||
}
|
||||
|
||||
if (['gt', 'gte', 'lt', 'lte'].includes(filter.condition)) {
|
||||
if (filter.value === null || filter.value === undefined) {
|
||||
throw new DataStoreValidationError(
|
||||
`${filter.condition.toUpperCase()} filter value cannot be null or undefined`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user