{"_id":"@billynd/mongoose-url-query","_rev":"4-595b32c8feea95d2a1fe8ac52b9beef8","name":"@billynd/mongoose-url-query","dist-tags":{"latest":"1.6.7"},"versions":{"1.6.3":{"name":"@billynd/mongoose-url-query","version":"1.6.3","keywords":["mongoose","mongodb","url","query","filter","pagination","aggregation","rest-api"],"author":{"name":"Peakify"},"license":"MIT","_id":"@billynd/mongoose-url-query@1.6.3","maintainers":[{"name":"billy621","email":"nduclong6201@gmail.com"}],"homepage":"https://github.com/BillyND/mongoose-url-query#readme","bugs":{"url":"https://github.com/BillyND/mongoose-url-query/issues"},"dist":{"shasum":"f0c4dbd7f5c3e9381b5b8026dbdffad0b99125a8","tarball":"https://registry.npmjs.org/@billynd/mongoose-url-query/-/mongoose-url-query-1.6.3.tgz","fileCount":14,"integrity":"sha512-0SeZSbELBcKq6TujM5dWlzMmen/xvYnCzle8qY3+FB583CnvgZ6jRPcBkfKc0VDeZK5s/yBuszGzIvl+akJphQ==","signatures":[{"sig":"MEYCIQD7SFH/jFHrLN0wzY3pYevFs7jvpUsG+Rm/ug2C78bwfgIhAJ3tIGUYHwiZR6BkGgAsQL370ZDW9u428x9qfon50Fis","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":63654},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"}},"gitHead":"8c3ac02ab62b0aa9752a2f35c2dadb5788fa1839","scripts":{"build":"tsc","clean":"rm -rf dist","prepublishOnly":"npm run build"},"_npmUser":{"name":"billy621","email":"nduclong6201@gmail.com"},"repository":{"url":"git+https://github.com/BillyND/mongoose-url-query.git","type":"git"},"_npmVersion":"10.8.2","description":"URL-based Mongoose query toolkit with filtering, pagination and aggregation","directories":{},"_nodeVersion":"20.19.0","_hasShrinkwrap":false,"devDependencies":{"mongoose":"^8.0.0","typescript":"^5.0.0","@types/node":"^22.0.0"},"peerDependencies":{"mongoose":">=6.0.0"},"_npmOperationalInternal":{"tmp":"tmp/mongoose-url-query_1.6.3_1765155941390_0.42833090417577013","host":"s3://npm-registry-packages-npm-production"}},"1.6.4":{"name":"@billynd/mongoose-url-query","version":"1.6.4","keywords":["mongoose","mongodb","url","query","filter","pagination","aggregation","rest-api"],"author":{"name":"Peakify"},"license":"MIT","_id":"@billynd/mongoose-url-query@1.6.4","maintainers":[{"name":"billy621","email":"nduclong6201@gmail.com"}],"homepage":"https://github.com/BillyND/mongoose-url-query#readme","bugs":{"url":"https://github.com/BillyND/mongoose-url-query/issues"},"dist":{"shasum":"478a5f8270419a26e9411e92fe28e2356ab38a21","tarball":"https://registry.npmjs.org/@billynd/mongoose-url-query/-/mongoose-url-query-1.6.4.tgz","fileCount":10,"integrity":"sha512-7+NGJdWSEOBtqzzH24SOKzq/EbDVD4Dv6mmvMVmoSxfb5VMi5Wy/lbzFNVftuEOmH8taMa4PtRLPpFsmDxK7gQ==","signatures":[{"sig":"MEUCIH/4RJ2m3ey0zf8lPTdAmBRgV+T9LiQTwsWCglS8/lLhAiEAvfDRrLzXNLFydemScLsteQ0LNPTZL9AXXkNHyngGe+s=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":59110},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"}},"gitHead":"397b69ea36b00f82d7a5cfcfd8ec356a7d022378","scripts":{"build":"tsc","clean":"rm -rf dist","prepublishOnly":"npm run build"},"_npmUser":{"name":"billy621","email":"nduclong6201@gmail.com"},"repository":{"url":"git+https://github.com/BillyND/mongoose-url-query.git","type":"git"},"_npmVersion":"10.8.2","description":"URL-based Mongoose query toolkit with filtering, pagination and aggregation","directories":{},"_nodeVersion":"20.19.0","_hasShrinkwrap":false,"devDependencies":{"mongoose":"^8.0.0","typescript":"^5.0.0","@types/node":"^22.0.0"},"peerDependencies":{"mongoose":">=6.0.0"},"_npmOperationalInternal":{"tmp":"tmp/mongoose-url-query_1.6.4_1765156241557_0.3183051916752615","host":"s3://npm-registry-packages-npm-production"}},"1.6.6":{"name":"@billynd/mongoose-url-query","version":"1.6.6","keywords":["mongoose","mongodb","url","query","filter","pagination","aggregation","rest-api"],"author":{"name":"Peakify"},"license":"MIT","_id":"@billynd/mongoose-url-query@1.6.6","maintainers":[{"name":"billy621","email":"nduclong6201@gmail.com"}],"homepage":"https://github.com/BillyND/mongoose-url-query#readme","bugs":{"url":"https://github.com/BillyND/mongoose-url-query/issues"},"dist":{"shasum":"24aef8660bf3aeb3a35ac5dc64418f92c1342bb2","tarball":"https://registry.npmjs.org/@billynd/mongoose-url-query/-/mongoose-url-query-1.6.6.tgz","fileCount":14,"integrity":"sha512-HBrz12ywlmtPP328oevVvIugZlTjrBHoQWToiEJ1twpbiQT0eons+ahEnMbmRuutfl7cGaYmjv8jZXULuID9/Q==","signatures":[{"sig":"MEUCIGE3MG1mxSQA3GA84o6huLCURgRNgSDm+dPrP//mrQs1AiEAq6VrenR5sbXHpaJijoJFbOfouFQB4GtpuAbeUvpUWLk=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":64158},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"}},"gitHead":"7bedf7010d549559adc5a6cb312d948941dd10ed","scripts":{"build":"tsc","clean":"rm -rf dist","prepublishOnly":"npm run build"},"_npmUser":{"name":"billy621","email":"nduclong6201@gmail.com"},"repository":{"url":"git+https://github.com/BillyND/mongoose-url-query.git","type":"git"},"_npmVersion":"10.8.2","description":"URL-based Mongoose query toolkit with filtering, pagination and aggregation","directories":{},"_nodeVersion":"20.19.0","_hasShrinkwrap":false,"devDependencies":{"mongoose":"^8.0.0","typescript":"^5.0.0","@types/node":"^22.0.0"},"peerDependencies":{"mongoose":">=6.0.0"},"_npmOperationalInternal":{"tmp":"tmp/mongoose-url-query_1.6.6_1765182886269_0.9887994859638825","host":"s3://npm-registry-packages-npm-production"}},"1.6.7":{"name":"@billynd/mongoose-url-query","version":"1.6.7","description":"URL-based Mongoose query toolkit with filtering, pagination and aggregation","author":{"name":"Peakify"},"license":"MIT","type":"module","main":"./dist/index.js","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"}},"scripts":{"build":"tsc","clean":"rm -rf dist","prepublishOnly":"npm run build"},"peerDependencies":{"mongoose":">=6.0.0"},"devDependencies":{"@types/node":"^22.0.0","mongoose":"^8.0.0","typescript":"^5.0.0"},"keywords":["mongoose","mongodb","url","query","filter","pagination","aggregation","rest-api"],"repository":{"type":"git","url":"git+https://github.com/BillyND/mongoose-url-query.git"},"_id":"@billynd/mongoose-url-query@1.6.7","gitHead":"cb3af5ee7a7bbd7fca29c7e5c69f46db6ad2fe6e","bugs":{"url":"https://github.com/BillyND/mongoose-url-query/issues"},"homepage":"https://github.com/BillyND/mongoose-url-query#readme","_nodeVersion":"20.19.0","_npmVersion":"10.8.2","dist":{"integrity":"sha512-x9+Q66E903kwFyUa9JpAbbTeoJevlmCVBzjzBMc6tcOENalaBj58SCgIA/pfLt+LhKoMsrkmoUR2X35Ps6fmbA==","shasum":"d2d498f96151c1aec8a37bc342c5ae7865f91639","tarball":"https://registry.npmjs.org/@billynd/mongoose-url-query/-/mongoose-url-query-1.6.7.tgz","fileCount":14,"unpackedSize":64374,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCYKhgN8WXt2UHdRV4N3McMrF35NWeoxwHVLIIzQxVjlwIgbVgoUkHxsxZeoy5q0CJk5wEl/lNFuL8ig6BenhsTYwc="}]},"_npmUser":{"name":"billy621","email":"nduclong6201@gmail.com"},"directories":{},"maintainers":[{"name":"billy621","email":"nduclong6201@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/mongoose-url-query_1.6.7_1765183216982_0.15845792684018267"},"_hasShrinkwrap":false}},"time":{"created":"2025-12-08T01:05:41.179Z","modified":"2025-12-08T08:40:17.349Z","1.6.3":"2025-12-08T01:05:41.547Z","1.6.4":"2025-12-08T01:10:41.726Z","1.6.6":"2025-12-08T08:34:46.409Z","1.6.7":"2025-12-08T08:40:17.113Z"},"bugs":{"url":"https://github.com/BillyND/mongoose-url-query/issues"},"author":{"name":"Peakify"},"license":"MIT","homepage":"https://github.com/BillyND/mongoose-url-query#readme","keywords":["mongoose","mongodb","url","query","filter","pagination","aggregation","rest-api"],"repository":{"type":"git","url":"git+https://github.com/BillyND/mongoose-url-query.git"},"description":"URL-based Mongoose query toolkit with filtering, pagination and aggregation","maintainers":[{"name":"billy621","email":"nduclong6201@gmail.com"}],"readme":"# @billynd/mongoose-url-query\r\n\r\n[![npm version](https://img.shields.io/npm/v/@billynd/mongoose-url-query.svg)](https://www.npmjs.com/package/@billynd/mongoose-url-query)\r\n[![npm downloads](https://img.shields.io/npm/dm/@billynd/mongoose-url-query.svg)](https://www.npmjs.com/package/@billynd/mongoose-url-query)\r\n[![license](https://img.shields.io/npm/l/@billynd/mongoose-url-query.svg)](https://github.com/BillyND/mongoose-url-query/blob/main/LICENSE)\r\n\r\nA simple, URL-based Mongoose query toolkit for Node.js. Parse filters from URL query strings, build aggregation pipelines, and fetch paginated data with ease.\r\n\r\n**Perfect for:** REST APIs, Remix, Next.js, Hono, Express, and any framework that uses Request objects.\r\n\r\n## Table of Contents\r\n\r\n- [Installation](#installation)\r\n- [Quick Start](#quick-start)\r\n- [Core Concepts](#core-concepts)\r\n- [API Reference](#api-reference)\r\n- [Filter System](#filter-system)\r\n- [Pipeline Customization](#pipeline-customization)\r\n- [Client-Side Usage](#client-side-usage)\r\n- [Response Format](#response-format)\r\n- [Changelog](#changelog)\r\n\r\n---\r\n\r\n## Installation\r\n\r\n```bash\r\nnpm install @billynd/mongoose-url-query\r\n# or\r\nyarn add @billynd/mongoose-url-query\r\n```\r\n\r\n**Peer Dependency:** Requires `mongoose >= 6.0.0`\r\n\r\n---\r\n\r\n## Quick Start\r\n\r\n```typescript\r\nimport { fetchList, fetchItem } from \"@billynd/mongoose-url-query\";\r\nimport { ProductModel } from \"./models\";\r\n\r\n// Fetch paginated list with filters from URL\r\n// URL: /api/products?filter=status|active&filter=price|amount|gt|100&page=2&limit=20\r\nconst result = await fetchList(request, ProductModel);\r\n// => { page: 2, total: 150, items: [...20 products...] }\r\n\r\n// With multi-tenant support\r\nconst result = await fetchList(request, ProductModel, {\r\n  tenantField: \"organizationId\",\r\n  tenantValue: currentOrg.id,\r\n});\r\n// Automatically adds filter: { organizationId: currentOrg.id }\r\n\r\n// Fetch single item by ID\r\n// URL: /api/products?id=abc123\r\nconst item = await fetchItem(request, ProductModel);\r\n// => { _id: \"abc123\", name: \"Product\", ... } or null\r\n```\r\n\r\n---\r\n\r\n## Core Concepts\r\n\r\n### 1. Request Input\r\n\r\nAll functions accept either a **Request object** or **URL string**:\r\n\r\n```typescript\r\n// ✅ Pass Request directly (Remix, Next.js, Hono)\r\nawait fetchList(request, Model);\r\n\r\n// ✅ Pass URL string\r\nawait fetchList(request.url, Model);\r\nawait fetchList(\"http://localhost/api?filter=status|active\", Model);\r\n```\r\n\r\n### 2. Pipeline Execution Order\r\n\r\nUnderstanding the order helps you customize queries effectively:\r\n\r\n```\r\n┌─────────────────────────────────────────────────────────────────────┐\r\n│  1. initialPipeline    →  Runs FIRST (setup, computed fields)       │\r\n│  2. URL Filters        →  Filters from ?filter=... params           │\r\n│  3. Count Total        →  Get total count for pagination            │\r\n│  4. Sort               →  Sort results                              │\r\n│  5. finalPipeline      →  Runs AFTER sort (lookups, projections)    │\r\n│  6. Pagination         →  $skip and $limit applied LAST             │\r\n└─────────────────────────────────────────────────────────────────────┘\r\n```\r\n\r\n---\r\n\r\n## API Reference\r\n\r\n### `fetchList(request, model, options?, initialPipeline?, finalPipeline?)`\r\n\r\nFetch a paginated list with filtering and sorting.\r\n\r\n**Parameters:**\r\n\r\n| Parameter         | Type                | Description                           |\r\n| ----------------- | ------------------- | ------------------------------------- |\r\n| `request`         | `Request \\| string` | Request object or URL string          |\r\n| `model`           | `Model`             | Mongoose model                        |\r\n| `options`         | `FetchOptions`      | Configuration options                 |\r\n| `initialPipeline` | `PipelineStage[]`   | Pipeline stages to run BEFORE filters |\r\n| `finalPipeline`   | `PipelineStage[]`   | Pipeline stages to run AFTER sort     |\r\n\r\n**Options:**\r\n\r\n| Option        | Type              | Default       | Description                                      |\r\n| ------------- | ----------------- | ------------- | ------------------------------------------------ |\r\n| `limit`       | `number`          | `250`         | Max items per page                               |\r\n| `sortField`   | `string`          | `\"updatedAt\"` | Default sort field                               |\r\n| `sortDir`     | `\"asc\" \\| \"desc\"` | `\"desc\"`      | Default sort direction                           |\r\n| `tenantField` | `string`          | -             | Field name for tenant/scope (e.g. \"orgId\")       |\r\n| `tenantValue` | `string`          | -             | Value to filter by (requires tenantField to set) |\r\n\r\n**Example:**\r\n\r\n```typescript\r\n// Basic usage (no multi-tenant)\r\nconst result = await fetchList(request, ProductModel, {\r\n  limit: 50,\r\n  sortField: \"createdAt\",\r\n});\r\n\r\n// With multi-tenant filtering\r\nconst result = await fetchList(request, ProductModel, {\r\n  tenantField: \"organizationId\",\r\n  tenantValue: currentOrg.id,\r\n  limit: 50,\r\n});\r\n```\r\n\r\n---\r\n\r\n### `fetchItem(request, model, initialPipeline?, finalPipeline?, id?)`\r\n\r\nFetch a single item by ID.\r\n\r\n**Parameters:**\r\n\r\n| Parameter         | Type                | Description                                      |\r\n| ----------------- | ------------------- | ------------------------------------------------ |\r\n| `request`         | `Request \\| string` | Request object or URL string                     |\r\n| `model`           | `Model`             | Mongoose model                                   |\r\n| `initialPipeline` | `PipelineStage[]`   | Pipeline stages before $match                    |\r\n| `finalPipeline`   | `PipelineStage[]`   | Pipeline stages after $match                     |\r\n| `id`              | `string \\| number`  | Explicit ID (optional, defaults to `?id=` param) |\r\n\r\n**ID Matching:** Automatically matches against `id`, `_id` (as string, number, or ObjectId).\r\n\r\n**Example:**\r\n\r\n```typescript\r\n// From URL: /api/products?id=abc123\r\nconst item = await fetchItem(request, ProductModel);\r\n\r\n// With explicit ID\r\nconst item = await fetchItem(request, ProductModel, [], [], \"abc123\");\r\n```\r\n\r\n---\r\n\r\n### `fetchItemBy(model, field, value, pipeline?)`\r\n\r\nFetch a single item by any field value.\r\n\r\n```typescript\r\n// Find by email\r\nconst user = await fetchItemBy(UserModel, \"email\", \"john@example.com\");\r\n\r\n// Find by slug with lookup\r\nconst post = await fetchItemBy(PostModel, \"slug\", \"my-post\", [\r\n  {\r\n    $lookup: {\r\n      from: \"users\",\r\n      localField: \"authorId\",\r\n      foreignField: \"_id\",\r\n      as: \"author\",\r\n    },\r\n  },\r\n]);\r\n```\r\n\r\n---\r\n\r\n### `fetchUnifiedList(request, models, options?)`\r\n\r\nFetch from multiple collections using `$unionWith`, useful for combining different document types.\r\n\r\n```typescript\r\nconst result = await fetchUnifiedList(\r\n  request,\r\n  [\r\n    { model: PostModel, initialPipeline: [...] },\r\n    { model: CommentModel, initialPipeline: [...] },\r\n  ],\r\n  {\r\n    tenantField: \"workspaceId\",\r\n    tenantValue: workspace.id,\r\n  }\r\n);\r\n// Each item will have `_sourceType` field indicating the source collection\r\n```\r\n\r\n---\r\n\r\n## Filter System\r\n\r\n### Filter String Format\r\n\r\nFilters are passed via URL query parameters: `?filter=field|type|operator|value`\r\n\r\n**Full Format:** `field|type|operator|value`\r\n\r\n```\r\n?filter=status|string|eq|active\r\n?filter=price|amount|gt|100\r\n?filter=createdAt|date|range|2024-01-01~2024-12-31\r\n```\r\n\r\n**Short Format:** `field|value` (auto-detects type and operator)\r\n\r\n```\r\n?filter=status|active        → string contains \"active\"\r\n?filter=price|100            → amount equals 100\r\n?filter=id|abc123            → id equals \"abc123\"\r\n?filter=tags|a,b,c           → array has any of [a,b,c]\r\n?filter=date|2024-01-01~2024-12-31  → date range\r\n```\r\n\r\n### Short Format Auto-Detection Rules\r\n\r\nWhen using short format, the library auto-detects type and operator based on these rules:\r\n\r\n**Type Detection:**\r\n\r\n| Value Pattern                           | Detected Type | Exception                      |\r\n| --------------------------------------- | ------------- | ------------------------------ |\r\n| `2024-01-01` or `2024-01-01~2024-12-31` | `date`        | -                              |\r\n| Only digits: `100`, `99.99`             | `amount`      | Unless field is `name` or `id` |\r\n| Contains comma: `a,b,c`                 | `array`       | -                              |\r\n| Everything else                         | `string`      | -                              |\r\n\r\n**Operator Detection:**\r\n\r\n| Condition              | Detected Operator  |\r\n| ---------------------- | ------------------ |\r\n| Field is `id` or `_id` | `eq` (exact match) |\r\n| Value contains `~`     | `range`            |\r\n| Value contains `,`     | `any`              |\r\n| Default                | `has` (contains)   |\r\n\r\n**Examples:**\r\n\r\n```\r\n?filter=name|123       → type: string, operator: has (name is forced string)\r\n?filter=price|123      → type: amount, operator: has\r\n?filter=id|abc123      → type: string, operator: eq (id uses exact match)\r\n?filter=status|active  → type: string, operator: has\r\n```\r\n\r\n> **Tip:** Use **Full Format** (`field|type|operator|value`) when you need explicit control over type and operator.\r\n\r\n### Supported Types\r\n\r\n| Type     | Description        | Example Values                        |\r\n| -------- | ------------------ | ------------------------------------- |\r\n| `string` | Text fields        | `\"active\"`, `\"john\"`                  |\r\n| `amount` | Numbers            | `100`, `99.99`                        |\r\n| `date`   | Dates (ISO format) | `2024-01-01`, `2024-01-01~2024-12-31` |\r\n| `array`  | Array fields       | `a,b,c` (comma-separated)             |\r\n\r\n### Supported Operators\r\n\r\n| Operator | Name         | Types                 | Example                               | MongoDB Equivalent                   |\r\n| -------- | ------------ | --------------------- | ------------------------------------- | ------------------------------------ |\r\n| `eq`     | Equals       | all                   | `status\\|string\\|eq\\|active`          | `{ status: \"active\" }`               |\r\n| `ne`     | Not Equals   | string, amount, array | `status\\|string\\|ne\\|deleted`         | `{ status: { $ne: \"deleted\" } }`     |\r\n| `has`    | Contains     | string                | `name\\|string\\|has\\|john`             | `{ name: /john/i }`                  |\r\n| `nh`     | Not Contains | string                | `name\\|string\\|nh\\|test`              | `{ name: { $not: /test/i } }`        |\r\n| `any`    | In Array     | string, array         | `status\\|array\\|any\\|a,b,c`           | `{ status: { $in: [\"a\",\"b\",\"c\"] } }` |\r\n| `none`   | Not In Array | string, array         | `status\\|array\\|none\\|x,y`            | `{ status: { $nin: [\"x\",\"y\"] } }`    |\r\n| `range`  | Between      | date, amount          | `price\\|amount\\|range\\|10~100`        | `{ price: { $gte: 10, $lte: 100 } }` |\r\n| `lt`     | Less Than    | amount                | `price\\|amount\\|lt\\|100`              | `{ price: { $lt: 100 } }`            |\r\n| `gt`     | Greater Than | amount                | `price\\|amount\\|gt\\|50`               | `{ price: { $gt: 50 } }`             |\r\n| `before` | Before Date  | date                  | `createdAt\\|date\\|before\\|2024-01-01` | `{ createdAt: { $lt: Date } }`       |\r\n| `after`  | After Date   | date                  | `createdAt\\|date\\|after\\|2024-01-01`  | `{ createdAt: { $gt: Date } }`       |\r\n\r\n### Percentage-Based Results\r\n\r\nYou can limit results to a **percentage** of the filtered data by adding a 5th parameter:\r\n\r\n**Format:** `field|type|operator|value|percentOfResult`\r\n\r\n| Value | Behavior                     | Example (100 records total) |\r\n| ----- | ---------------------------- | --------------------------- |\r\n| `10`  | Get **first 10%** of results | Returns first 10 records    |\r\n| `-10` | Get **last 10%** of results  | Skips 90, returns last 10   |\r\n\r\n**Examples:**\r\n\r\n```\r\n# Get top 10% of products with price > 0\r\n?filter=price|amount|gt|0|10\r\n\r\n# Get bottom 20% of users by score\r\n?filter=score|amount|gt|0|-20\r\n\r\n# Get first 5% of active orders\r\n?filter=status|string|eq|active|5\r\n```\r\n\r\n**Use Cases:**\r\n\r\n- **Top performers:** Get top 10% highest-value customers\r\n- **Bottom analysis:** Identify bottom 20% lowest-performing products\r\n- **Sampling:** Quick preview of a percentage of large datasets\r\n\r\n> **Note:** Percentage is calculated AFTER filters are applied. If filter returns 1000 items and you use `|10`, you get 100 items.\r\n\r\n---\r\n\r\n## Pipeline Customization\r\n\r\n### initialPipeline\r\n\r\n**What:** MongoDB aggregation stages that run **BEFORE** URL filters are applied.\r\n\r\n**Use Cases:**\r\n\r\n- Create computed/virtual fields for filtering\r\n- Add base $match conditions that always apply\r\n- $lookup to join related data before filtering\r\n\r\n**Example: Create searchable field**\r\n\r\n```typescript\r\nconst result = await fetchList(request, ProductModel, { limit: 50 }, [\r\n  // Create a combined search field from multiple fields\r\n  {\r\n    $addFields: {\r\n      searchText: {\r\n        $concat: [\r\n          { $toString: \"$_id\" },\r\n          \" \",\r\n          { $ifNull: [\"$title\", \"\"] },\r\n          \" \",\r\n          { $ifNull: [\"$name\", \"\"] },\r\n          \" \",\r\n          { $ifNull: [\"$sku\", \"\"] },\r\n        ],\r\n      },\r\n    },\r\n  },\r\n  // Always exclude deleted items\r\n  { $match: { deletedAt: null, isActive: true } },\r\n]);\r\n// Client can now filter: ?filter=searchText|string|has|keyword\r\n```\r\n\r\n### finalPipeline\r\n\r\n**What:** MongoDB aggregation stages that run **AFTER** sorting but **BEFORE** pagination.\r\n\r\n**Use Cases:**\r\n\r\n- $lookup to join related data (runs on sorted data, before limiting)\r\n- $project to shape the final output\r\n- $unwind to flatten arrays\r\n\r\n**Example: Add related data**\r\n\r\n```typescript\r\nconst result = await fetchList(\r\n  request,\r\n  OrderModel,\r\n  { limit: 50 },\r\n  [], // no initialPipeline\r\n  [\r\n    // Lookup customer info\r\n    {\r\n      $lookup: {\r\n        from: \"customers\",\r\n        localField: \"customerId\",\r\n        foreignField: \"_id\",\r\n        as: \"customer\",\r\n      },\r\n    },\r\n    { $unwind: { path: \"$customer\", preserveNullAndEmptyArrays: true } },\r\n    // Shape output\r\n    {\r\n      $project: {\r\n        _id: 1,\r\n        orderNumber: 1,\r\n        total: 1,\r\n        status: 1,\r\n        customerName: \"$customer.name\",\r\n        customerEmail: \"$customer.email\",\r\n      },\r\n    },\r\n  ]\r\n);\r\n```\r\n\r\n### Combined Example\r\n\r\n```typescript\r\nconst result = await fetchList(\r\n  request,\r\n  ProductModel,\r\n  {\r\n    tenantField: \"storeId\",\r\n    tenantValue: currentStore.id,\r\n    limit: 20,\r\n    sortField: \"createdAt\",\r\n  },\r\n  // initialPipeline: runs FIRST\r\n  [\r\n    { $addFields: { searchText: { $concat: [\"$title\", \" \", \"$sku\"] } } },\r\n    { $match: { isActive: true } },\r\n  ],\r\n  // finalPipeline: runs AFTER sort, BEFORE pagination\r\n  [\r\n    {\r\n      $lookup: {\r\n        from: \"categories\",\r\n        localField: \"categoryId\",\r\n        foreignField: \"_id\",\r\n        as: \"category\",\r\n      },\r\n    },\r\n    { $unwind: { path: \"$category\", preserveNullAndEmptyArrays: true } },\r\n  ]\r\n);\r\n```\r\n\r\n---\r\n\r\n## Client-Side Usage\r\n\r\n### `buildQueryUrl(baseUrl, options)`\r\n\r\nBuild a query URL with filters, pagination, and sorting. **Works in both browser and Node.js.**\r\n\r\n```typescript\r\nimport { buildQueryUrl } from \"@billynd/mongoose-url-query\";\r\n\r\nconst url = buildQueryUrl(\"/api/products\", {\r\n  page: 1,\r\n  limit: 20,\r\n  sort: \"price|desc\",\r\n  filters: [\r\n    \"status|string|eq|active\",\r\n    \"price|amount|range|100~500\",\r\n    \"category|array|any|electronics,books\",\r\n  ],\r\n});\r\n// => \"/api/products?page=1&limit=20&sort=price|desc&filter=status|string|eq|active&filter=...\"\r\n```\r\n\r\n**Options:**\r\n\r\n| Option      | Type       | Description                             |\r\n| ----------- | ---------- | --------------------------------------- |\r\n| `page`      | `number`   | Page number                             |\r\n| `limit`     | `number`   | Items per page                          |\r\n| `sort`      | `string`   | Sort: `\"field\\|asc\"` or `\"field\\|desc\"` |\r\n| `filters`   | `string[]` | Array of filter strings                 |\r\n| `id`        | `string`   | Item ID (for fetchItem)                 |\r\n| `export`    | `boolean`  | Skip pagination                         |\r\n| `countOnly` | `boolean`  | Return only count                       |\r\n\r\n### React Example\r\n\r\n```tsx\r\nimport { buildQueryUrl } from \"@billynd/mongoose-url-query\";\r\n\r\nfunction ProductList() {\r\n  const [products, setProducts] = useState([]);\r\n  const [filters, setFilters] = useState<string[]>([]);\r\n  const [page, setPage] = useState(1);\r\n\r\n  useEffect(() => {\r\n    const url = buildQueryUrl(\"/api/products\", {\r\n      page,\r\n      limit: 20,\r\n      filters,\r\n    });\r\n\r\n    fetch(url)\r\n      .then((res) => res.json())\r\n      .then((data) => setProducts(data.items));\r\n  }, [page, filters]);\r\n\r\n  return (\r\n    <div>\r\n      <button onClick={() => setFilters([...filters, \"status|string|eq|active\"])}>\r\n        Active Only\r\n      </button>\r\n      <button onClick={() => setFilters([...filters, \"price|amount|gt|100\"])}>\r\n        Price > $100\r\n      </button>\r\n      {/* Render products */}\r\n    </div>\r\n  );\r\n}\r\n```\r\n\r\n### Remix Example\r\n\r\n```tsx\r\n// app/routes/products.tsx\r\nimport { json, type LoaderFunctionArgs } from \"@remix-run/node\";\r\nimport { useLoaderData, useSearchParams } from \"@remix-run/react\";\r\nimport { fetchList } from \"@billynd/mongoose-url-query\";\r\n\r\nexport async function loader({ request }: LoaderFunctionArgs) {\r\n  const result = await fetchList(request, ProductModel, { limit: 20 });\r\n  return json(result);\r\n}\r\n\r\nexport default function Products() {\r\n  const { items, total, page } = useLoaderData<typeof loader>();\r\n  const [searchParams, setSearchParams] = useSearchParams();\r\n\r\n  const addFilter = (filter: string) => {\r\n    const params = new URLSearchParams(searchParams);\r\n    params.append(\"filter\", filter);\r\n    setSearchParams(params);\r\n  };\r\n\r\n  return (\r\n    <div>\r\n      <p>Total: {total} items</p>\r\n      <button onClick={() => addFilter(\"status|active\")}>Active</button>\r\n      {items.map((item) => (\r\n        <div key={item._id}>{item.name}</div>\r\n      ))}\r\n    </div>\r\n  );\r\n}\r\n```\r\n\r\n---\r\n\r\n## Response Format\r\n\r\n### fetchList / fetchUnifiedList\r\n\r\n```typescript\r\ninterface FetchListResult<T> {\r\n  page: number; // Current page number (1-indexed)\r\n  limit: number; // Items per page\r\n  total: number; // Total matching documents\r\n  totalPages: number; // Total number of pages\r\n  hasNextPage: boolean; // Whether there is a next page\r\n  hasPrevPage: boolean; // Whether there is a previous page\r\n  nextPage: number | null; // Next page number (null if none)\r\n  prevPage: number | null; // Previous page number (null if none)\r\n  items: T[]; // Array of documents for current page\r\n}\r\n```\r\n\r\n**Example Response:**\r\n\r\n```json\r\n{\r\n  \"page\": 2,\r\n  \"limit\": 20,\r\n  \"total\": 150,\r\n  \"totalPages\": 8,\r\n  \"hasNextPage\": true,\r\n  \"hasPrevPage\": true,\r\n  \"nextPage\": 3,\r\n  \"prevPage\": 1,\r\n  \"items\": [\r\n    { \"_id\": \"...\", \"name\": \"Product 1\" },\r\n    { \"_id\": \"...\", \"name\": \"Product 2\" }\r\n  ]\r\n}\r\n```\r\n\r\n**Pagination Helpers for Client:**\r\n\r\n```typescript\r\n// Easy to build pagination UI\r\nconst { page, totalPages, hasNextPage, hasPrevPage, nextPage, prevPage } = result;\r\n\r\n// Example: Pagination component\r\n<button disabled={!hasPrevPage} onClick={() => goToPage(prevPage)}>Prev</button>\r\n<span>Page {page} of {totalPages}</span>\r\n<button disabled={!hasNextPage} onClick={() => goToPage(nextPage)}>Next</button>\r\n```\r\n\r\n### fetchItem / fetchItemBy\r\n\r\nReturns the document or `null` if not found.\r\n\r\n```typescript\r\n// Found\r\n{ _id: \"abc123\", name: \"Product\", price: 99.99, ... }\r\n\r\n// Not found\r\nnull\r\n```\r\n\r\n---\r\n\r\n## URL Query Parameters\r\n\r\n| Parameter         | Description                         | Example                                                |\r\n| ----------------- | ----------------------------------- | ------------------------------------------------------ |\r\n| `page`            | Page number (default: 1)            | `?page=2`                                              |\r\n| `limit`           | Items per page (max: options.limit) | `?limit=20`                                            |\r\n| `sort`            | Sort field and direction            | `?sort=price\\|desc` or `?sort=createdAt\\|asc`          |\r\n| `filter`          | Filter string (repeatable)          | `?filter=status\\|active&filter=price\\|amount\\|gt\\|100` |\r\n| `id`              | Item ID for fetchItem               | `?id=abc123`                                           |\r\n| `export`          | Skip pagination (return all)        | `?export=true`                                         |\r\n| `countResultOnly` | Return only count, no items         | `?countResultOnly=true`                                |\r\n\r\n---\r\n\r\n## TypeScript Support\r\n\r\nFull TypeScript support with generics:\r\n\r\n```typescript\r\ninterface Product {\r\n  _id: string;\r\n  name: string;\r\n  price: number;\r\n}\r\n\r\nconst result = await fetchList<Product>(request, ProductModel);\r\n// result.items is Product[]\r\n\r\nconst item = await fetchItem<Product>(request, ProductModel);\r\n// item is Product | null\r\n```\r\n\r\n---\r\n\r\n## Changelog\r\n\r\nSee [GitHub Releases](https://github.com/BillyND/mongoose-url-query/releases) for version history and release notes.\r\n\r\n---\r\n\r\n## License\r\n\r\nMIT\r\n","readmeFilename":"README.md"}