adonis-lucid-filter

repository·master·Indexed 41 days ago

https://github.com/lookinlab/adonis-lucid-filter

An addon for AdonisJS (v6 compatible) that provides a structured way to filter Lucid ORM models using query parameters. It simplifies complex conditional query building by mapping request input to dedicated filter classes, supporting features like method mapping, blacklisting, and dynamic filters. Version 5.2.0 requires @adonisjs/lucid ^20.*.*.

Tokens
5.3K
Snippets
20
Records
23
Agent score
100%

What's inside adonis-lucid-filter

  1. Blacklist and Whitelist filter methods

    master

    Use the static blacklist array to prevent specific methods from being called by the filter engine. This is useful for internal logic methods that shouldn't be exposed to query parameters.

    You can dynamically whitelist a blacklisted method inside the setup() method using this.whitelistMethod('methodName').

    export default class UserFilter extends BaseModelFilter {
      static blacklist: string[] = ['secretMethod']
    
      setup($query) {
        // Dynamically allow 'secretMethod' to be called if it exists in input
        this.whitelistMethod('secretMethod')
        this.$query.where('is_admin', true)
      }
    
      secretMethod(secretParameter: any) {
        this.$query.where('some_column', true)
      }
    }
  2. Install adonis-lucid-filter

    master

    Install the package using your preferred package manager, then run the configuration command to complete the setup.

    Compatibility Note:

    • adonis-lucid-filter ^5.. requires @adonisjs/lucid ^20..
    • adonis-lucid-filter ^4.. requires @adonisjs/lucid <=18..
    • adonis-lucid-filter ^3.. requires @adonisjs/lucid ^15..
    # npm
    npm i adonis-lucid-filter
    
    # yarn
    yarn add adonis-lucid-filter
    
    # pnpm
    pnpm add adonis-lucid-filter
    
    # After install, run configuration
    node ace configure adonis-lucid-filter
  3. Register the provider and commands in adonisrc.ts

    master

    To ensure the addon works correctly, you must register the provider and the commands in your adonisrc.ts file.

    providers: [
      // ...
      () => import('adonis-lucid-filter/provider'),
    ],
    commands: [
      // ...
      () => import('adonis-lucid-filter/commands')
    ]
  4. Understand the InputObject type structure

    master

    The InputObject<Instance> type represents the shape of the data passed into a filter. It is automatically derived from your LucidFilter implementation using several transformations:

    1. Key Picking: It picks keys from your filter methods (excluding internal LucidFilter methods and the constructor).
    2. Snake Case Conversion: Keys are converted to snake_case.
    3. ID Suffixing: If a method's return type is number, the library automatically generates a corresponding key with an _id suffix (e.g., user_id).
    4. CamelCase Support: The type supports both snake_case and camelCase versions of the keys to allow flexibility in how API clients send data.

    This ensures that your TypeScript types remain in sync with the dynamic filtering logic applied at runtime.

  5. Define filter logic in a BaseModelFilter class

    master

    Create a class that extends BaseModelFilter. The filter logic is driven by the keys provided in the input object (e.g., from request.qs()).

    Key Concepts:

    • Method Mapping: Input keys are mapped to methods in the class. For example, an input key companyId will call the company(id: number) method.
    • $query: Access the Lucid QueryBuilder via this.$query.
    • $input: Access the full input object via this.$input.
    • setup(): A method called on every filter execution. Use it for mandatory filters (e.g., this.$query.where('is_active', true)).
    • Empty Strings: Input values that are empty strings are automatically ignored.
    • _id Suffix: If static dropId = true is set, user_id in the input will call the user() method.
    • camelCase: If static camelCase = true is set, mobile_phone or mobilePhone will both call the mobilePhone() method.
    import { BaseModelFilter } from 'adonis-lucid-filter'
    import type { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model'
    import User from '#models/user'
    
    export default class UserFilter extends BaseModelFilter {
      declare $query: ModelQueryBuilderContract<typeof User>
      
      static blacklist: string[] = ['secretMethod']
      static dropId: boolean = true
      static camelCase: boolean = true
    
      // Filters 'companyId', 'company_id', or 'company'
      company(id: number) {
        this.$query.where('company_id', id)
      }
    
      name(name: string) {
        this.$query.where((builder) => {
          builder
            .where('first_name', 'LIKE', `%${name}%`)
            .orWhere('last_name', `%${name}%`)
        })
      }
    
      mobilePhone(phone: string) {
        this.$query.where('mobile_phone', 'LIKE', `${phone}%`)
      }
    
      secretMethod(secretParameter: any) {
        this.$query.where('some_column', true)
      }
    }
  6. Use dynamic filters

    master

    You can override the model's default filter by passing a specific filter class as the second argument to the .filter() method. This is useful for conditional logic, such as applying different filters based on user permissions.

    import AdminFilter from '#models/filters/admin_filter'
    import UserFilter from '#models/filters/user_filter'
    
    export default class UsersController {
      async index({ request, auth }: HttpContext): Promise<User[]> {
        const filter = auth.user.isAdmin() ? AdminFilter : UserFilter
        return User.filter(request.qs(), filter).exec()
      }
    }
  7. Apply a filter to a Lucid Model

    master

    To enable filtering on a model, extend it using compose(BaseModel, Filterable) and define the $filter static property to return your filter class.

    Once applied, you can call .filter(input) on the model to execute the query.

    import UserFilter from '#models/filters/user_filter'
    import { compose } from '@adonisjs/core/helpers'
    import { Filterable } from 'adonis-lucid-filter'
    import { BaseModel } from '@adonisjs/lucid/orm'
    
    export default class User extends compose(BaseModel, Filterable) {
      static $filter = () => UserFilter
    }
    
    // Usage in a Controller
    export default class UsersController {
      async index({ request }: HttpContext): Promise<User[]> {
        // Standard filtering
        return User.filter(request.qs()).exec()
      }
    
      async paginate({ request }: HttpContext): Promise<ModelPaginatorContract<User>> {
        const { page = 1, ...input } = request.qs()
        return User.filter(input).paginate(page, 15)
      }
    }
  8. Filter model relations

    master

    To filter a related model, ensure the related model is also Filterable and has its $filter property defined. You can then use .filter() directly on the relation query or use the filtration scope.

    // Using .filter() directly
    async index({ params, request }: HttpContext): Promise<Post[]> {
      const user: User = await User.findOrFail(params.user_id)
      return user.related('posts').query().filter(request.qs()).exec()
    }
    
    // Using the filtration scope
    async index({ params, request }: HttpContext): Promise<Post[]> {
      const user: User = await User.findOrFail(params.user_id)
      return user.related('posts').query()
        .apply(scopes => scopes.filtration(request.qs()))
        .exec()
    }
  9. Add filtering capabilities to Lucid models using the Filterable mixin

    master

    To enable filtering on an AdonisJS Lucid model, wrap your model class with the Filterable mixin. This adds two primary static methods to your model: filter() for direct query building and filtration as a Lucid Query Scope.

    Once applied, your model must implement a static $filter() method that returns an instance of a class implementing the LucidFilterContract.

    import { BaseModel } from '@adonisjs/lucid/orm'
    import { Filterable } from 'adonis-lucid-filter'
    
    class MyModel extends Filterable(BaseModel) {
      // You must implement $filter to define your filtering logic
      static $filter = () => new MyFilter()
    }
  10. Configure a LucidFilterContract

    master

    The LucidFilterContract defines the static configuration and constructor requirements for a filter class. When implementing a filter, your class must adhere to this contract to be compatible with the library's automation.

    Configuration options:

    • blacklist: An array of strings representing method names that should not be used for filtering.
    • dropId: A boolean indicating whether the id field should be excluded from the filterable input.
    • camelCase: A boolean indicating whether input keys should be automatically converted to camelCase.
    • removeEmptyInput(input): A method to clean the input object (e.g., removing null or empty values).

    The constructor must accept a ModelQueryBuilderContract and an object representing the input.

    export interface LucidFilterContract {
      blacklist: string[]
      dropId: boolean
      camelCase: boolean
      removeEmptyInput(input: object): object
      new ($query: ModelQueryBuilderContract<LucidModel, LucidRow>, $input: object): LucidFilter
    }
  11. Extend BaseModelFilter to implement custom filter logic

    master

    To create a custom filter for an AdonisJS Lucid model, extend the BaseModelFilter class. This class provides the core engine for mapping input keys to specific filtering methods on your class.

    Key Behaviors:

    • Input Cleaning: The constructor automatically removes empty values ('', null, or undefined) from the $input object.
    • Method Mapping: Input keys are automatically transformed into method names based on static configuration:
      • If static camelCase is true (default), a key like user_name becomes the method userName.
      • If static dropId is true (default), keys ending in _id or Id have that suffix removed before camelCasing (e.g., user_id becomes user).
    • Lifecycle: When calling .handle(), the class first executes the setup($query) method (if defined) and then iterates through the $input to call matching methods.
    • Blacklisting: You can prevent certain methods from being called by input keys by defining a static blacklist: string[].
    import BaseModelFilter from 'adonis-lucid-filter/base_model'
    
    export default class UserFilter extends BaseModelFilter {
      // Prevent specific methods from being triggered by input keys
      static blacklist = ['someInternalMethod']
    
      // Optional: Run logic before input-based filtering
      setup($query: any) {
        $query.where('is_active', true)
      }
    
      // This method will be called if the input contains { name: '...' }
      name(value: string) {
        this.$query.where('name', 'like', `%${value}%`)
      }
    
      // This method will be called if the input contains { user_id: 123 }
      // because dropId is true by default
      user(value: number) {
        this.$query.where('user_id', value)
      }
    }