add-query-filter

Add custom query parameter filters to entity endpoints. Use when extending search/filter capabilities beyond the base pagination. Triggers on "add filter", "query parameter", "search filter", "filter by".

181 stars

Best use case

add-query-filter is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Add custom query parameter filters to entity endpoints. Use when extending search/filter capabilities beyond the base pagination. Triggers on "add filter", "query parameter", "search filter", "filter by".

Teams using add-query-filter should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/add-query-filter/SKILL.md --create-dirs "https://raw.githubusercontent.com/majiayu000/claude-skill-registry/main/skills/data/add-query-filter/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/add-query-filter/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How add-query-filter Compares

Feature / Agentadd-query-filterStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Add custom query parameter filters to entity endpoints. Use when extending search/filter capabilities beyond the base pagination. Triggers on "add filter", "query parameter", "search filter", "filter by".

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Add Query Filter

Adds custom query parameter filters to entity schemas and repository implementations.

## Quick Reference

**Files to modify**:

1. `src/schemas/{entity}.schema.ts` - Add filter to query params schema
2. `src/repositories/mockdb/{entity}.mockdb.repository.ts` - Implement filter logic
3. `src/repositories/mongodb/{entity}.mongodb.repository.ts` - Implement filter logic
4. `tests/schemas/{entity}.schema.test.ts` - Test new filter
5. `tests/repositories/{entity}.*.repository.test.ts` - Test filter behavior

## Prerequisites

- Entity schema exists with `{entity}QueryParamsSchema`
- Repository implementations exist

## Instructions

### Step 1: Add Filter to Query Params Schema

Update `src/schemas/{entity}.schema.ts`:

```typescript
import { z } from "zod";
import { queryParamsSchema } from "@/schemas/shared.schema";

// Extend the base query params with entity-specific filters
export const {entity}QueryParamsSchema = queryParamsSchema.extend({
  // Existing filters...
  createdBy: z.string().optional(),

  // Add new filter
  {filterName}: z.string().optional(),
  // OR for enum filter
  status: z.enum(["draft", "published", "archived"]).optional(),
  // OR for boolean filter (from query string)
  isActive: z.coerce.boolean().optional(),
  // OR for date range
  createdAfter: z.coerce.date().optional(),
  createdBefore: z.coerce.date().optional(),
  // OR for numeric range
  minPrice: z.coerce.number().optional(),
  maxPrice: z.coerce.number().optional(),
});

export type {Entity}QueryParamsType = z.infer<typeof {entity}QueryParamsSchema>;
```

### Step 2: Implement Filter in MockDB Repository

Update `src/repositories/mockdb/{entity}.mockdb.repository.ts`:

```typescript
async findAll(query: {Entity}QueryParamsType): Promise<PaginatedResultType<{Entity}Type>> {
  let filtered = [...this.{entities}];

  // Existing filters
  if (query.createdBy) {
    filtered = filtered.filter((item) => item.createdBy === query.createdBy);
  }

  if (query.search) {
    const searchLower = query.search.toLowerCase();
    filtered = filtered.filter((item) =>
      item.content.toLowerCase().includes(searchLower)
    );
  }

  // NEW: Add your filter
  if (query.{filterName}) {
    filtered = filtered.filter((item) => item.{fieldName} === query.{filterName});
  }

  // For enum filter
  if (query.status) {
    filtered = filtered.filter((item) => item.status === query.status);
  }

  // For boolean filter
  if (query.isActive !== undefined) {
    filtered = filtered.filter((item) => item.isActive === query.isActive);
  }

  // For date range
  if (query.createdAfter) {
    filtered = filtered.filter((item) => item.createdAt >= query.createdAfter!);
  }
  if (query.createdBefore) {
    filtered = filtered.filter((item) => item.createdAt <= query.createdBefore!);
  }

  // For numeric range
  if (query.minPrice !== undefined) {
    filtered = filtered.filter((item) => item.price >= query.minPrice!);
  }
  if (query.maxPrice !== undefined) {
    filtered = filtered.filter((item) => item.price <= query.maxPrice!);
  }

  // ... sorting and pagination continue as before
}
```

### Step 3: Implement Filter in MongoDB Repository

Update `src/repositories/mongodb/{entity}.mongodb.repository.ts`:

```typescript
async findAll(query: {Entity}QueryParamsType): Promise<PaginatedResultType<{Entity}Type>> {
  const collection = await this.getCollection();
  const filter: Filter<{Entity}Document> = {};

  // Existing filters
  if (query.createdBy) {
    filter.createdBy = query.createdBy;
  }

  if (query.search) {
    filter.$text = { $search: query.search };
  }

  // NEW: Add your filter
  if (query.{filterName}) {
    filter.{fieldName} = query.{filterName};
  }

  // For enum filter
  if (query.status) {
    filter.status = query.status;
  }

  // For boolean filter
  if (query.isActive !== undefined) {
    filter.isActive = query.isActive;
  }

  // For date range
  if (query.createdAfter || query.createdBefore) {
    filter.createdAt = {};
    if (query.createdAfter) {
      filter.createdAt.$gte = query.createdAfter;
    }
    if (query.createdBefore) {
      filter.createdAt.$lte = query.createdBefore;
    }
  }

  // For numeric range
  if (query.minPrice !== undefined || query.maxPrice !== undefined) {
    filter.price = {};
    if (query.minPrice !== undefined) {
      filter.price.$gte = query.minPrice;
    }
    if (query.maxPrice !== undefined) {
      filter.price.$lte = query.maxPrice;
    }
  }

  // ... continue with sorting, pagination
}
```

### Step 4: Add Schema Tests

Update `tests/schemas/{entity}.schema.test.ts`:

```typescript
describe("{entity}QueryParamsSchema", () => {
  // ... existing tests ...

  it("accepts {filterName} filter", () => {
    const parsed = {entity}QueryParamsSchema.parse({
      {filterName}: "filter-value",
    });
    expect(parsed.{filterName}).toBe("filter-value");
  });

  // For enum filter
  it("accepts valid status values", () => {
    expect({entity}QueryParamsSchema.parse({ status: "draft" }).status).toBe("draft");
    expect({entity}QueryParamsSchema.parse({ status: "published" }).status).toBe("published");
  });

  it("rejects invalid status values", () => {
    expect(() => {entity}QueryParamsSchema.parse({ status: "invalid" })).toThrow();
  });

  // For boolean filter
  it("coerces isActive to boolean", () => {
    expect({entity}QueryParamsSchema.parse({ isActive: "true" }).isActive).toBe(true);
    expect({entity}QueryParamsSchema.parse({ isActive: "false" }).isActive).toBe(false);
  });

  // For date filter
  it("coerces date strings to Date objects", () => {
    const parsed = {entity}QueryParamsSchema.parse({
      createdAfter: "2024-01-01",
    });
    expect(parsed.createdAfter).toBeInstanceOf(Date);
  });

  // For numeric filter
  it("coerces price filters to numbers", () => {
    const parsed = {entity}QueryParamsSchema.parse({
      minPrice: "10",
      maxPrice: "100",
    });
    expect(parsed.minPrice).toBe(10);
    expect(parsed.maxPrice).toBe(100);
  });
});
```

### Step 5: Add Repository Tests

Update repository test files:

```typescript
describe("findAll", () => {
  // ... existing tests ...

  it("filters by {filterName}", async () => {
    await repo.create({ content: "A", {fieldName}: "value1" }, userId);
    await repo.create({ content: "B", {fieldName}: "value2" }, userId);
    await repo.create({ content: "C", {fieldName}: "value1" }, userId);

    const result = await repo.findAll({ {filterName}: "value1" });

    expect(result.data.length).toBe(2);
    expect(result.data.every((item) => item.{fieldName} === "value1")).toBe(true);
  });

  // For enum filter
  it("filters by status", async () => {
    await repo.create({ content: "A", status: "draft" }, userId);
    await repo.create({ content: "B", status: "published" }, userId);

    const result = await repo.findAll({ status: "published" });

    expect(result.data.length).toBe(1);
    expect(result.data[0].status).toBe("published");
  });

  // For date range
  it("filters by date range", async () => {
    // Create items with different dates
    const old = await repo.create({ content: "Old" }, userId);
    await new Promise((r) => setTimeout(r, 10));
    const recent = await repo.create({ content: "Recent" }, userId);

    const result = await repo.findAll({
      createdAfter: old.createdAt,
    });

    expect(result.data.length).toBeGreaterThanOrEqual(1);
  });

  // For numeric range
  it("filters by price range", async () => {
    await repo.create({ content: "Cheap", price: 10 }, userId);
    await repo.create({ content: "Mid", price: 50 }, userId);
    await repo.create({ content: "Expensive", price: 100 }, userId);

    const result = await repo.findAll({ minPrice: 20, maxPrice: 80 });

    expect(result.data.length).toBe(1);
    expect(result.data[0].content).toBe("Mid");
  });
});
```

## Common Filter Patterns

### String Exact Match

```typescript
// Schema
categoryId: z.string().optional(),

// MockDB
if (query.categoryId) {
  filtered = filtered.filter((item) => item.categoryId === query.categoryId);
}

// MongoDB
if (query.categoryId) {
  filter.categoryId = query.categoryId;
}
```

### String Array (IN query)

```typescript
// Schema
tags: z.array(z.string()).optional(),
// OR from comma-separated string
tags: z.string().transform(s => s.split(",")).optional(),

// MockDB
if (query.tags?.length) {
  filtered = filtered.filter((item) =>
    query.tags!.some(tag => item.tags.includes(tag))
  );
}

// MongoDB
if (query.tags?.length) {
  filter.tags = { $in: query.tags };
}
```

### Partial Text Match

```typescript
// Schema
title: z.string().optional(),

// MockDB
if (query.title) {
  const titleLower = query.title.toLowerCase();
  filtered = filtered.filter((item) =>
    item.title.toLowerCase().includes(titleLower)
  );
}

// MongoDB (requires text index for $text, or use regex)
if (query.title) {
  filter.title = { $regex: query.title, $options: "i" };
}
```

### Null/Not Null Check

```typescript
// Schema
hasParent: z.coerce.boolean().optional(),

// MockDB
if (query.hasParent !== undefined) {
  if (query.hasParent) {
    filtered = filtered.filter((item) => item.parentId != null);
  } else {
    filtered = filtered.filter((item) => item.parentId == null);
  }
}

// MongoDB
if (query.hasParent !== undefined) {
  if (query.hasParent) {
    filter.parentId = { $ne: null };
  } else {
    filter.parentId = null;
  }
}
```

## Adding MongoDB Indexes for Filters

If a filter is frequently used, add an index:

```typescript
// In MongoDB repository constructor or initialization
async ensureIndexes() {
  const collection = await this.getCollection();

  // Single field index
  await collection.createIndex({ status: 1 });

  // Compound index for common filter combinations
  await collection.createIndex({ createdBy: 1, status: 1 });

  // For date range queries
  await collection.createIndex({ createdAt: -1 });
}
```

## Filter Validation Tips

### Coercion for Query Strings

Query parameters are always strings. Use `z.coerce` for non-string types:

```typescript
// Numbers
minPrice: z.coerce.number().optional(),

// Booleans
isActive: z.coerce.boolean().optional(),

// Dates
createdAfter: z.coerce.date().optional(),
```

### Default Values

```typescript
// With default
status: z.enum(["all", "active", "inactive"]).default("all"),

// Optional without default
status: z.enum(["draft", "published"]).optional(),
```

### Validation Constraints

```typescript
// Positive numbers only
minPrice: z.coerce.number().positive().optional(),

// Max length
search: z.string().max(100).optional(),

// Future dates only
eventDate: z.coerce.date().min(new Date()).optional(),
```

## What NOT to Do

- Do NOT forget to add the filter to both MockDB and MongoDB implementations
- Do NOT skip coercion for query string values
- Do NOT use complex filters without indexes in MongoDB
- Do NOT forget to test the filter behavior
- Do NOT filter on fields that don't exist in the schema

## See Also

- `create-schema` - Creating entity schemas
- `create-mockdb-repository` - MockDB repository implementation
- `create-mongodb-repository` - MongoDB repository implementation
- `test-schema` - Testing schema validation
- `test-mockdb-repository` - Testing repository filters

Related Skills

ActiveRecord Query Patterns

181
from majiayu000/claude-skill-registry

Complete guide to ActiveRecord query optimization, associations, scopes, and PostgreSQL-specific patterns. Use this skill when writing database queries, designing model associations, creating migrations, optimizing query performance, or debugging N+1 queries and grouping errors.

acc-create-query

181
from majiayu000/claude-skill-registry

Generates CQRS Queries and Handlers for PHP 8.5. Creates read-only query DTOs with handlers that return data without side effects. Includes unit tests.

content-filter

181
from majiayu000/claude-skill-registry

Filter and classify AI research content for relevance. Use when processing raw content from Twitter, Substacks, blogs, or podcasts to determine if it's worth extracting claims from. Assigns relevance scores, topics, and author categories.

add-multi-filter

174
from majiayu000/claude-skill-registry

为Flutter插件视图添加多条件过滤功能(MultiFilterBar),支持标签、关键词、日期、优先级、复选框等多种过滤类型。替代传统的FilterDialog,提供更直观的两层级交互UI。适用场景:(1) 列表视图需要多维度筛选,(2) 数据量较大需要快速过滤,(3) 需要实时显示过滤条件的应用

ux

159
from majiayu000/claude-skill-registry

This AI agent skill provides comprehensive guidance for creating professional and insightful User Experience (UX) designs, covering user research, information architecture, interaction design, visual guidance, and usability evaluation. It aims to produce actionable, user-centered solutions that avoid generic AI aesthetics.

UX Design & StrategyClaude

tech-blog

159
from majiayu000/claude-skill-registry

Generates comprehensive technical blog posts, offering detailed explanations of system internals, architecture, and implementation, either through source code analysis or document-driven research.

Content & DocumentationClaude

whisper-transcribe

159
from majiayu000/claude-skill-registry

Transcribes audio and video files to text using OpenAI's Whisper CLI, enhanced with contextual grounding from local markdown files for improved accuracy.

Media Processing

modal-deployment

159
from majiayu000/claude-skill-registry

Run Python code in the cloud with serverless containers, GPUs, and autoscaling using Modal. This skill enables agents to generate code for deploying ML models, running batch jobs, serving APIs, and scaling compute-intensive workloads.

DevOps & Infrastructure

chrome-debug

159
from majiayu000/claude-skill-registry

This skill empowers AI agents to debug web applications and inspect browser behavior using the Chrome DevTools Protocol (CDP), offering both collaborative (headful) and automated (headless) modes.

Coding & DevelopmentClaude

astro

159
from majiayu000/claude-skill-registry

This skill provides essential Astro framework patterns, focusing on server-side rendering (SSR), static site generation (SSG), middleware, and TypeScript best practices. It helps AI agents implement secure authentication, manage API routes, and debug rendering behaviors within Astro projects.

Coding & Development

thor-skills

159
from majiayu000/claude-skill-registry

An entry point and router for AI agents to manage various THOR-related cybersecurity tasks, including running scans, analyzing logs, troubleshooting, and maintenance.

SecurityClaude

grail-miner

159
from majiayu000/claude-skill-registry

This skill assists in setting up, managing, and optimizing Grail miners on Bittensor Subnet 81, handling tasks like environment configuration, R2 storage, model checkpoint management, and performance tuning.

DevOps & Infrastructure