npm package: @rabstack/rab-query-decorators
Description: Query decorators for rab-api that integrate rab-query with schema validation
Author: apps
Latest version: 0.8.0

# @rabstack/rab-query-decorators

Query decorators that seamlessly integrate [`@rabstack/rab-api`](https://www.npmjs.com/package/@rabstack/rab-api) with [`@rabstack/rab-query`](https://www.npmjs.com/package/@rabstack/rab-query).

Provides the `@Query` decorator with built-in query parameter parsing, validation, and type safety.

## Features

- **Seamless Integration**: Connects rab-api routing with rab-query parsing
- **Type-Safe**: Full TypeScript support with generic types
- **Joi Validation**: Optional schema validation for query parameters
- **Zero Configuration**: Works out of the box with sensible defaults

## Installation

```bash
npm install @rabstack/rab-query-decorators @rabstack/rab-api @rabstack/rab-query joi
```

## Quick Start

```typescript
import { Query } from '@rabstack/rab-query-decorators';
import { AtomApiGet } from '@rabstack/rab-api';
import Joi from 'joi';

@Query('/users', {
  querySchema: Joi.object({
    name: Joi.string().optional(),
    email: Joi.string().email().optional(),
    age: Joi.number().optional(),
  }),
  permission: 'canListUsers',
})
export class ListUsersController implements AtomApiGet {
  async execute(request) {
    // request.query is automatically parsed and validated
    const { page, pageSize, orderBy, ...filters } = request.query;

    return this.userService.findMany({
      where: filters,
      take: pageSize,
      skip: (page - 1) * pageSize,
      orderBy,
    });
  }
}
```

## API Reference

### `Query(path, options?)`

Decorator for GET routes with integrated query parameter parsing.

**Parameters:**
- `path` (string): The route path
- `options` (ControllerRouteDefinitionOptions): Optional configuration
  - `querySchema` (Joi.ObjectSchema): Joi schema for validation
  - `permission` (string): Permission required to access this route
  - ... other rab-api options

**Returns:** ClassDecorator

### `buildQueryValidationSchema(baseSchema, options?)`

Builds a Joi validation schema for query parameters.

**Parameters:**
- `baseSchema` (Joi.ObjectSchema): Your data model schema
- `options` (object): Optional configuration
  - `maxPageSize` (number): Maximum allowed page size (default: 1000)

**Returns:** Joi.ObjectSchema

### `validateQuery(query, baseSchema, options?)`

Validates and transforms query parameters.

**Parameters:**
- `query` (any): Raw query object
- `baseSchema` (Joi.ObjectSchema): Your data model schema
- `options` (object): Optional configuration

**Returns:** `{ value: T, error?: Joi.ValidationError }`

## Usage Examples

### Basic Usage (No Validation)

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

@Query('/products')
export class ListProductsController implements AtomApiGet {
  async execute(request) {
    // Query parameters are automatically parsed
    // URL: /products?name__contains=phone&price__gte=100
    console.log(request.query);
    // {
    //   name: { contains: 'phone', mode: 'insensitive' },
    //   price: { gte: 100 },
    //   page: 1,
    //   pageSize: 10
    // }

    return this.productService.findMany({ where: request.query });
  }
}
```

### With Joi Validation

```typescript
import { Query } from '@rabstack/rab-query-decorators';
import Joi from 'joi';

const userQuerySchema = Joi.object({
  name: Joi.string().max(100),
  email: Joi.string().email(),
  age: Joi.number().min(0).max(150),
  status: Joi.string().valid('active', 'inactive', 'pending'),
  createdAt: Joi.date(),
});

@Query('/users', {
  querySchema: userQuerySchema,
  permission: 'users:read',
})
export class SearchUsersController implements AtomApiGet {
  async execute(request) {
    // Validated and type-safe query
    return this.userService.search(request.query);
  }
}
```

### Nested Object Schema

```typescript
import { Query } from '@rabstack/rab-query-decorators';
import Joi from 'joi';

const orderQuerySchema = Joi.object({
  customer: Joi.object({
    name: Joi.string(),
    email: Joi.string().email(),
  }),
  product: Joi.object({
    name: Joi.string(),
    category: Joi.string(),
    price: Joi.number(),
  }),
  status: Joi.string().valid('pending', 'shipped', 'delivered'),
});

@Query('/orders', { querySchema: orderQuerySchema })
export class ListOrdersController implements AtomApiGet {
  async execute(request) {
    // URL: /orders?customer.name__contains=John&product.price__gte=100
    // Parses to: { customer: { name: { contains: 'John' } }, product: { price: { gte: 100 } } }
    return this.orderService.findMany({ where: request.query });
  }
}
```

### Custom Page Size Limit

```typescript
import { Query, buildQueryValidationSchema } from '@rabstack/rab-query-decorators';
import Joi from 'joi';

const schema = buildQueryValidationSchema(
  Joi.object({
    name: Joi.string(),
  }),
  { maxPageSize: 50 } // Limit to 50 items per page
);

@Query('/items', { querySchema: schema })
export class ListItemsController implements AtomApiGet {
  async execute(request) {
    // pageSize is capped at 50
    return this.itemService.findMany(request.query);
  }
}
```

### Manual Validation

If you need more control, use `validateQuery` directly:

```typescript
import { validateQuery } from '@rabstack/rab-query-decorators';
import Joi from 'joi';

const schema = Joi.object({
  name: Joi.string(),
  age: Joi.number(),
});

const { value, error } = validateQuery(req.query, schema);

if (error) {
  throw new ValidationError(error.message);
}

// Use validated query
const results = await db.findMany({ where: value });
```

## Supported Query Features

All features from `@rabstack/rab-query` are automatically supported:

### Operators
- Comparison: `equals`, `not`, `in`, `notIn`, `lt`, `lte`, `gt`, `gte`
- String: `contains`, `startsWith`, `endsWith`, `search`
- Array: `has`, `hasSome`, `hasEvery`, `isEmpty`
- Null: `isSet`

### Logical Operators
- `OR` - Match any condition
- `AND` - Match all conditions
- `NOT` - Negate conditions

### Pagination
- `page` - Page number (default: 1)
- `pageSize` - Items per page (default: 10, max: 1000)

### Sorting
- `orderBy` - Sort by fields (supports nested fields)

## Query String Format

```
# Basic operator
?name__contains=John

# Nested fields
?user.profile.age__gte=18

# Logical operators
?age__gt__or=18&age__lt__or=65

# Arrays
?status__in=active,pending,approved

# Pagination
?page=2&pageSize=20

# Sorting
?orderBy=name__asc,createdAt__desc
```

## Integration with rab-api

This package decouples `@rabstack/rab-api` from `@rabstack/rab-query`.

**Before** (tightly coupled):
```typescript
// rab-api had direct dependency on rab-query
import { Query } from '@rabstack/rab-api';
```

**After** (decoupled):
```typescript
// rab-api is independent
// rab-query is independent
// query-decorators connects them
import { Query } from '@rabstack/rab-query-decorators';
```

## TypeScript Support

Full type safety with generic types:

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

interface User {
  name: string;
  email: string;
  age: number;
}

@Query('/users')
export class ListUsersController implements AtomApiGet {
  async execute(request: { query: RabQueryParams<User> }) {
    // request.query is fully typed
    return this.userService.findMany(request.query);
  }
}
```

## Best Practices

1. **Always use querySchema**: Validate user input to prevent invalid queries
2. **Set maxPageSize**: Protect your API from excessive page sizes
3. **Use TypeScript**: Leverage type safety for better developer experience
4. **Document your API**: Share supported query parameters with API consumers

## Dependencies

This package requires:
- `@rabstack/rab-api` (peer dependency)
- `@rabstack/rab-query` (peer dependency)
- `joi` (peer dependency)
- `reflect-metadata` (peer dependency)

## Related Packages

- [`@rabstack/rab-api`](https://www.npmjs.com/package/@rabstack/rab-api) - REST API framework
- [`@rabstack/rab-query`](https://www.npmjs.com/package/@rabstack/rab-query) - Query string parser

## License

MIT

## Contributing

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