Skip to content

Using Filters in Your Application

How filters flow through the application layers.

Architecture Overview

+-----------------------------------------------------------------+
|                        HTTP Request                              |
|   GET /products?filter={"where":{"status":"active"},"limit":10} |
+--------------------------------+--------------------------------+
                                 |
                                 v
+-----------------------------------------------------------------+
|                     Controller Layer                             |
|   - Validates filter via Zod schema                              |
|   - Parses JSON string -> Filter object                          |
|   - Passes to service/repository                                 |
+-----------------------------------------------------------------+
                                 |
                                 v
+-----------------------------------------------------------------+
|                     Service Layer (Optional)                     |
|   - Business logic, authorization                                |
|   - May modify filter before passing                             |
+-----------------------------------------------------------------+
                                 |
                                 v
+-----------------------------------------------------------------+
|                     Repository Layer                             |
|   - FilterBuilder transforms Filter -> SQL                       |
|   - Executes query via Drizzle ORM                               |
|   - Returns typed results                                        |
+-----------------------------------------------------------------+

Controller Layer

The ControllerFactory automatically handles filter parsing and validation:

typescript
// src/controllers/product.controller.ts
import { Product } from '@/models';
import { ProductRepository } from '@/repositories';
import {
  controller,
  ControllerFactory,
  inject,
  BindingKeys,
  BindingNamespaces,
} from '@venizia/ignis';

const BASE_PATH = '/products';

const _Controller = ControllerFactory.defineCrudController({
  repository: { name: ProductRepository.name },
  controller: {
    name: 'ProductController',
    basePath: BASE_PATH,
    isStrict: true,
    defaultLimit: 20,
  },
  entity: () => Product,
});

@controller({ path: BASE_PATH })
export class ProductController extends _Controller {
  constructor(
    @inject({
      key: BindingKeys.build({
        namespace: BindingNamespaces.REPOSITORY,
        key: ProductRepository.name,
      }),
    })
    repository: ProductRepository,
  ) {
    super(repository);
  }
}

Generated Endpoints:

MethodEndpointFilter Location
GET/productsQuery param: ?filter={...}
GET/products/:idQuery param: ?filter={...} (for includes)
GET/products/oneQuery param: ?filter={...}
GET/products/countQuery param: ?where={...}

Custom Controller with Manual Filter Handling

typescript
@controller({ path: '/products' })
export class ProductController extends BaseController {
  constructor(
    @inject({ key: 'repositories.ProductRepository' })
    private _productRepo: ProductRepository,
  ) {
    super({ scope: 'ProductController', path: '/products' });
  }

  override binding() {
    this.defineRoute({
      configs: {
        path: '/search',
        method: 'get',
        query: {
          filter: FilterSchema,
        },
      },
      handler: async (context) => {
        const { filter = {} } = context.req.valid('query');
        const results = await this._productRepo.find({ filter });
        return context.json(results);
      },
    });
  }
}

Service Layer

Services can modify filters before passing to repositories:

typescript
@service()
export class ProductService {
  constructor(
    @inject({ key: 'repositories.ProductRepository' })
    private _productRepo: ProductRepository,
  ) {}

  async findProducts(filter: TFilter<TProductSchema> = {}) {
    // Merge user filter with soft-delete condition
    const enhancedFilter: TFilter<TProductSchema> = {
      ...filter,
      where: {
        ...filter.where,
        deletedAt: { is: null },
      },
    };

    return this._productRepo.find({ filter: enhancedFilter });
  }

  async findProductsForTenant(
    tenantId: string,
    filter: TFilter<TProductSchema> = {},
  ) {
    const isolatedFilter: TFilter<TProductSchema> = {
      ...filter,
      where: {
        ...filter.where,
        tenantId,
      },
    };

    return this._productRepo.find({ filter: isolatedFilter });
  }
}

HTTP Request Examples

cURL:

bash
# Simple filter
curl "http://localhost:3000/products?filter=%7B%22where%22%3A%7B%22status%22%3A%22active%22%7D%2C%22limit%22%3A10%7D"

# Decoded filter: {"where":{"status":"active"},"limit":10}

# Complex filter with URL encoding
curl -G "http://localhost:3000/products" \
  --data-urlencode 'filter={"where":{"price":{"gte":100,"lte":500},"tags":{"contains":["featured"]}},"order":["price ASC"],"limit":20}'

JavaScript/TypeScript:

typescript
// Using fetch
const filter = {
  where: { status: 'active', price: { lte: 100 } },
  order: ['createdAt DESC'],
  limit: 10,
};

const response = await fetch(
  `/api/products?filter=${encodeURIComponent(JSON.stringify(filter))}`
);

// Using axios
const response = await axios.get('/api/products', {
  params: { filter: JSON.stringify(filter) },
});

Debugging Filters

typescript
// Enable logging to see generated SQL
const result = await repo.find({
  filter: complexFilter,
  options: {
    log: { use: true, level: 'debug' },
  },
});

// Or use buildQuery to inspect without executing
const queryOptions = repo.buildQuery({ filter: complexFilter });
console.log('Generated query options:', queryOptions);