feat(core): Add support for data table row comparison filters (no-changelog) (#18863)

This commit is contained in:
Daria
2025-08-27 16:13:33 +03:00
committed by GitHub
parent f67b7f2ba2
commit 2ea3d034e3
6 changed files with 1230 additions and 818 deletions

View File

@@ -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

View File

@@ -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) => {

View File

@@ -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 },
]);
});
});
});

View File

@@ -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()

View File

@@ -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`,
);
}
}
}
}
}