Skip to content

V2-Digital/rsqlts

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

rsqlts

A type-safe TypeScript DSL for building RSQL queries with full IntelliSense support.

What is RSQL?

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

Why rsqlts?

  • 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

Installation

Copy and paste index.ts into your code.

Quick Start

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

API Reference

Query Builder

Create a type-safe query builder for your type:

const q = QueryOf<Person>();

Comparison Operators

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>=18

Array Operators

q.inArray('status', ['active', 'pending'])     // status=in=(active,pending)
q.notIn('status', ['inactive'])                // status=out=(inactive)

Logical Operators

// 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==pending

Complex Nested Queries

const 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>=946684800000

React Component Composition

Perfect for building queries from multiple form components:

Low-Level API (Manual AST Construction)

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"
}

High-Level API (Builder Functions)

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

Advanced Patterns

Conditional Filters

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;

Building Queries Incrementally

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'));
}

Multiple Filter Groups (OR)

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)

Type Safety Examples

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 union

RSQL Specification

This library implements the RSQL specification, which is based on FIQL (Feed Item Query Language).

Supported Operators

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

Logical Operators

Operator Alternative Description
; and Logical AND (higher precedence)
, or Logical OR (lower precedence)

Value Quoting

  • 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"

Testing

npm test              # Run all tests
npm run test:watch    # Run in watch mode

The library includes comprehensive tests covering:

  • All comparison and logical operators
  • Value quoting and escaping
  • Complex nested queries
  • React component composition patterns
  • Type safety verification

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors