A type-safe TypeScript DSL for building RSQL queries with full IntelliSense support.
RSQL is a query language for parametrized filtering of entries in RESTful APIs. It provides a URI-friendly syntax for expressing filters:
/api/movies?query=name=="Kill Bill";year>2003
/api/users?query=status=in=(active,pending);age>=18
- Type-safe: Field names and values are checked against your TypeScript types
- Composable: Build complex queries from smaller pieces
- React-friendly: Perfect for form components that return query fragments
- IntelliSense: Auto-complete field names and operators
- Serializable: Converts to RSQL strings for API requests
Copy and paste index.ts into your code.
import { QueryOf, serialize } from 'rsqlts';
// Define your data model
type Person = {
id: string;
name: string;
age: number;
status: 'active' | 'inactive' | 'pending';
};
// Create a type-safe query builder
const q = QueryOf<Person>();
// Build queries with full type safety
const query = q.and(
q.eq('name', 'John'), // ✓ Type-safe field names
q.gt('age', 25), // ✓ Type-safe values
q.eq('status', 'active') // ✓ Literal type checking
);
// Serialize to RSQL string
const rsqlString = serialize(query);
// => "name==John;age>25;status==active"
// Use in API call
fetch(`/api/users?query=${encodeURIComponent(rsqlString)}`);Create a type-safe query builder for your type:
const q = QueryOf<Person>();q.eq('name', 'John') // name==John
q.neq('status', 'inactive') // status!=inactive
q.lt('age', 30) // age<30
q.lte('age', 30) // age<=30
q.gt('age', 18) // age>18
q.gte('age', 18) // age>=18q.inArray('status', ['active', 'pending']) // status=in=(active,pending)
q.notIn('status', ['inactive']) // status=out=(inactive)// AND - higher precedence
q.and(
q.eq('name', 'John'),
q.gt('age', 25)
)
// => name==John;age>25
// OR - lower precedence
q.or(
q.eq('status', 'active'),
q.eq('status', 'pending')
)
// => status==active,status==pendingconst query = q.and(
q.inArray('status', ['active', 'pending']),
q.or(
q.eq('name', 'John'),
q.gt('age', 50)
),
q.gte('dob', 946684800000)
);
// => status=in=(active,pending);(name==John,age>50);dob>=946684800000Perfect for building queries from multiple form components:
import { RsqlExpression, serialize } from 'rsqlts';
// Child components return raw expressions
function NameFilter({ value }: { value: string }): RsqlExpression<Person> {
return {
type: 'comparison',
field: 'name', // Type-safe: must be keyof Person
operator: '==',
value: value
};
}
function AgeRangeFilter({ min, max }: { min: number; max: number }): RsqlExpression<Person> {
return {
type: 'and',
expressions: [
{ type: 'comparison', field: 'age', operator: '>=', value: min },
{ type: 'comparison', field: 'age', operator: '<=', value: max }
]
};
}
// Parent combines them
function SearchForm() {
const nameExpr = NameFilter({ value: 'John' });
const ageExpr = AgeRangeFilter({ min: 25, max: 65 });
const query: RsqlExpression<Person> = {
type: 'and',
expressions: [nameExpr, ageExpr]
};
const rsql = serialize(query);
// => "name==John;age>=25;age<=65"
}import { QueryOf, RsqlExpression, serialize } from 'rsqlts';
const q = QueryOf<Person>();
// Each filter section returns an expression or undefined
function useNameFilter(value: string | undefined): RsqlExpression<Person> | undefined {
if (!value) return undefined;
return q.eq('name', value);
}
function useAgeFilter(min?: number, max?: number): RsqlExpression<Person> | undefined {
const conditions: RsqlExpression<Person>[] = [];
if (min !== undefined) conditions.push(q.gte('age', min));
if (max !== undefined) conditions.push(q.lte('age', max));
return conditions.length > 0 ? q.and(...conditions) : undefined;
}
function useStatusFilter(statuses: string[]): RsqlExpression<Person> | undefined {
if (statuses.length === 0) return undefined;
return q.inArray('status', statuses as Person['status'][]);
}
// Parent component combines all filters
function SearchForm() {
const filters: RsqlExpression<Person>[] = [
useNameFilter('John'),
useAgeFilter(25, 65),
useStatusFilter(['active', 'pending'])
].filter((f): f is RsqlExpression<Person> => f !== undefined);
const query = filters.length > 0 ? q.and(...filters) : undefined;
if (query) {
const rsql = serialize(query);
// => "name==John;age>=25;age<=65;status=in=(active,pending)"
// Use in API call
fetch(`/api/users?query=${encodeURIComponent(rsql)}`);
}
}const filters: RsqlExpression<Person>[] = [];
if (searchTerm) {
filters.push(q.eq('name', searchTerm));
}
if (minAge !== undefined) {
filters.push(q.gte('age', minAge));
}
const query = filters.length > 0 ? q.and(...filters) : undefined;let query: RsqlExpression<Person> = q.eq('status', 'active');
if (includeAgeFilter) {
query = q.and(query, q.gte('age', 18));
}
if (includeVIP) {
query = q.or(query, q.eq('name', 'VIP'));
}const adminFilter = q.and(
q.eq('status', 'active'),
q.eq('role', 'admin')
);
const seniorFilter = q.and(
q.eq('status', 'active'),
q.gt('age', 60)
);
const query = q.or(adminFilter, seniorFilter);
// => (status==active;role==admin),(status==active;age>60)const q = QueryOf<Person>();
// ✓ Valid: field exists and type matches
q.eq('name', 'John');
q.gt('age', 25);
q.eq('status', 'active');
// ✗ TypeScript errors:
q.eq('nonexistent', 'value'); // Error: field doesn't exist
q.eq('age', 'string'); // Error: type mismatch
q.eq('status', 'invalid'); // Error: not in literal unionThis library implements the RSQL specification, which is based on FIQL (Feed Item Query Language).
| Operator | Alternative | Description |
|---|---|---|
== |
Equal to | |
!= |
Not equal to | |
=lt= |
< |
Less than |
=le= |
<= |
Less than or equal to |
=gt= |
> |
Greater than |
=ge= |
>= |
Greater than or equal to |
=in= |
In array | |
=out= |
Not in array |
| Operator | Alternative | Description |
|---|---|---|
; |
and |
Logical AND (higher precedence) |
, |
or |
Logical OR (lower precedence) |
- Simple alphanumeric values:
name==John - Values with spaces or special chars:
name=="John Doe" - Escaping quotes:
name=="John \"The Boss\" Doe" - Escaping backslashes:
path=="C:\\Users\\John"
npm test # Run all tests
npm run test:watch # Run in watch modeThe library includes comprehensive tests covering:
- All comparison and logical operators
- Value quoting and escaping
- Complex nested queries
- React component composition patterns
- Type safety verification