Type-safe Sieve query string builder for TypeScript
npm install ts-sieve-query-builderA type-safe query string builder for Sieve-compatible APIs in TypeScript. Build complex filtering, sorting, and pagination queries with full IntelliSense support.
- Type-safe: Full TypeScript support with compile-time property name checking
- Fluent API: Chain methods for readable query building
- Multiple output formats: Query strings, SieveModel objects, or query parameter objects
- All Sieve operators: ==, !=, @= (contains), _= (starts with), >, <, >=, <=
- Custom property support: Handle mapped properties from your Sieve processor
- Date handling: Automatic ISO string conversion for Date objects
- Zero dependencies: Lightweight and standalone
``bash`
npm install ts-sieve-query-builder
`typescript
import { SieveQueryBuilder } from 'ts-sieve-query-builder';
// Define your entity interface (or use generated types from NSwag)
interface Author {
id: string;
name: string;
createdat: Date;
}
// Build a type-safe query
const sieveModel = SieveQueryBuilder.create
.filterContains('name', 'Bob')
.sortBy('name')
.pageSize(10)
.buildSieveModel();
// Result: { filters: "name@=Bob", sorts: "name", pageSize: 10 }
`
`typescript
const builder = SieveQueryBuilder.create
// Equals
builder.filterEquals('name', 'Bob_5');
// Result: "name==Bob_5"
// Not Equals
builder.filterNotEquals('name', 'Bob_0');
// Result: "name!=Bob_0"
// Contains
builder.filterContains('name', 'Bob');
// Result: "name@=Bob"
// Starts With
builder.filterStartsWith('name', 'Bob');
// Result: "name_=Bob"
// Greater Than
builder.filterGreaterThan('pages', 200);
// Result: "pages>200"
// Less Than
builder.filterLessThan('pages', 500);
// Result: "pages<500"
// Greater Than or Equal
builder.filterGreaterThanOrEqual('pages', 200);
// Result: "pages>=200"
// Less Than or Equal
builder.filterLessThanOrEqual('pages', 500);
// Result: "pages<=500"
`
By default, filter methods append to existing filters. Use the replace parameter to replace existing filters for the same property:
`typescript
const builder = SieveQueryBuilder.create
// Without replace - appends filters (can create duplicates)
builder
.filterContains('title', 'a')
.filterContains('title', 'as')
.filterContains('title', 'ass');
// Result: "title@=a,title@=as,title@=ass"
// With replace=true - replaces existing filters for that property
builder
.filterContains('title', 'a', true)
.filterContains('title', 'as', true)
.filterContains('title', 'ass', true);
// Result: "title@=ass"
// Real-world example: Search input that updates on every keystroke
const handleSearchInput = (e: React.ChangeEvent
// Replace the title filter each time - prevents duplicates
queryBuilder.filterContains('title', e.target.value, true);
// Other filters are preserved
};
// Manual filter removal
builder.removeFilters('title'); // Remove all filters for 'title' property
builder.removeFiltersByName('BooksCount'); // For custom mapped properties
`
All filter methods support the replace parameter:filterEquals(property, value, replace?)
- filterNotEquals(property, value, replace?)
- filterContains(property, value, replace?)
- filterStartsWith(property, value, replace?)
- filterGreaterThan(property, value, replace?)
- filterLessThan(property, value, replace?)
- filterGreaterThanOrEqual(property, value, replace?)
- filterLessThanOrEqual(property, value, replace?)
- filterByName(propertyName, operator, value, replace?)
-
`typescript
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const query = SieveQueryBuilder.create
.filterGreaterThanOrEqual('createdat', thirtyDaysAgo)
.buildFiltersString();
// Result: "createdat>=2024-10-14T12:00:00.000Z" (automatically converted to ISO string)
`
For properties mapped in your C# ApplicationSieveProcessor:
This feature intentionally bypasses type-safety.
`typescript
// C# Sieve Processor maps a.Books.Count to "BooksCount"
const query = SieveQueryBuilder.create
.filterByName('BooksCount', '>=', 5)
.sortByName('BooksCount', true) // descending
.buildSieveModel();
// Result: { filters: "BooksCount>=5", sorts: "-BooksCount" }
`
`typescript
// Ascending sort
builder.sortBy('name');
// Result: "name"
// Descending sort
builder.sortByDescending('createdat');
// Result: "-createdat"
// Multiple sorts
builder
.sortByDescending('createdat')
.sortBy('name');
// Result: "-createdat,name"
`
`typescript
const query = SieveQueryBuilder.create
.page(2)
.pageSize(25)
.buildSieveModel();
// Result: { page: 2, pageSize: 25 }
`
`typescript
const sieveModel = SieveQueryBuilder.create
.filterContains('name', 'Bob')
.filterGreaterThanOrEqual('createdat', new Date('2024-01-01'))
.filterByName('BooksCount', '>=', 3)
.sortByDescending('createdat')
.sortBy('name')
.page(1)
.pageSize(20)
.buildSieveModel();
// Result:
// {
// filters: "name@=Bob,createdat>=2024-01-01T00:00:00.000Z,BooksCount>=3",
// sorts: "-createdat,name",
// page: 1,
// pageSize: 20
// }
`
You can parse an existing query string back into a builder:
`typescript
// Parse from a complete query string
const queryString = 'filters=name@=Bob&sorts=-createdat&page=2&pageSize=20';
const builder = SieveQueryBuilder.parseQueryString
// Works with leading '?' too
const urlSearch = '?filters=name@=Bob&page=1';
const builder2 = SieveQueryBuilder.parseQueryString
// Continue building on top of the parsed query
builder
.filterEquals('status', 'active')
.sortBy('name');
const model = builder.buildSieveModel();
`
You can construct a query builder from a SieveModel object (useful with URL search params):
`typescript
import { useSearchParams } from 'react-router-dom';
// In your React component
const [searchParams] = useSearchParams();
const filters = searchParams.get('filters') ?? "";
const sorts = searchParams.get('sorts') ?? "";
const pageSize = Number.parseInt(searchParams.get('pageSize') ?? "10");
const page = Number.parseInt(searchParams.get('page') ?? "1");
const queryBuilder = SieveQueryBuilder.fromSieveModel
pageSize: pageSize,
page: page,
sorts: sorts,
filters: filters
});
// Continue building on top of the parsed query
queryBuilder
.filterEquals('status', 'active')
.sortByDescending('createdat');
// Or just use it as-is
const model = queryBuilder.buildSieveModel();
`
Both parsing methods support round-trip conversion:
`typescript
// Build a query
const original = SieveQueryBuilder.create
.filterContains('name', 'Bob')
.sortBy('name')
.page(1)
.pageSize(10);
// Round-trip via query string
const queryString = original.buildQueryString();
const fromQuery = SieveQueryBuilder.parseQueryString
const rebuilt1 = fromQuery.buildQueryString();
// rebuilt1 === queryString
// Round-trip via SieveModel
const model = original.buildSieveModel();
const fromModel = SieveQueryBuilder.fromSieveModel
const rebuilt2 = fromModel.buildSieveModel();
// rebuilt2 equals model
// Add more filters/sorts after parsing
const modified = SieveQueryBuilder.parseQueryString
.filterGreaterThanOrEqual('createdat', new Date('2024-01-01'))
.page(2);
// Result includes both original and new filters/sorts
`
#### 1. SieveModel Object
`typescript`
const model = builder.buildSieveModel();
// { filters: "name@=Bob", sorts: "name", page: 1, pageSize: 10 }
#### 2. Query String
`typescript`
const queryString = builder.buildQueryString();
// "filters=name%40%3DBob&sorts=name&page=1&pageSize=10"
#### 3. Query Parameters Object
`typescript
const params = builder.buildQueryParams();
// { filters: "name@=Bob", sorts: "name", page: 1, pageSize: 10 }
// Use with URLSearchParams
const url = /api/authors?${new URLSearchParams(params)};`
#### 4. Individual Components
`typescript
const filtersString = builder.buildFiltersString();
// "name@=Bob,name!=Bob_0"
const sortsString = builder.buildSortsString();
// "-createdat,name"
`
The package includes comprehensive tests. Run them with:
`bash``
npm test
Works in all modern browsers and Node.js environments that support ES2020.
MIT
Contributions are welcome! Please open an issue or submit a pull request.