> ## Documentation Index
> Fetch the complete documentation index at: https://woltz.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Query Params & Adapters

> API integration, field mapping adapters, and advanced features

## Query Params Integration

Criteria can be constructed directly from URL query parameters, making it perfect for REST APIs.

### Basic Usage

```typescript theme={null}
import { Criteria } from "@woltz/rich-domain";

// Express.js example
app.get("/users", (req, res) => {
  const criteria = Criteria.fromQueryParams<User>(req.query);
  const users = await userRepository.find(criteria);
  res.json(users.toJSON());
});
```

### Query Parameter Format

Filters follow the pattern: `field:operator=value`, nested under the `filters` key:

```typescript theme={null}
const criteria = Criteria.fromQueryParams<User>({
  filters: {
    "name:contains": "john",
    "age:greaterThan": "18",
    "status:equals": "active",
  },
});
```

### Supported Parameters

| Parameter       | Key in `QueryParamsObject` | Type                              | Example                          |
| --------------- | -------------------------- | --------------------------------- | -------------------------------- |
| Filters         | `filters`                  | `Record<string, unknown>`         | `{ "age:greaterThan": "18" }`    |
| Ordering        | `orderBy`                  | `string[]`                        | `["createdAt:desc"]`             |
| Multiple orders | `orderBy`                  | `string[]`                        | `["featured:desc", "price:asc"]` |
| Pagination      | `pagination`               | `{ page: number, limit: number }` | `{ page: 2, limit: 20 }`         |
| Search          | `search`                   | `string`                          | `"laptop"`                       |

### Complete Example

```typescript theme={null}
const criteria = Criteria.fromQueryParams<Product>({
  filters: {
    "status:equals": "active",
    "price:lessThanOrEqual": "100",
    "category:in": ["electronics", "accessories"],
  },
  search: "wireless",
  orderBy: ["featured:desc", "price:asc"],
  pagination: { page: 1, limit: 24 },
});
```

### Value Parsing

Values are automatically parsed to their correct types:

```typescript theme={null}
// Numbers
"age:greaterThan": "25"     → 25 (number)

// Booleans
"active:equals": "true"     → true (boolean)
"active:equals": "false"    → false (boolean)

// Dates (ISO format)
"createdAt:greaterThan": "2024-01-01T00:00:00Z"  → Date object

// Arrays (comma-separated)
"status:in": "active,pending,draft"  → ["active", "pending", "draft"]

// Between (two values)
"price:between": "10,100"  → [10, 100]
```

### Nested Fields in Query Params

```typescript theme={null}
const criteria = Criteria.fromQueryParams<User>({
  filters: {
    "profile.location.city:equals": "New York",
    "settings.theme:equals": "dark",
  },
});
```

### Quantifiers in Query Params

Use `@quantifier` suffix for array field quantifiers:

```typescript theme={null}
const criteria = Criteria.fromQueryParams<User>({
  filters: {
    "posts.views:greaterThan@some": "1000",   // Any post with > 1000 views
    "posts.published:equals@every": "true",   // All posts published
    "comments.flagged:equals@none": "true",   // No flagged comments
  },
});
```

## Field Adapters

Adapters map domain field names to database column names, allowing you to maintain clean API contracts while using different database schemas.

### Why Use Adapters?

```typescript theme={null}
// Your domain model uses camelCase
interface UserDto {
  id: string;
  firstName: string;
  lastName: string;
  emailAddress: string;
  createdAt: Date;
}

// But your database uses snake_case
// users table: id, first_name, last_name, email_address, created_at
```

### Creating an Adapter

```typescript theme={null}
import { CriteriaAdapter } from "@woltz/rich-domain";

const UserAdapter: CriteriaAdapter<UserDto, UserInDatabase> = {
  firstName: "first_name",
  lastName: "last_name",
  emailAddress: "email_address",
  createdAt: "created_at",
};
```

### Using an Adapter

```typescript theme={null}
// Method 1: With useAdapter()
const criteria = Criteria.create<UserDto>()
  .useAdapter(UserAdapter)
  .whereEquals("firstName", "John") // Becomes: first_name = 'John'
  .orderByDesc("createdAt"); // Becomes: ORDER BY created_at DESC

// Method 2: With fromQueryParams()
const criteria = Criteria.fromQueryParams<UserDto>(
  { "firstName:equals": "John" },
  UserAdapter
);

// Method 3: With fromObject()
const criteria = Criteria.fromObject<UserDto>(
  { filters: [{ field: "firstName", operator: "equals", value: "John" }] },
  UserAdapter
);
```

### Nested Field Adapters

Map nested paths to different structures:

```typescript theme={null}
interface OrderDto {
  id: string;
  customer: {
    name: string;
    email: string;
  };
  items: {
    productName: string;
    quantity: number;
  }[];
}

interface OrderInDb {
  id: string;
  customer_name: string;
  customer_email: string;
  order_items: {
    product_name: string;
    qty: number;
  }[];
}

const OrderAdapter: CriteriaAdapter<OrderDto, OrderInDb> = {
  "customer.name": "customer_name",
  "customer.email": "customer_email",
  items: "order_items",
  "items.productName": "order_items.product_name",
  "items.quantity": "order_items.qty",
};

const criteria = Criteria.create<OrderDto>()
  .useAdapter(OrderAdapter)
  .whereContains("customer.name", "John") // → customer_name LIKE '%John%'
  .where("items.quantity", "greaterThan", 5); // → order_items.qty > 5
```

### Adapter with Prefix Matching

Adapters support prefix matching for nested paths:

```typescript theme={null}
const adapter: CriteriaAdapter<Source, Target> = {
  user: "app_user", // Maps all user.* paths
};

// user.name → app_user.name
// user.profile.bio → app_user.profile.bio
```

### Getting the Adapter

```typescript theme={null}
const criteria = Criteria.create<User>().useAdapter(UserAdapter);

const adapter = criteria.getAdapter(); // Returns the adapter or undefined
```

## Serialization

### toJSON()

Convert criteria to a plain object:

```typescript theme={null}
const criteria = Criteria.create<User>()
  .whereEquals("status", "active")
  .where("age", "greaterThan", 18)
  .orderByDesc("createdAt")
  .search("john")
  .paginate(2, 20);

const json = criteria.toJSON();
```

Result:

```json theme={null}
{
  "filters": [
    { "field": "status", "operator": "equals", "value": "active" },
    { "field": "age", "operator": "greaterThan", "value": 18 }
  ],
  "orders": [{ "field": "createdAt", "direction": "desc" }],
  "pagination": {
    "page": 2,
    "limit": 20,
    "offset": 20
  },
  "search": 'john'
}
```

### toQueryObject()

Convert criteria to a `QueryParamsObject` — useful when you need the structured object before converting to URL params:

```typescript theme={null}
const criteria = Criteria.create<User>()
  .whereEquals("status", "active")
  .where("age", "greaterThan", 18)
  .orderByDesc("createdAt")
  .paginate(2, 20);

const obj = criteria.toQueryObject();
// {
//   filters: { "status:equals": "active", "age:greaterThan": "18" },
//   orders: [{ field: "createdAt", direction: "desc" }],  ← resolved via adapter
//   orderBy: ["createdAt:desc"],
//   pagination: { page: 2, limit: 20, offset: 20 },
// }
```

### toQueryParams()

Convert criteria directly to `URLSearchParams` for use in HTTP requests:

```typescript theme={null}
const criteria = Criteria.create<User>()
  .whereEquals("status", "active")
  .orderByDesc("createdAt")
  .paginate(1, 20);

const params = criteria.toQueryParams();
// URLSearchParams {
//   filters → '{"status:equals":"active"}',
//   orderBy → '["createdAt:desc"]',
//   page    → "1",
//   limit   → "20",
// }

// Use in fetch
const response = await fetch(`/api/users?${params.toString()}`);
```

<Note>
  `toQueryParams()` and `fromQueryParams()` use the `QueryParamsObject` structure (with a `filters` key). When integrating with an HTTP server, parse the `filters` query param as JSON before passing it to `fromQueryParams`.
</Note>

### fromObject()

Reconstruct criteria from a plain object:

```typescript theme={null}
const criteria = Criteria.fromObject<User>({
  filters: [
    { field: "status", operator: "equals", value: "active" },
    { field: "age", operator: "greaterThan", value: 18 },
  ],
  orders: [{ field: "createdAt", direction: "desc" }],
  pagination: { page: 2, limit: 20, offset: 20 },
  search: "john",
});
```

### Use Cases for Serialization

<CardGroup cols={2}>
  <Card title="API Transport" icon="globe">
    Send criteria from frontend to backend
  </Card>

  <Card title="Caching" icon="database">
    Cache query configurations
  </Card>

  <Card title="Saved Filters" icon="bookmark">
    Store user's saved filter presets
  </Card>

  <Card title="Logging" icon="file-lines">
    Log query configurations for debugging
  </Card>
</CardGroup>

## Cloning

Create independent copies for query variations:

```typescript theme={null}
const baseCriteria = Criteria.create<User>()
  .whereEquals("status", "active")
  .whereNotNull("email")
  .orderByDesc("createdAt");

// Clone for different use cases
const adminUsers = baseCriteria
  .clone()
  .whereEquals("role", "admin")
  .paginate(1, 10);

const recentUsers = baseCriteria
  .clone()
  .where("createdAt", "greaterThan", lastWeek)
  .paginate(1, 50);

const searchResults = baseCriteria
  .clone()
  .search(searchQuery)
  .limit(20);

// Original is unchanged
console.log(baseCriteria.getFilters().length); // 2 (status, email)
```

## Error Handling

### Invalid Operator

```typescript theme={null}
try {
  const criteria = Criteria.create<User>().where("age", "contains", 18); // contains not valid for number
} catch (error) {
  // InvalidCriteriaError: Operator "contains" is not valid for type "number".
  // Valid operators: equals, notEquals, greaterThan, ...
}
```

### Invalid Quantifier

```typescript theme={null}
try {
  const criteria = Criteria.fromQueryParams<User>({
    "posts.title:contains@invalid": "test",
  });
} catch (error) {
  // InvalidCriteriaError: Invalid quantifier. Valid values: some, every, none
}
```

## Express.js Integration Example

```typescript theme={null}
import express from "express";
import { Criteria, PaginatedResult } from "@woltz/rich-domain";

const app = express();

// Middleware to parse criteria from query params
function parseCriteria<T>(adapter?: CriteriaAdapter<any, any>) {
  return (
    req: express.Request,
    res: express.Response,
    next: express.NextFunction
  ) => {
    try {
      req.criteria = Criteria.fromQueryParams<T>(req.query, adapter);
      next();
    } catch (error) {
      res.status(400).json({ error: error.message });
    }
  };
}

// Usage
app.get("/users", parseCriteria<User>(UserAdapter), async (req, res) => {
  const criteria = req.criteria;
  const result = await userRepository.find(criteria);
  res.json(result.toJSON());
});

app.get(
  "/products",
  parseCriteria<Product>(ProductAdapter),
  async (req, res) => {
    const criteria = req.criteria
      .whereEquals("published", true) // Add server-side filter
      .whereNull("deletedAt");

    const result = await productRepository.find(criteria);
    res.json(result.toJSON());
  }
);
```

## Fastify Integration Example

```typescript theme={null}
import Fastify from "fastify";
import { Criteria } from "@woltz/rich-domain";

const fastify = Fastify();

fastify.get("/users", async (request, reply) => {
  const criteria = Criteria.fromQueryParams<User>(
    request.query as Record<string, string>,
    UserAdapter
  );

  const result = await userRepository.find(criteria);
  return result.toJSON();
});
```

## Frontend Integration Example

```typescript theme={null}
// React hook for building query strings
function useCriteriaQueryString<T>(criteria: Criteria<T>): string {
  const json = criteria.toJSON();
  const params = new URLSearchParams();

  // Add filters
  json.filters.forEach((filter) => {
    const key = `${filter.field}:${filter.operator}`;
    const value = Array.isArray(filter.value)
      ? filter.value.join(",")
      : String(filter.value);
    params.set(key, value);
  });

  // Add ordering
  if (json.orders.length > 0) {
    params.set(
      "orderBy",
      json.orders.map((o) => `${o.field}:${o.direction}`).join(",")
    );
  }

  // Add pagination
  if (json.pagination) {
    params.set("page", String(json.pagination.page));
    params.set("limit", String(json.pagination.limit));
  }

  // Add search
  if (json.search) {
    params.set("search", json.search);
  }

  return params.toString();
}

// Usage
const criteria = Criteria.create<Product>()
  .whereEquals("category", "electronics")
  .orderByAsc("price")
  .paginate(1, 20);

const queryString = useCriteriaQueryString(criteria);
// "category:equals=electronics&orderBy=price:asc&page=1&limit=20"

const response = await fetch(`/api/products?${queryString}`);
```

## CriteriaAdapter Type Reference

```typescript theme={null}
type CriteriaAdapter<Input, Output> = {
  [K in FieldPath<Input>]?: FieldPath<Output>;
};

// Example
interface SourceType {
  userName: string;
  userEmail: string;
  profile: {
    avatar: string;
  };
}

interface TargetType {
  user_name: string;
  user_email: string;
  user_profile: {
    avatar_url: string;
  };
}

const adapter: CriteriaAdapter<SourceType, TargetType> = {
  userName: "user_name",
  userEmail: "user_email",
  profile: "user_profile",
  "profile.avatar": "user_profile.avatar_url",
};
```
