npm package: @rabstack/rab-query
Description: Type-safe, bidirectional query string parser for building REST APIs with rich filtering capabilities
Author: Softin
Latest version: 0.6.0

# RabQuery

Type-safe, bidirectional query string parser for building REST APIs with rich filtering capabilities.

Parse query strings into typed objects (Server) and encode objects into query strings (Client) with operator syntax inspired by Prisma, supporting nested objects and logical conditions.

## Features

- **Bidirectional**: Client → Query String ↔ Server Object
- **Type-Safe**: Full TypeScript support with generics
- **Rich Operators**: 19+ operators with syntax inspired by Prisma
- **Nested Objects**: Deep filtering with dot notation (`user.profile.age`)
- **Logical Operators**: `OR`, `AND`, `NOT` conditions
- **Sorting**: Nested `orderBy` support
- **Zero Dependencies**: Lightweight and fast
- **Framework Agnostic**: Works with any Node.js framework or database

## Installation

```bash
npm install @rabstack/rab-query
# or
yarn add @rabstack/rab-query
# or
pnpm add @rabstack/rab-query
```

## Quick Start

### Client Side (Build Query Strings)

```typescript
import { encodeRabQuery } from '@rabstack/rab-query';

// Build a query string from an object
const queryString = encodeRabQuery({
  name: { contains: 'John' },
  age: { gte: 18 },
  status: { in: ['active', 'pending'] },
  orderBy: [{ createdAt: 'desc' }]
});

// Result: "name__contains=John&age__gte=18&status__in=active,pending&orderBy=createdAt__desc"
```

### Server Side (Parse Query Strings)

```typescript
import { buildQueryParameters } from '@rabstack/rab-query';

// Express.js example
app.get('/users', (req, res) => {
  const params = buildQueryParameters(req.query);

  // Use with Prisma
  const users = await prisma.user.findMany({
    where: params,
    orderBy: params.orderBy
  });

  res.json(users);
});
```

**Note**: `rab-query` focuses on filtering and sorting. Pagination (`page`, `pageSize`) is intentionally not included to keep the library generic - implement pagination according to your specific needs.

## API Reference

### Core Functions

#### `buildQueryParameters<T>(filters: Record<string, any>): RabQueryParams<T>`

Parses URL query parameters into a typed object suitable for database queries.

**Server-side usage**: Parse incoming request query strings.

```typescript
// URL: ?name__contains=john&age__gte=18
// Express automatically parses to: req.query = { name__contains: 'john', age__gte: '18' }

const params = buildQueryParameters(req.query);

// Result:
{
  name: { contains: 'john', mode: 'insensitive' },
  age: { gte: 18 }
}
```

#### `encodeRabQuery<T>(query: RabQueryParams<T>): string`

Encodes a typed object into a URL query string.

**Client-side usage**: Build query strings for API requests.

```typescript
const queryString = encodeRabQuery({
  name: { contains: 'John' },
  age: { gte: 18 }
});

// Output: "name__contains=John&age__gte=18"
```

### Supported Operators

Operator syntax inspired by Prisma:

#### Comparison Operators
- `equals` - Exact match
- `not` - Not equal
- `in` - Match any value in array
- `notIn` - Match none of the values in array
- `lt` - Less than
- `lte` - Less than or equal
- `gt` - Greater than
- `gte` - Greater than or equal

#### String Operators
- `contains` - Contains substring (case-insensitive)
- `startsWith` - Starts with substring
- `endsWith` - Ends with substring
- `search` - Full-text search

#### Array/List Operators
- `has` - Array contains value
- `hasSome` - Array contains some values
- `hasEvery` - Array contains all values
- `isEmpty` - Array is empty

#### Null Checks
- `isSet` - Field is set (not null)

## Usage Examples

### Basic Filtering

```typescript
// Simple equality
const params = buildQueryParameters({ name: 'John' });
// → { name: 'John' }

// With operator
const params = buildQueryParameters({ 'age__gte': '18' });
// → { age: { gte: 18 } }
```

### Nested Objects

Use dot notation for deep filtering:

```typescript
// Client side
const queryString = encodeRabQuery({
  user: {
    profile: {
      age: { gte: 18 }
    }
  }
});
// → "user.profile.age__gte=18"

// Server side
const params = buildQueryParameters({ 'user.profile.age__gte': '18' });
// → { user: { profile: { age: { gte: 18 } } } }
```

### Array Operators

```typescript
// IN operator (comma-separated values)
const params = buildQueryParameters({ 'status__in': 'active,pending' });
// → { status: { in: ['active', 'pending'] } }

// Array contains
const params = buildQueryParameters({ 'tags__hasSome': 'javascript,typescript' });
// → { tags: { hasSome: ['javascript', 'typescript'] } }
```

### Logical Operators (OR, AND, NOT)

```typescript
// OR condition - append __or to any operator
const params = buildQueryParameters({
  'age__gt__or': '18',
  'age__lt__or': '65'
});
// → { OR: [{ age: { gt: 18 } }, { age: { lt: 65 } }] }

// Client side - build OR queries
const queryString = encodeRabQuery({
  OR: [
    { age: { gt: 18 } },
    { status: 'premium' }
  ]
});
// → "age__gt__or=18&status__or=premium"
```

### Sorting (OrderBy)

```typescript
// Single field
const params = buildQueryParameters({ 'orderBy': 'createdAt__desc' });
// → { orderBy: [{ createdAt: 'desc' }] }

// Multiple fields (comma-separated)
const params = buildQueryParameters({ 'orderBy': 'name__asc,createdAt__desc' });
// → { orderBy: [{ name: 'asc' }, { createdAt: 'desc' }] }

// Nested fields
const params = buildQueryParameters({ 'orderBy': 'user.profile.age__desc' });
// → { orderBy: [{ user: { profile: { age: 'desc' } } }] }

// Client side - array format (multiple fields)
const queryString = encodeRabQuery({
  orderBy: [
    { name: 'asc' },
    { createdAt: 'desc' }
  ]
});
// → "orderBy=name__asc,createdAt__desc"

// Client side - single object format (one field)
const queryString = encodeRabQuery({
  orderBy: { name: 'asc' }
});
// → "orderBy=name__asc"

// Client side - single object with multiple fields (NEW!)
const queryString = encodeRabQuery({
  orderBy: { name: 'asc', createdAt: 'desc' }
});
// → "orderBy=name__asc,createdAt__desc"

// All three formats work!
// Note: For critical sort priority, array format is recommended for explicit ordering
```

### Complete Example

```typescript
// Client builds complex query
const queryString = encodeRabQuery({
  name: { contains: 'John' },
  age: { gte: 18, lte: 65 },
  status: { in: ['active', 'pending'] },
  user: {
    profile: {
      verified: true
    }
  },
  OR: [
    { role: 'admin' },
    { credits: { gt: 100 } }
  ],
  orderBy: [{ createdAt: 'desc' }]
});

// Server parses and uses with database
app.get('/users', async (req, res) => {
  const params = buildQueryParameters(req.query);

  // Implement your own pagination
  const page = Number(req.query.page) || 1;
  const pageSize = Math.min(Number(req.query.pageSize) || 10, 100);

  const users = await prisma.user.findMany({
    where: params,
    take: pageSize,
    skip: (page - 1) * pageSize,
    orderBy: params.orderBy
  });

  res.json(users);
});
```

## TypeScript Support

RabQuery is fully typed with generics for your data models:

```typescript
interface User {
  id: string;
  name: string;
  age: number;
  email: string;
  profile: {
    bio: string;
    verified: boolean;
  };
}

// Type-safe query building
const params: RabQueryParams<User> = buildQueryParameters(req.query);

// Type-safe encoding
const queryString = encodeRabQuery<User>({
  name: { contains: 'John' },
  age: { gte: 18 },
  profile: {
    verified: true
  }
});

// Autocomplete and type checking!
```

### Core Types

```typescript
import type {
  RabQueryParams,      // Main query type
  BaseRabQuery,        // Base query type with orderBy
  RabQueryFilters,     // Filter types
  OrderByInput,        // OrderBy type
  FieldOperators,      // Available operators per field
  SupportedOperator,   // All operator names
  SortOrder,           // 'asc' | 'desc'
} from '@rabstack/rab-query';
```

## Query String Format

RabQuery uses a specific format for query strings:

### Basic Format
```
field__operator=value
```

### Nested Objects (dot notation)
```
user.profile.age__gte=18
```

### Conditions (suffix)
```
age__gt__or=18
status__equals__and=active
```

### Arrays (comma-separated)
```
status__in=active,pending,approved
tags__hasSome=javascript,typescript
```

### Multiple Values (same key)
```
?age__gt__or=18&age__lt__or=65
// Results in: OR: [{ age: { gt: 18 } }, { age: { lt: 65 } }]
```

## Advanced Usage

### Utility Functions

```typescript
import {
  parseQueryKey,
  isSupportedOperator,
  isArrayOperator,
  isCaseInsensitiveOperator,
  supportedOperators
} from '@rabstack/rab-query';

// Parse individual query keys
const parsed = parseQueryKey('user.age__gte__or');
// → { path: ['user', 'age'], modifier: { operator: 'gte', condition: 'or' } }

// Check operator types
isSupportedOperator('contains'); // true
isArrayOperator('in'); // true
isCaseInsensitiveOperator('contains'); // true
```

### Custom Validation

```typescript
import { buildQueryParameters, RabQueryParams } from '@rabstack/rab-query';

function validateAndParse<T>(query: any): RabQueryParams<T> {
  const params = buildQueryParameters<T>(query);

  // Add custom validation as needed
  // Example: Validate specific fields or business rules
  if (params.age && typeof params.age === 'object' && 'gte' in params.age) {
    if (params.age.gte < 0) {
      throw new Error('Age cannot be negative');
    }
  }

  return params;
}
```

## Framework Integration

### Express.js

```typescript
import express from 'express';
import { buildQueryParameters } from '@rabstack/rab-query';

const app = express();

app.get('/api/users', async (req, res) => {
  const params = buildQueryParameters(req.query);
  const users = await db.users.find(params);
  res.json(users);
});
```

### NestJS

```typescript
import { Controller, Get, Query } from '@nestjs/common';
import { buildQueryParameters } from '@rabstack/rab-query';

@Controller('users')
export class UsersController {
  @Get()
  async findAll(@Query() query: any) {
    const params = buildQueryParameters(query);
    return this.usersService.findAll(params);
  }
}
```

### Fastify

```typescript
import Fastify from 'fastify';
import { buildQueryParameters } from '@rabstack/rab-query';

const fastify = Fastify();

fastify.get('/users', async (request, reply) => {
  const params = buildQueryParameters(request.query);
  return db.users.find(params);
});
```

### React Query (Client)

```typescript
import { useQuery } from '@tanstack/react-query';
import { encodeRabQuery } from '@rabstack/rab-query';

function useUsers(filters: RabQueryParams<User>) {
  return useQuery({
    queryKey: ['users', filters],
    queryFn: async () => {
      const queryString = encodeRabQuery(filters);
      const response = await fetch(`/api/users?${queryString}`);
      return response.json();
    }
  });
}

// Usage
const { data } = useUsers({
  name: { contains: 'John' },
  age: { gte: 18 }
});
```

## Best Practices

1. **Always validate input**: RabQuery parses query strings but doesn't validate business rules
2. **Implement pagination separately**: Add `page` and `pageSize` handling according to your needs
3. **Use TypeScript**: Leverage generic types for better type safety
4. **Sanitize user input**: While RabQuery is safe from injection, always validate data
5. **Document your API**: Share the query format with your API consumers

## Roadmap

### Future Enhancements

- **Elasticsearch Adapter**: Transform RabQuery output to Elasticsearch Query DSL
  - Convert Prisma-style operators to Elasticsearch format
  - Map `contains` → `match`, `gte`/`lte` → `range`, `in` → `terms`
  - Transform `OR`/`AND`/`NOT` to `bool` queries (`should`/`must`/`must_not`)
  - Maintain type safety and full query compatibility

## Migration from V1

RabQuery V2 is backward compatible with V1. The main differences:

- New nested object support via dot notation
- Extended operator set (19 vs 8 operators)
- Improved TypeScript types
- New `encodeRabQuery` function for client-side

Existing V1 queries will continue to work without changes.

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## License

MIT