npm package: @origins-digital/query-filter-core
Description: Common query filter utilities for all projects
Author: Origins Digital
Latest version: 3.1.4

# @origins-digital/query-filter-core

A core package for building type-safe query filters in TypeScript.

> **Zod compatibility**: Zod-based filter schemas are compatible with **Zod v4.x**.

## Installation

```bash
npm install @origins-digital/query-filter-core
```

## Features

- Type-safe query filtering
- Support for complex filter conditions
- Built-in comparison operators
- OpenAPI/Swagger integration
- Flexible filter grouping (AND/OR)
- Support for nested object filtering
- Zod schema validation

## Usage

### Basic Filtering with Zod

```typescript
import { QueryFilter } from '@origins-digital/query-filter-core';
import { z } from 'zod';

// Define your schema with Zod
const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  age: z.number(),
  isActive: z.boolean(),
});

type User = z.infer<typeof UserSchema>;

// Simple equality filter
const filter: QueryFilter<User> = {
  name: { eq: 'John' },
  age: { gt: 18 },
  isActive: { is: true },
};

// Validate filter with Zod
const validateFilter = (filter: unknown) => {
  return UserSchema.parse(filter);
};
```

### Complex Filtering with Zod

```typescript
import { z } from 'zod';
import { QueryFilter } from '@origins-digital/query-filter-core';

// Define nested schemas
const ProfileSchema = z.object({
  bio: z.string(),
  location: z.string(),
});

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  age: z.number(),
  isActive: z.boolean(),
  profile: ProfileSchema,
});

type User = z.infer<typeof UserSchema>;

// Complex filter with nested objects
const filter: QueryFilter<User> = {
  or: [
    { name: { contains: 'John' } },
    {
      and: [
        { age: { gt: 18 } },
        { isActive: { is: true } },
        { profile: { bio: { contains: 'developer' } } },
      ],
    },
  ],
};
```

### Using ApiQuerifyZodSchema and ApiQueryZodSchema

```typescript
import { z } from 'zod';
import {
  ApiQuerifyZodSchema,
  ApiQueryZodSchema,
} from '@origins-digital/query-filter-core';

// Define your entity schema with Zod
const UserSchema = z.object({
  name: z.string().describe('User full name'),
  age: z.number().describe('User age in years'),
  isActive: z.boolean().describe('Whether the user is active'),
  createdAt: z.date().describe('User creation date'),
  profile: z.object({
    bio: z.string().max(500).describe('User biography'),
    location: z.string().describe('User location'),
  }),
});

// Define query parameters schema
const UserQueryParamsSchema = z.object({
  page: z.number().min(1).default(1).describe('Page number'),
  limit: z.number().min(1).max(100).default(10).describe('Items per page'),
  sortBy: z
    .enum(['name', 'age', 'createdAt'])
    .default('createdAt')
    .describe('Field to sort by'),
  sortOrder: z.enum(['asc', 'desc']).default('desc').describe('Sort order'),
});

// Controller usage
import { Controller, Get, Query } from '@nestjs/common';
import { ApiQuery } from '@nestjs/swagger';
import { QueryFilter } from '@origins-digital/query-filter-core';

@Controller('users')
export class UserController {
  @Get()
  @ApiQuerifyZodSchema(UserSchema, {
    description: 'Filter users by various criteria',
    example: {
      name: { contains: 'John' },
      age: { gt: 18 },
      isActive: { is: true },
    },
  })
  async getUsers(
    @Query('filter') filter?: QueryFilter<z.infer<typeof UserSchema>>,
  ) {
    if (filter) {
      // Your implementation here
    }
  }

  @Get('search')
  @ApiQueryZodSchema(UserQueryParamsSchema)
  async searchUsers(@Query() query: z.infer<typeof UserQueryParamsSchema>) {
    // Your implementation here
    // query.page, query.limit, query.sortBy, query.sortOrder are typed
  }
}
```

### Custom Zod Validations

```typescript
import { z } from 'zod';
import { QueryFilter } from '@origins-digital/query-filter-core';

// Define custom validations
const UserSchema = z.object({
  name: z.string().min(2).max(50),
  age: z.number().min(0).max(120),
  email: z.string().email(),
  isActive: z.boolean(),
  createdAt: z.date(),
  profile: z.object({
    bio: z.string().max(500),
    location: z.string(),
  }),
});

// Create filter validation schema
const FilterSchema = z.object({
  name: z
    .object({
      eq: z.string().optional(),
      contains: z.string().optional(),
      startsWith: z.string().optional(),
      endsWith: z.string().optional(),
    })
    .optional(),
  age: z
    .object({
      gt: z.number().optional(),
      lt: z.number().optional(),
      gte: z.number().optional(),
      lte: z.number().optional(),
      in: z.array(z.number()).optional(),
    })
    .optional(),
  isActive: z
    .object({
      is: z.boolean().optional(),
      isNot: z.boolean().optional(),
    })
    .optional(),
  profile: z
    .object({
      bio: z
        .object({
          contains: z.string().optional(),
        })
        .optional(),
    })
    .optional(),
});

// Validate filter
const validateFilter = (filter: unknown) => {
  return FilterSchema.parse(filter);
};
```

## API Reference

### QueryFilter

```typescript
type QueryFilter<T> = {
  and?: QueryFilter<T>[];
  or?: QueryFilter<T>[];
  [K in keyof T]?: FilterFieldComparison<T[K]>;
};
```

### FilterFieldComparison

```typescript
type FilterFieldComparison<FieldType> = {
  // Common operators
  eq?: FieldType;
  neq?: FieldType;
  gt?: FieldType;
  gte?: FieldType;
  lt?: FieldType;
  lte?: FieldType;
  in?: FieldType[];
  notIn?: FieldType[];
  is?: FieldType | null;
  isNot?: FieldType | null;

  // String specific operators
  contains?: string;
  startsWith?: string;
  endsWith?: string;
  mode?: 'default' | 'insensitive';
};
```

## Filter Operators

### Common Operators

- `eq`: Equal to
- `neq`: Not equal to
- `gt`: Greater than
- `gte`: Greater than or equal to
- `lt`: Less than
- `lte`: Less than or equal to
- `in`: In array
- `notIn`: Not in array
- `is`: Is (for boolean and null)
- `isNot`: Is not (for boolean and null)

### String Operators

- `contains`: Contains substring
- `startsWith`: Starts with
- `endsWith`: Ends with
- `mode`: Case sensitivity ('default' or 'insensitive')

## Contributing

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