{"_id":"@alis-kit/mongoose","_rev":"7-83553ef1a024dc47812612319b2efb72","name":"@alis-kit/mongoose","dist-tags":{"latest":"2.0.0"},"versions":{"0.1.0":{"name":"@alis-kit/mongoose","version":"0.1.0","keywords":["mongodb","mongoose","decorator","repository","query-builder","soft-delete","ttl"],"license":"MIT","_id":"@alis-kit/mongoose@0.1.0","maintainers":[{"name":"alisdev","email":"ibnu.ali56@gmail.com"}],"dist":{"shasum":"486541431d9c9b73f56169b2da59090a0338c090","tarball":"https://registry.npmjs.org/@alis-kit/mongoose/-/mongoose-0.1.0.tgz","fileCount":110,"integrity":"sha512-Vv0Id/Mu+T4Ef5iqlsNvrx0d082w1LJf8v3er1AklpwbwDkFJeEAmTD30j8fBBcFnqiLA4bOzQIzyyl+FjPgnQ==","signatures":[{"sig":"MEQCIGdo+r1LtI/OPMhhXW0uWWVEyRILwG6zsV7z34pXIux+AiANrpx3w6gIFkUpPZE0B980Wb9khUTnZr029w5dkMocsQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":217281},"main":"./dist/index.js","type":"module","_from":"file:alis-kit-mongoose-0.1.0.tgz","types":"./dist/index.d.ts","engines":{"node":">=18","typescript":">=5.2"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"dev":"tsc --watch","lint":"tsc --noEmit","test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"alisdev","email":"ibnu.ali56@gmail.com"},"_resolved":"C:\\Users\\ibnu-pc\\AppData\\Local\\Temp\\1b49ea3428463e38cd7c5afbe3bab6d1\\alis-kit-mongoose-0.1.0.tgz","_integrity":"sha512-Vv0Id/Mu+T4Ef5iqlsNvrx0d082w1LJf8v3er1AklpwbwDkFJeEAmTD30j8fBBcFnqiLA4bOzQIzyyl+FjPgnQ==","_npmVersion":"10.9.2","description":"Decorator-based MongoDB schema & repository library with fluent query builder, soft delete, TTL, and auto-populate relations","directories":{},"_nodeVersion":"22.15.0","dependencies":{"zod":"^3.23.0","mongoose":"^8.0.0"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^2.0.0","typescript":"^5.6.0","@types/node":"^22.0.0","mongodb-memory-server":"^10.0.0"},"_npmOperationalInternal":{"tmp":"tmp/mongoose_0.1.0_1784432454073_0.4246171173018447","host":"s3://npm-registry-packages-npm-production"}},"1.0.0":{"name":"@alis-kit/mongoose","version":"1.0.0","keywords":["mongodb","mongoose","decorator","repository","query-builder","soft-delete","ttl"],"license":"MIT","_id":"@alis-kit/mongoose@1.0.0","maintainers":[{"name":"alisdev","email":"ibnu.ali56@gmail.com"}],"dist":{"shasum":"61c03d585bb8204840c355a5cc502adca15abc5f","tarball":"https://registry.npmjs.org/@alis-kit/mongoose/-/mongoose-1.0.0.tgz","fileCount":110,"integrity":"sha512-2JD0rUKItrmXUvF3EOfjvJYoD2nKyU8R26DbA8rJtf37k/KaaYx8IFL2m0ew5WG1Zqo2vblFts/iMXxaAGjPyQ==","signatures":[{"sig":"MEYCIQDUuWewrCnJPmd++ZfDGQUSMvOXDeHT3mOt6OQKw+PDcgIhAOLDtUUnJlLlRCOAxLo5cjkuiHbQTeva1On2nJ2R4kK9","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":218995},"main":"./dist/index.js","type":"module","_from":"file:alis-kit-mongoose-1.0.0.tgz","types":"./dist/index.d.ts","engines":{"node":">=18","typescript":">=5.2"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"dev":"tsc --watch","lint":"tsc --noEmit","test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"alisdev","email":"ibnu.ali56@gmail.com"},"_resolved":"C:\\Users\\ibnu-pc\\AppData\\Local\\Temp\\f804ccffb6f0a841d988819f768fa8be\\alis-kit-mongoose-1.0.0.tgz","_integrity":"sha512-2JD0rUKItrmXUvF3EOfjvJYoD2nKyU8R26DbA8rJtf37k/KaaYx8IFL2m0ew5WG1Zqo2vblFts/iMXxaAGjPyQ==","_npmVersion":"10.9.2","description":"Decorator-based MongoDB schema & repository library with fluent query builder, soft delete, TTL, and auto-populate relations","directories":{},"_nodeVersion":"22.15.0","dependencies":{"zod":"^3.23.0","mongoose":"^8.0.0"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^2.0.0","typescript":"^5.6.0","@types/node":"^22.0.0","mongodb-memory-server":"^10.0.0"},"_npmOperationalInternal":{"tmp":"tmp/mongoose_1.0.0_1784432767060_0.25956262739893576","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@alis-kit/mongoose","version":"1.1.0","keywords":["mongodb","mongoose","decorator","repository","query-builder","soft-delete","ttl"],"license":"MIT","_id":"@alis-kit/mongoose@1.1.0","maintainers":[{"name":"alisdev","email":"ibnu.ali56@gmail.com"}],"dist":{"shasum":"2ce124c6768163f1b19a8757621eafbdd3c21675","tarball":"https://registry.npmjs.org/@alis-kit/mongoose/-/mongoose-1.1.0.tgz","fileCount":110,"integrity":"sha512-rBB+7MXM1n7OYYovDfgxg11Ct4vBC2lR+y9Bl1sY7rafn3M1WrmDrdcslSvEfhDvBUMWcxk+o0rdLeQkSvns0w==","signatures":[{"sig":"MEUCICxmkWfFuKRHguOl5iMtidysdT9eRiH5kWuB39NmBbtlAiEA7j5IjOGrMDup707osp8picpgVaIWrmey29EQnJ9Sh/0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":218551},"main":"./dist/index.js","type":"module","_from":"file:alis-kit-mongoose-1.1.0.tgz","types":"./dist/index.d.ts","engines":{"node":">=18","typescript":">=5.2"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"dev":"tsc --watch","lint":"tsc --noEmit","test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"alisdev","email":"ibnu.ali56@gmail.com"},"_resolved":"C:\\Users\\ibnu-pc\\AppData\\Local\\Temp\\a4e7f78ab24771d39b806a4fcadf615f\\alis-kit-mongoose-1.1.0.tgz","_integrity":"sha512-rBB+7MXM1n7OYYovDfgxg11Ct4vBC2lR+y9Bl1sY7rafn3M1WrmDrdcslSvEfhDvBUMWcxk+o0rdLeQkSvns0w==","_npmVersion":"11.16.0","description":"Decorator-based MongoDB schema & repository library with fluent query builder, soft delete, TTL, and auto-populate relations","directories":{},"_nodeVersion":"24.18.0","dependencies":{"zod":"^4.4.3","mongoose":"^8.0.0"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^2.0.0","typescript":"^5.6.0","@types/node":"^22.0.0","mongodb-memory-server":"^10.0.0"},"_npmOperationalInternal":{"tmp":"tmp/mongoose_1.1.0_1784476553601_0.04359108434013814","host":"s3://npm-registry-packages-npm-production"}},"1.2.0":{"name":"@alis-kit/mongoose","version":"1.2.0","keywords":["mongodb","mongoose","decorator","repository","query-builder","soft-delete","ttl"],"license":"MIT","_id":"@alis-kit/mongoose@1.2.0","maintainers":[{"name":"alisdev","email":"ibnu.ali56@gmail.com"}],"dist":{"shasum":"0c504cd4813b352affe6e4f8af7efc68fd8fac6e","tarball":"https://registry.npmjs.org/@alis-kit/mongoose/-/mongoose-1.2.0.tgz","fileCount":110,"integrity":"sha512-fq+k/af465Mm1fU+41fCfiee9oivvf+jFngmL5jmZ4a8gGziqM95zAnGm+4VsJylvC+ANwjt3YEk6UpT9vPb5g==","signatures":[{"sig":"MEUCIA8/0a+dR4cyhyCsG3RqstaCFtnW8Ngxfcrxf6Xr9Y0+AiEAySlOrtsFgdG19wtqSLqj45nr8vGJTyWGMkn7FUo5bnI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":219769},"main":"./dist/index.js","type":"module","_from":"file:alis-kit-mongoose-1.2.0.tgz","types":"./dist/index.d.ts","engines":{"node":">=20.19.0","typescript":">=5.6"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"dev":"tsc --watch","lint":"tsc --noEmit","test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"alisdev","email":"ibnu.ali56@gmail.com"},"_resolved":"C:\\Users\\IBNU~1.BAD\\AppData\\Local\\Temp\\2e9cf7513ec147466ed4c43ffdc319a2\\alis-kit-mongoose-1.2.0.tgz","_integrity":"sha512-fq+k/af465Mm1fU+41fCfiee9oivvf+jFngmL5jmZ4a8gGziqM95zAnGm+4VsJylvC+ANwjt3YEk6UpT9vPb5g==","_npmVersion":"11.6.2","description":"Decorator-based MongoDB schema & repository library with fluent query builder, soft delete, TTL, and auto-populate relations","directories":{},"_nodeVersion":"24.12.0","dependencies":{"zod":"^4.4.3","mongoose":"^9.8.0"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^6.4.3","vitest":"^4.1.10","typescript":"^7.0.2","@types/node":"^26.1.1","mongodb-memory-server":"^11.2.0"},"_npmOperationalInternal":{"tmp":"tmp/mongoose_1.2.0_1785131236215_0.6078282806248636","host":"s3://npm-registry-packages-npm-production"}},"1.3.0":{"name":"@alis-kit/mongoose","version":"1.3.0","keywords":["mongodb","mongoose","decorator","repository","query-builder","soft-delete","ttl"],"license":"MIT","_id":"@alis-kit/mongoose@1.3.0","maintainers":[{"name":"alisdev","email":"ibnu.ali56@gmail.com"}],"dist":{"shasum":"18d4e02a38bc825f6704c25d6556e80ccb3be3c9","tarball":"https://registry.npmjs.org/@alis-kit/mongoose/-/mongoose-1.3.0.tgz","fileCount":110,"integrity":"sha512-El7GVlX6UxbMFGdiJmAz9UUM81lPRapxAZZPrvtShcltf2b8r/hHoXEywU8fjwh8cwvlETOScQrH+HxGGB9SLQ==","signatures":[{"sig":"MEUCIQDWFF4zBGrUhSdH2KOyWrCW7FZJJK4yzTtNgA1c2imRcAIgJ4H8IZPLCSXx84p+ee5dKQJTHdAzDT8C3mjx04UhiRQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":222766},"main":"./dist/index.js","type":"module","_from":"file:alis-kit-mongoose-1.3.0.tgz","types":"./dist/index.d.ts","engines":{"node":">=20.19.0","typescript":">=5.6"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"dev":"tsc --watch","lint":"tsc --noEmit","test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"alisdev","email":"ibnu.ali56@gmail.com"},"_resolved":"C:\\Users\\IBNU~1.BAD\\AppData\\Local\\Temp\\4d0353f3626510ef19372d5ea8e93dfb\\alis-kit-mongoose-1.3.0.tgz","_integrity":"sha512-El7GVlX6UxbMFGdiJmAz9UUM81lPRapxAZZPrvtShcltf2b8r/hHoXEywU8fjwh8cwvlETOScQrH+HxGGB9SLQ==","_npmVersion":"11.6.2","description":"Decorator-based MongoDB schema & repository library with fluent query builder, soft delete, TTL, and auto-populate relations","directories":{},"_nodeVersion":"24.12.0","dependencies":{"zod":"^4.4.3","mongoose":"^9.8.0"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^6.4.3","vitest":"^4.1.10","typescript":"^7.0.2","@types/node":"^26.1.1","mongodb-memory-server":"^11.2.0"},"_npmOperationalInternal":{"tmp":"tmp/mongoose_1.3.0_1785381576979_0.5762681070696112","host":"s3://npm-registry-packages-npm-production"}},"1.3.1":{"name":"@alis-kit/mongoose","version":"1.3.1","keywords":["mongodb","mongoose","decorator","repository","query-builder","soft-delete","ttl"],"license":"MIT","_id":"@alis-kit/mongoose@1.3.1","maintainers":[{"name":"alisdev","email":"ibnu.ali56@gmail.com"}],"dist":{"shasum":"ff802ca72b479af9629d6a40add746610c036b6c","tarball":"https://registry.npmjs.org/@alis-kit/mongoose/-/mongoose-1.3.1.tgz","fileCount":110,"integrity":"sha512-BbJPjbUgE9DYiz17g6pVkGxdWTmds7g/BNeqI/YePyEfssiEgiiTRygISuRETe/o3Si0X1fgBC4dJ2Y63G52hQ==","signatures":[{"sig":"MEUCIQCS0CbCXfkQwNrWC5VYycZIspUvBG/KYv8Ct5g9WrRBUgIgKXB12p7NZZGi1M597b0rI7sYbt+lVGR0x3MMiwC+Y+k=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":223758},"main":"./dist/index.js","type":"module","_from":"file:alis-kit-mongoose-1.3.1.tgz","types":"./dist/index.d.ts","engines":{"node":">=20.19.0","typescript":">=5.6"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"dev":"tsc --watch","lint":"tsc --noEmit","test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"alisdev","email":"ibnu.ali56@gmail.com"},"_resolved":"C:\\Users\\IBNU~1.BAD\\AppData\\Local\\Temp\\7b3d4b558c1179c11680226efe6339f7\\alis-kit-mongoose-1.3.1.tgz","_integrity":"sha512-BbJPjbUgE9DYiz17g6pVkGxdWTmds7g/BNeqI/YePyEfssiEgiiTRygISuRETe/o3Si0X1fgBC4dJ2Y63G52hQ==","_npmVersion":"11.6.2","description":"Decorator-based MongoDB schema & repository library with fluent query builder, soft delete, TTL, and auto-populate relations","directories":{},"_nodeVersion":"24.12.0","dependencies":{"zod":"^4.4.3","mongoose":"^9.8.0"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^6.4.3","vitest":"^4.1.10","typescript":"^7.0.2","@types/node":"^26.1.1","mongodb-memory-server":"^11.2.0"},"_npmOperationalInternal":{"tmp":"tmp/mongoose_1.3.1_1785392603882_0.7471486147610658","host":"s3://npm-registry-packages-npm-production"}},"2.0.0":{"name":"@alis-kit/mongoose","version":"2.0.0","description":"Decorator-based MongoDB schema & repository library with fluent query builder, soft delete, TTL, and auto-populate relations","type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":"./dist/index.js","types":"./dist/index.d.ts"}},"engines":{"node":">=20.19.0","typescript":">=5.6"},"keywords":["mongodb","mongoose","decorator","repository","query-builder","soft-delete","ttl"],"license":"MIT","dependencies":{"mongoose":"^9.8.0","zod":"^4.4.3"},"devDependencies":{"@types/node":"^26.1.1","mongodb-memory-server":"^11.2.0","typescript":"^7.0.2","vite":"^6.4.3","vitest":"^4.1.10"},"scripts":{"build":"tsc","dev":"tsc --watch","test":"vitest run","test:watch":"vitest","lint":"tsc --noEmit"},"_id":"@alis-kit/mongoose@2.0.0","_integrity":"sha512-7dUq0zRioqjfag8iiXhAmT/+WsAAvMf6mboih6V7jZoMJaqtXY9RaSCho/Sy2mOEy+L+Y7XguEhQxILGCx5ErQ==","_resolved":"C:\\Users\\IBNU~1.BAD\\AppData\\Local\\Temp\\60e447bde62c71d9cf977f6aef83150b\\alis-kit-mongoose-2.0.0.tgz","_from":"file:alis-kit-mongoose-2.0.0.tgz","_nodeVersion":"24.12.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-7dUq0zRioqjfag8iiXhAmT/+WsAAvMf6mboih6V7jZoMJaqtXY9RaSCho/Sy2mOEy+L+Y7XguEhQxILGCx5ErQ==","shasum":"1e5316b9e9bf00d460dba7dbc1b12c69de1b8bf4","tarball":"https://registry.npmjs.org/@alis-kit/mongoose/-/mongoose-2.0.0.tgz","fileCount":118,"unpackedSize":302733,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCxrVkR2rtjT9er1MSrQvIc8aaOE3JUxSNpV7l1QRdQBAIhAJtQec6iINpEBBFhefLXS10Op+VIihxwZQ2L2+gbREYI"}]},"_npmUser":{"name":"alisdev","email":"ibnu.ali56@gmail.com"},"directories":{},"maintainers":[{"name":"alisdev","email":"ibnu.ali56@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/mongoose_2.0.0_1786607892360_0.18532173247720363"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-19T03:40:53.946Z","modified":"2026-08-13T07:58:12.711Z","0.1.0":"2026-07-19T03:40:54.240Z","1.0.0":"2026-07-19T03:46:07.196Z","1.1.0":"2026-07-19T15:55:53.738Z","1.2.0":"2026-07-27T05:47:16.366Z","1.3.0":"2026-07-30T03:19:37.124Z","1.3.1":"2026-07-30T06:23:24.024Z","2.0.0":"2026-08-13T07:58:12.511Z"},"license":"MIT","keywords":["mongodb","mongoose","decorator","repository","query-builder","soft-delete","ttl"],"description":"Decorator-based MongoDB schema & repository library with fluent query builder, soft delete, TTL, and auto-populate relations","maintainers":[{"name":"alisdev","email":"ibnu.ali56@gmail.com"}],"readme":"# @alis-kit/mongoose\r\n\r\n> Decorator-based MongoDB schema & repository library with a fluent query builder, soft delete, TTL, custom indexes, and JPA-style eager/lazy relations.\r\n\r\n[![npm version](https://img.shields.io/npm/v/@alis-kit/mongoose.svg)](https://www.npmjs.com/package/@alis-kit/mongoose)\r\n[![license](https://img.shields.io/npm/l/@alis-kit/mongoose.svg)](#license)\r\n\r\n---\r\n\r\n## Table of Contents\r\n\r\n- [Features](#features)\r\n- [Requirements](#requirements)\r\n- [Installation](#installation)\r\n- [Quick Start](#quick-start)\r\n    - [1. Connect to MongoDB](#1-connect-to-mongodb)\r\n    - [2. Define an Entity](#2-define-an-entity)\r\n    - [3. Define Zod Schemas & Types](#3-define-zod-schemas--types)\r\n    - [4. Create a Repository](#4-create-a-repository)\r\n- [Relations (Eager & Lazy)](#relations-eager--lazy)\r\n- [CRUD Operations](#crud-operations)\r\n- [Paginated Search](#paginated-search)\r\n- [Performance & Limits](#performance--limits)\r\n- [Custom Indexes](#custom-indexes)\r\n- [TTL (Time-To-Live)](#ttl-time-to-live)\r\n- [Query Operations Reference](#query-operations-reference)\r\n- [BaseEntity Fields](#baseentity-fields)\r\n- [Real-World Usage](#real-world-usage)\r\n- [Complete Example](#complete-example)\r\n- [Testing](#testing)\r\n- [Architecture](#architecture)\r\n- [License](#license)\r\n\r\n---\r\n\r\n## Features\r\n\r\n- **Native TC39 Decorators** — no `reflect-metadata`, no `experimentalDecorators`\r\n- **Core Architecture** — pure logic in `src/core/`, decorators are thin wrappers\r\n- **Zero `any`** — fully typed codebase\r\n- **MongoDB Connection** — `MongoConnection` utility with lifecycle callbacks\r\n- **Decorator-based Schema** — `@Schema`, `@VirtualField`, `@Repository`\r\n- **Relations** — `@Relation` decorator with JPA-style fetch types: `belongsTo` eager, `hasMany` lazy\r\n- **Fluent Query Builder** — incremental condition building with `$lookup` support\r\n- **Pageable** — `PageResult` (with total) and `SliceResult` (no count query)\r\n- **Soft Delete** — `softDelete()`, `restore()`, auto-filtering\r\n- **Hard Delete** — `delete()` / `hardDelete()` for permanent removal\r\n- **Performance Guard Rails** — result caps, page-size clamp, time budget, `allowDiskUse`\r\n- **Streaming** — `stream()` cursor for exports and migrations, deliberately uncapped\r\n- **TTL (Time-To-Live)** — `@TTL` decorator + per-document `expireAt`\r\n- **Custom Indexes** — `@Index` decorator for unique, compound, text, and geospatial indexes\r\n- **Zod Integration** — `BaseEntitySchema` for runtime validation\r\n- **Audit Fields** — `createdBy`, `updatedBy`, `deletedBy` with actor tracking\r\n- **JSDoc + Examples** — every exported function is documented\r\n\r\n## Requirements\r\n\r\n- Node.js >= 20.19.0\r\n- TypeScript >= 5.6\r\n\r\n## Installation\r\n\r\n```bash\r\nnpm install @alis-kit/mongoose\r\n```\r\n\r\n## Quick Start\r\n\r\n```typescript\r\nimport {\r\n  MongoConnection,\r\n  Schema, VirtualField, Repository, Relation, Index, TTL,\r\n  BaseRepository, BaseEntity, BaseEntitySchema,\r\n  CustomBuilder, SearchCustom, MultipleSearch, CustomOperation,\r\n  Pageable\r\n} from \"@alis-kit/mongoose\";\r\nimport { z } from \"zod\";\r\n```\r\n\r\n> No `reflect-metadata` import needed.\r\n\r\n### 1. Connect to MongoDB\r\n\r\n```typescript\r\n// Simple connection\r\nawait MongoConnection.connect({\r\n  uri: \"mongodb://localhost:27017/mydb\",\r\n});\r\n\r\n// With full options\r\nawait MongoConnection.connect({\r\n  uri: process.env.MONGODB_URI!,\r\n  debug: process.env.NODE_ENV === \"development\",\r\n  options: {\r\n    maxPoolSize: 10,\r\n    serverSelectionTimeoutMS: 5000,\r\n  },\r\n  onConnected: () => console.log(\"MongoDB connected\"),\r\n  onError: (err) => console.error(\"MongoDB error:\", err),\r\n  onDisconnected: () => console.log(\"MongoDB disconnected\"),\r\n});\r\n\r\n// Check connection state\r\nconsole.log(MongoConnection.isConnected()); // true\r\nconsole.log(MongoConnection.getState());    // 'connected'\r\n```\r\n\r\n### 2. Define an Entity\r\n\r\n```typescript\r\n@Schema({ collection: \"products\", timestamps: true })\r\nclass Product extends BaseEntity {\r\n  name!: string;\r\n  price!: number;\r\n}\r\n```\r\n\r\n> **💡 Tip — Create your own base entity with shared fields**\r\n> If you want every entity to automatically include fields like `tenantId`, `createdBy`, or soft-delete markers, extend `BaseEntity` yourself first:\r\n>\r\n> ```typescript\r\n> import { BaseEntity } from '@alis-kit/mongoose';\r\n>\r\n> /** Your app-wide base entity — common fields for all entities */\r\n> export abstract class AppBaseEntity extends BaseEntity {\r\n>   tenantId!: string;\r\n>   createdBy!: string;\r\n>   updatedBy?: string;\r\n>   deletedAt?: Date;\r\n>   deletedBy?: string;\r\n> }\r\n> ```\r\n>\r\n> Then use **your** `AppBaseEntity` instead of `BaseEntity` in domain entities:\r\n>\r\n> ```typescript\r\n> @Schema('users', { timestamps: true })\r\n> class User extends AppBaseEntity {    // ← your custom base\r\n>   @Index({ unique: true })\r\n>   email!: string;\r\n> }\r\n> ```\r\n>\r\n> Now every `User` document automatically includes `tenantId`, `createdBy`, `updatedBy`, and soft-delete fields — no repetition needed. The audit hooks (`pre-save` / `pre-remove`) read these fields automatically; just set them before saving.\r\n\r\nYou can also combine multiple decorators — indexes, virtual fields, and relations — on a single entity:\r\n\r\n```typescript\r\n@Index({ email: 1 }, { unique: true })\r\n@Index({ firstName: \"text\", lastName: \"text\" })\r\n@Schema({ collection: \"users\", timestamps: true })\r\nclass User extends BaseEntity {\r\n  @VirtualField((doc) => `${doc.firstName} ${doc.lastName}`)\r\n  fullName: string;\r\n\r\n  firstName: string;\r\n  lastName: string;\r\n  email: string;\r\n  age: number;\r\n\r\n  // belongsTo defaults to eager — profile is joined on every query\r\n  @Relation({ collection: \"profiles\", localField: \"profileId\" })\r\n  profile: IProfile | null;\r\n}\r\n\r\n// TTL example — sessions expire 30 days after creation\r\n@TTL(\"createdAt\", 2592000)\r\n@Schema({ collection: \"sessions\", timestamps: true })\r\nclass Session extends BaseEntity {\r\n  name!: string;\r\n}\r\n```\r\n\r\n### 3. Define Zod Schemas & Types\r\n\r\n```typescript\r\nconst IProfileSchema = BaseEntitySchema.extend({\r\n  nama: z.string(),\r\n  city: z.string(),\r\n});\r\ntype IProfile = z.infer<typeof IProfileSchema>;\r\n\r\nconst IUserSchema = BaseEntitySchema.extend({\r\n  firstName: z.string(),\r\n  lastName: z.string(),\r\n  fullName: z.string(),\r\n  email: z.string(),\r\n  age: z.number(),\r\n});\r\ntype IUser = z.infer<typeof IUserSchema>;\r\n```\r\n\r\n### 4. Create a Repository\r\n\r\n```typescript\r\n@Repository(User)\r\nclass UserRepository extends BaseRepository<IUser> {\r\n\r\n  // find() is capped at maxUnpaginatedResults (1000) — see Performance & Limits.\r\n  // For an open-ended search, prefer findAll() with a Pageable.\r\n  async findByName(name: string): Promise<IUser[]> {\r\n    const builder = new CustomBuilder<IUser>()\r\n      .with(SearchCustom.of(\"firstName\", CustomOperation.LIKE, name));\r\n    return this.find(builder.build());\r\n  }\r\n\r\n  async search(filter: {\r\n    name?: string;\r\n    city?: string;\r\n    minAge?: number;\r\n    maxAge?: number;\r\n  }, page: number, size: number) {\r\n    const builder = new CustomBuilder<IUser>();\r\n\r\n    if (filter.name) {\r\n      builder.with(SearchCustom.of(\"firstName\", CustomOperation.LIKE, filter.name));\r\n    }\r\n\r\n    if (filter.city) {\r\n      builder.with(\r\n        SearchCustom.of(\"user.profile.city\", CustomOperation.OPERATION_JOIN_EQUAL, filter.city)\r\n      );\r\n    }\r\n\r\n    if (filter.minAge !== undefined && filter.maxAge !== undefined) {\r\n      builder.with(\r\n        MultipleSearch.of(\r\n          SearchCustom.OPERATION_AND,\r\n          SearchCustom.of(\"age\", CustomOperation.GTE, filter.minAge),\r\n          SearchCustom.of(\"age\", CustomOperation.LTE, filter.maxAge)\r\n        )\r\n      );\r\n    }\r\n\r\n    return this.findAll(builder.build(), Pageable.of(page, size));\r\n  }\r\n}\r\n```\r\n\r\n---\r\n\r\n## Relations (Eager & Lazy)\r\n\r\nFetch strategy follows JPA's defaults, and for the same reason: a to-one\r\nreference is bounded, a to-many collection is not.\r\n\r\n| Type | Default fetch | Included in query results? |\r\n|---|---|---|\r\n| `belongsTo` (to-one) | **eager** | Yes, on every read |\r\n| `hasMany`, `hasManyRefs` (to-many) | **lazy** | No — fetch it explicitly |\r\n\r\n```typescript\r\nconst userRepo = new UserRepository();\r\n\r\n// belongsTo is eager — profile comes along automatically\r\nconst user = await userRepo.findById(\"some-id\");\r\n// {\r\n//   _id: \"some-id\",\r\n//   firstName: \"Dudi\",\r\n//   profile: { _id: \"...\", nama: \"Dudi S\", city: \"Jakarta\" },\r\n//   ...\r\n// }\r\n\r\n// hasMany is lazy — user.orders is NOT present above. Fetch it paginated:\r\nconst orders = await userRepo.findRelation(user, \"orders\", Pageable.of(1, 20));\r\n// { content: [{ total: 150000 }, ...], page: 1, total: 348, ... }\r\n\r\n// Or join it into the query itself (the JOIN FETCH equivalent):\r\nconst users = await userRepo.find(undefined, { relations: [\"orders\"] });\r\n```\r\n\r\n> **Why lazy?** A `$lookup` embeds the joined rows *inside* the parent document,\r\n> and a MongoDB document cannot exceed 16MB. A user with 200,000 orders would\r\n> make the whole query fail. `findRelation()` returns a `PageResult` instead, so\r\n> it is not bound by any limit.\r\n\r\n### `@Relation` Options\r\n\r\n```typescript\r\n// belongsTo (default) — eager, result: single object or null\r\n@Relation({ collection: \"profiles\", localField: \"profileId\" })\r\nprofile: IProfile | null;\r\n\r\n// hasMany — lazy, result: array (inverse lookup)\r\n@Relation({\r\n  collection: \"orders\",\r\n  localField: \"_id\",\r\n  foreignField: \"userId\",\r\n  type: \"hasMany\"\r\n})\r\norders: IOrder[];\r\n\r\n// hasManyRefs — lazy, result: array (field stores array of _id references)\r\n@Relation({\r\n  collection: \"tags\",\r\n  localField: \"tagIds\",\r\n  type: \"hasManyRefs\"   // foreignField defaults to \"_id\"\r\n})\r\ntags: ITag[];\r\n\r\n// Override the fetch strategy, cap the join, and narrow the joined fields\r\n@Relation({\r\n  collection: \"orders\",\r\n  localField: \"_id\",\r\n  foreignField: \"userId\",\r\n  type: \"hasMany\",\r\n  fetch: \"eager\",        // opt back into automatic joining\r\n  limit: 10,             // max joined docs embedded (default: relationLimit, 50)\r\n  select: [\"total\"],     // $project inside the $lookup\r\n})\r\norders: IOrder[];\r\n```\r\n\r\n| Option | Default | Description |\r\n|---|---|---|\r\n| `collection` | — | Foreign collection name (required) |\r\n| `localField` | property name | Field holding the reference |\r\n| `foreignField` | `\"_id\"` | Field matched on the foreign side |\r\n| `type` | `\"belongsTo\"` | `belongsTo` \\| `hasMany` \\| `hasManyRefs` |\r\n| `fetch` | eager for `belongsTo`, lazy otherwise | When the relation is joined |\r\n| `limit` | `relationLimit` (50) | Max joined docs embedded (to-many only) |\r\n| `select` | all fields | Fields kept from the joined document |\r\n\r\nSoft-deleted documents are excluded on the joined side too.\r\n\r\n### belongsTo (Many-to-One)\r\n\r\n```typescript\r\n// User has ONE profile → profileId stores the Profile._id\r\n@Schema({ collection: \"users\" })\r\nclass User extends BaseEntity {\r\n  firstName: string;\r\n\r\n  @Relation({ collection: \"profiles\", localField: \"profileId\" })\r\n  profile: IProfile | null;\r\n}\r\n\r\n// Usage — profile auto-populated on ALL queries:\r\nconst user = await userRepo.findById(\"user123\");\r\n// user.profile = { _id: \"...\", nama: \"Dudi\", city: \"Jakarta\" }  ← auto!\r\n\r\nconst users = await userRepo.find();\r\n// users[0].profile = { ... }  ← auto!\r\n\r\nconst paged = await userRepo.findAll(undefined, Pageable.of(1, 10));\r\n// paged.content[0].profile = { ... }  ← auto!\r\n```\r\n\r\n### hasMany (One-to-Many) — lazy\r\n\r\n```typescript\r\n// User has MANY orders → Order.userId references User._id\r\n@Schema({ collection: \"users\" })\r\nclass User extends BaseEntity {\r\n  firstName: string;\r\n\r\n  @Relation({\r\n    collection: \"orders\",\r\n    localField: \"_id\",           // match User._id\r\n    foreignField: \"userId\",      // against Order.userId\r\n    type: \"hasMany\",\r\n  })\r\n  orders: IOrder[];              // ← array of orders\r\n}\r\n\r\n// Usage — lazy, so it is NOT on the query result:\r\nconst user = await userRepo.findById(\"user123\");\r\n// user.orders → undefined\r\n\r\n// Fetch it explicitly, with pagination and no cap:\r\nconst orders = await userRepo.findRelation<IOrder>(user, \"orders\", Pageable.of(1, 20));\r\n// orders.content = [{ _id: \"...\", total: 150000 }, { _id: \"...\", total: 80000 }]\r\n// orders.total   = 348\r\n\r\n// Or force it into the query — capped at `limit` / `relationLimit`:\r\nconst withOrders = await userRepo.findById(\"user123\", { relations: [\"orders\"] });\r\n// withOrders.orders = [{ total: 150000 }, ...]  ← at most 50 by default\r\n```\r\n\r\n### hasManyRefs (Many-to-Many via Embedded IDs) — lazy\r\n\r\n```typescript\r\n// Post has MANY tags → post.tagIds stores an array of Tag._id values\r\n@Schema({ collection: \"posts\" })\r\nclass Post extends BaseEntity {\r\n  title: string;\r\n  content: string;\r\n\r\n  @Relation({\r\n    collection: \"tags\",\r\n    localField: \"tagIds\",       // Post.tagIds is an array of _id strings\r\n    type: \"hasManyRefs\",        // no foreignField needed — defaults to '_id'\r\n  })\r\n  tags: ITag[];                 // ← populated into an array of full tag objects\r\n}\r\n\r\n// Usage — lazy, same as hasMany:\r\nconst post = await postRepo.findById(\"post123\");\r\n// post.tags → undefined\r\n\r\nconst tags = await postRepo.findRelation<ITag>(post, \"tags\");\r\n// tags.content = [{ _id: \"...\", name: \"news\" }, { _id: \"...\", name: \"tech\" }]\r\n\r\n// A tag list is usually small, so eager is reasonable here:\r\n// @Relation({ collection: \"tags\", localField: \"tagIds\", type: \"hasManyRefs\", fetch: \"eager\" })\r\n```\r\n\r\n> 💡 MongoDB's `$lookup` natively handles `localField` as an array — it matches each array element against `foreignField` (`_id` by default). No `$unwind` is applied, so the result is always an array.\r\n\r\n### Multiple Relations on One Entity\r\n\r\n```typescript\r\n@Schema({ collection: \"users\" })\r\nclass User extends BaseEntity {\r\n  firstName: string;\r\n  lastName: string;\r\n\r\n  // belongsTo profile\r\n  @Relation({ collection: \"profiles\", localField: \"profileId\" })\r\n  profile: IProfile | null;\r\n\r\n  // belongsTo department\r\n  @Relation({ collection: \"departments\", localField: \"departmentId\" })\r\n  department: IDepartment | null;\r\n\r\n  // hasMany orders\r\n  @Relation({ collection: \"orders\", localField: \"_id\", foreignField: \"userId\", type: \"hasMany\" })\r\n  orders: IOrder[];\r\n}\r\n\r\n// Both belongsTo relations are eager; orders (hasMany) is lazy:\r\nconst user = await userRepo.findById(\"user123\");\r\n// user.profile     = { nama: \"Dudi\", city: \"Jakarta\" }   ← auto\r\n// user.department  = { name: \"Engineering\" }             ← auto\r\n// user.orders      → undefined                           ← lazy\r\n\r\n// Pull orders separately, paginated:\r\nconst orders = await userRepo.findRelation<IOrder>(user, \"orders\", Pageable.of(1, 20));\r\n\r\n// Or ask for it up front — one query, capped:\r\nconst full = await userRepo.findById(\"user123\", { relations: [\"profile\", \"orders\"] });\r\n// note: naming relations explicitly REPLACES the default set,\r\n// so `department` is not joined here.\r\n```\r\n\r\n### Relation + Query Builder (Combined)\r\n\r\n```typescript\r\n@Repository(User)\r\nclass UserRepository extends BaseRepository<IUser> {\r\n\r\n  // @Relation joins profile (eager) on the result\r\n  // CustomBuilder adds filtering logic\r\n  async searchByCity(city: string, page: number, size: number) {\r\n    const builder = new CustomBuilder<IUser>()\r\n      .with(SearchCustom.of(\r\n        \"user.profile.city\",\r\n        CustomOperation.OPERATION_JOIN_EQUAL,\r\n        city\r\n      ));\r\n\r\n    return this.findAll(builder.build(), Pageable.of(page, size));\r\n  }\r\n}\r\n```\r\n\r\n### Without `@Relation`\r\n\r\n```typescript\r\n// If you DON'T use @Relation, relations are NOT auto-populated.\r\n// You must explicitly use OPERATION_JOIN_* in CustomBuilder:\r\n\r\n@Schema({ collection: \"users\" })\r\nclass User extends BaseEntity {\r\n  firstName: string;\r\n  // No @Relation here — profile NOT auto-populated\r\n}\r\n\r\nconst userRepo = new UserRepository();\r\n\r\n// findById → NO profile data\r\nconst user = await userRepo.findById(\"user123\");\r\n// user = { _id: \"...\", firstName: \"Dudi\" }  ← no profile!\r\n\r\n// To get profile, you must use the builder with a join instead:\r\nconst builder = new CustomBuilder<IUser>()\r\n  .with(SearchCustom.of(\"profileId\", CustomOperation.OPERATION_JOIN_EQUAL, \"profile_id_value\"));\r\n```\r\n\r\n---\r\n\r\n## CRUD Operations\r\n\r\n### Save\r\n\r\n```typescript\r\n// Save with actor\r\nconst newUser = await userRepo.save(\r\n  { firstName: \"Dudi\", lastName: \"Setiawan\", email: \"dudi@email.com\", age: 25, profileId: \"profile_id\" },\r\n  { actorId: \"admin_user_id\" }\r\n);\r\n\r\n// Save from system — createdBy/updatedBy will be null\r\nconst systemUser = await userRepo.save(\r\n  { firstName: \"System\", lastName: \"Bot\", email: \"system@bot.com\", age: 0 }\r\n);\r\n\r\n// Save with TTL — document expires in 1 hour\r\nconst tempUser = await userRepo.save(\r\n  { firstName: \"Temp\", lastName: \"User\", email: \"temp@email.com\", age: 0 },\r\n  { ttl: 3600 }\r\n);\r\n\r\n// Save with exact expiry date\r\nconst scheduledUser = await userRepo.save(\r\n  { firstName: \"Scheduled\", lastName: \"User\", email: \"sched@email.com\", age: 0 },\r\n  { expireAt: new Date(\"2025-12-31T23:59:59Z\") }\r\n);\r\n```\r\n\r\n### Read\r\n\r\n```typescript\r\n// By id\r\nconst user = await userRepo.findById(\"user123\");\r\n\r\n// Several ids at once — use this instead of findById() in a loop (N+1)\r\nconst users = await userRepo.findByIds([\"id1\", \"id2\", \"id3\"]);\r\n\r\n// Existence check without transferring the document\r\nconst taken = await userRepo.exists(\r\n  new CustomBuilder<IUser>()\r\n    .with(SearchCustom.of(\"email\", CustomOperation.EQUAL, \"dudi@email.com\"))\r\n    .build()\r\n);\r\n\r\n// Only the fields you need, no relation joins\r\nconst emails = await userRepo.find(undefined, { select: [\"email\"], relations: false });\r\n```\r\n\r\n### Update\r\n\r\n```typescript\r\n// By id\r\nawait userRepo.update(newUser._id, { age: 26 }, { actorId: \"admin_user_id\" });\r\n\r\n// By condition — first match\r\nawait userRepo.updateOne(query, { age: 26 }, { actorId: \"admin_user_id\" });\r\n\r\n// Bulk — returns the number of documents modified\r\nconst modified = await userRepo.updateMany(query, { status: \"active\" }, { actorId: \"admin\" });\r\n\r\n// Insert when missing, update when present (createdBy is written on insert only)\r\nconst settings = await userRepo.upsert(query, { theme: \"dark\" }, { actorId: \"admin\" });\r\n```\r\n\r\n> Bulk writes accept only `$match` conditions. Join conditions\r\n> (`OPERATION_JOIN_*`) are rejected with an error — MongoDB cannot evaluate a\r\n> join inside a write filter. Resolve the ids first, then pass them with\r\n> `CustomOperation.IN`.\r\n\r\n### Soft Delete & Restore\r\n\r\n```typescript\r\n// Soft delete — document hidden from standard queries\r\nawait userRepo.softDelete(newUser._id, { actorId: \"admin_user_id\" });\r\n\r\n// Find only soft-deleted documents (eager relations joined)\r\nconst deleted = await userRepo.findOnlyDeleted();\r\n\r\n// Include soft-deleted in queries (eager relations joined)\r\nconst all = await userRepo.findWithDeleted();\r\n\r\n// Soft delete in bulk\r\nconst affected = await userRepo.softDeleteMany(\r\n  new CustomBuilder<IUser>()\r\n    .with(SearchCustom.of(\"status\", CustomOperation.EQUAL, \"inactive\"))\r\n    .build(),\r\n  { actorId: \"admin_user_id\" }\r\n);\r\n\r\n// Restore a soft-deleted document\r\nawait userRepo.restore(newUser._id, { actorId: \"admin_user_id\" });\r\n```\r\n\r\n### Hard Delete\r\n\r\n```typescript\r\n// Permanently remove from database (irreversible)\r\nawait userRepo.delete(newUser._id);\r\n// or\r\nawait userRepo.hardDelete(newUser._id);\r\n\r\n// Bulk — also purges already soft-deleted documents\r\nconst removed = await userRepo.deleteMany(query);\r\n```\r\n\r\n---\r\n\r\n## Paginated Search\r\n\r\n```typescript\r\nconst result = await userRepo.search(\r\n  { city: \"Jakarta\", minAge: 18, maxAge: 35 },\r\n  1,\r\n  10\r\n);\r\n// {\r\n//   content: [{ _id: \"...\", firstName: \"Dudi\", profile: { ... }, ... }],\r\n//   page: 1,\r\n//   total: 12,\r\n//   ...\r\n// }\r\n```\r\n\r\n### `PageResult` vs `SliceResult`\r\n\r\n`findAll()` runs a second `count` query to fill in `total`, and that count scales\r\nwith the number of matching documents rather than with the page size. On a large\r\ncollection it becomes the expensive half of the request.\r\n\r\n`findSlice()` skips it entirely — it fetches one extra document to decide\r\n`hasNext`. Use it for infinite scroll and next/previous navigation.\r\n\r\n```typescript\r\n// With total — good for numbered page controls\r\nconst page = await userRepo.findAll(query, Pageable.of(1, 20));\r\n// { content, page, size, total, totalPages, hasPrev, hasNext }\r\n\r\n// Without total — no count query at all\r\nconst slice = await userRepo.findSlice(query, Pageable.of(1, 20));\r\n// { content, page, size, hasPrev, hasNext }\r\n```\r\n\r\n---\r\n\r\n## Performance & Limits\r\n\r\nReads that are **not** paginated are capped, so a query written against a small\r\ndevelopment database cannot quietly turn into a full-collection scan in\r\nproduction.\r\n\r\n```typescript\r\nconst users = await userRepo.find();\r\n// Error: find() on collection \"users\" matched more than 1000 documents.\r\n// Paginate with findAll(query, Pageable.of(page, size)), raise the cap for this\r\n// call via options.limit, or change the global default with\r\n// configurePerformance({ maxUnpaginatedResults }).\r\n```\r\n\r\nConfigure the guard rails once at bootstrap:\r\n\r\n```typescript\r\nimport { configurePerformance } from \"@alis-kit/mongoose\";\r\n\r\nconfigurePerformance({\r\n  maxUnpaginatedResults: 1000,   // cap for find() / findAll() without Pageable\r\n  maxPageSize: 200,              // Pageable.size is clamped to this\r\n  maxTimeMS: 10_000,             // server-side time budget per query; 0 disables\r\n  allowDiskUse: true,            // let aggregations spill past the 100MB limit\r\n  relationLimit: 50,             // max joined docs per to-many relation\r\n  onLimitExceeded: \"throw\",      // or \"truncate\" to return the first N instead\r\n});\r\n```\r\n\r\n| Option | Default | What it protects against |\r\n|---|---|---|\r\n| `maxUnpaginatedResults` | `1000` | Loading a whole collection into memory |\r\n| `maxPageSize` | `200` | A client sending `?size=1000000` |\r\n| `maxTimeMS` | `10000` | A runaway query holding resources |\r\n| `allowDiskUse` | `true` | `QueryExceededMemoryLimitNoDiskUseAllowed` |\r\n| `relationLimit` | `50` | A joined array blowing the 16MB document limit |\r\n| `onLimitExceeded` | `\"throw\"` | Silently dropping documents |\r\n\r\nPer call, `QueryOptions` overrides the defaults:\r\n\r\n```typescript\r\nawait userRepo.find(query, {\r\n  select: [\"email\", \"firstName\"],  // $project — smaller payloads\r\n  relations: false,                // skip relation joins entirely\r\n  limit: 5000,                     // raise the cap just here\r\n  maxTimeMS: 30_000,\r\n});\r\n```\r\n\r\n### Streaming (uncapped, on purpose)\r\n\r\nWhen you genuinely need every document — an export, a migration — use a cursor\r\nrather than raising the cap. Memory stays flat regardless of collection size.\r\n\r\n```typescript\r\nfor await (const user of userRepo.stream()) {\r\n  await writeCsvRow(user);\r\n}\r\n```\r\n\r\n### Raw aggregation\r\n\r\n```typescript\r\n// Prefer this over this.model.aggregate() — the raw model bypasses every guard rail.\r\n// Note: no soft-delete filter is injected, add it yourself.\r\nconst stats = await userRepo.aggregate([\r\n  { $match: { deletedAt: null } },\r\n  { $group: { _id: \"$city\", count: { $sum: 1 } } },\r\n]);\r\n```\r\n\r\n---\r\n\r\n## Custom Indexes\r\n\r\n```typescript\r\n// Unique index\r\n@Index({ email: 1 }, { unique: true })\r\n\r\n// Compound index\r\n@Index({ category: 1, price: -1 })\r\n\r\n// Text search index\r\n@Index({ firstName: \"text\", lastName: \"text\" })\r\n\r\n// Geospatial index\r\n@Index({ location: \"2dsphere\" })\r\n\r\n// Sparse index\r\n@Index({ optionalField: 1 }, { sparse: true })\r\n```\r\n\r\n### Indexes created for you\r\n\r\nEvery entity automatically gets:\r\n\r\n- `{ expireAt: 1 }` TTL index, unless `@TTL` already covers `expireAt`\r\n- `{ deletedAt: 1, createdAt: -1 }` — covers the default \"list newest, excluding\r\n  deleted\" query without an in-memory sort (just `{ deletedAt: 1 }` when\r\n  `timestamps: false`)\r\n- `{ <localField>: 1 }` for every `@Relation`, skipping `_id` and any field you\r\n  already declared with `@Index`\r\n\r\n> These cover the library's own query patterns, **not yours**. A filter on\r\n> `status` sorted by `createdAt` still needs its own `@Index({ status: 1, createdAt: -1 })`.\r\n> With `allowDiskUse` enabled a missing index no longer crashes the query — it\r\n> just gets quietly slow, so declare them deliberately.\r\n\r\n---\r\n\r\n## TTL (Time-To-Live)\r\n\r\n### Entity-level TTL\r\n\r\n```typescript\r\n// Documents expire 24 hours after creation\r\n@TTL(\"createdAt\", 86400)\r\n@Schema({ collection: \"otps\" })\r\nclass OTP extends BaseEntity {\r\n  code!: string;\r\n  userId!: string;\r\n}\r\n```\r\n\r\n### Per-document TTL\r\n\r\n```typescript\r\n// Expires in 5 minutes\r\nawait otpRepo.save({ code: \"123456\", userId: \"user1\" }, { ttl: 300 });\r\n\r\n// Expires at a specific date\r\nawait otpRepo.save({ code: \"789012\", userId: \"user2\" }, {\r\n  expireAt: new Date(\"2025-06-01T00:00:00Z\")\r\n});\r\n```\r\n\r\n> MongoDB's background thread checks TTL indexes every ~60 seconds and removes expired documents automatically.\r\n\r\n---\r\n\r\n## Query Operations Reference\r\n\r\n| Operation | Description | MongoDB Equivalent |\r\n|-----------|-------------|---------------------|\r\n| `EQUAL` | Exact match | `{ field: value }` |\r\n| `NOT_EQUAL` | Not equal | `{ $ne: value }` |\r\n| `GT` | Greater than | `{ $gt: value }` |\r\n| `GTE` | Greater than or equal | `{ $gte: value }` |\r\n| `LT` | Less than | `{ $lt: value }` |\r\n| `LTE` | Less than or equal | `{ $lte: value }` |\r\n| `LIKE` | Case-insensitive contains | `{ $regex: value, $options: 'i' }` |\r\n| `STARTS_WITH` | Case-insensitive starts with | `{ $regex: '^value', $options: 'i' }` |\r\n| `ENDS_WITH` | Case-insensitive ends with | `{ $regex: 'value$', $options: 'i' }` |\r\n| `STARTS_WITH_CASE_SENSITIVE` | Starts with — **index-backed** | `{ $regex: '^value' }` |\r\n| `TEXT_SEARCH` | Full-text search — needs a text index | `{ $text: { $search: value } }` |\r\n| `IN` | In array | `{ $in: [values] }` |\r\n| `NOT_IN` | Not in array | `{ $nin: [values] }` |\r\n| `IS_NULL` | Is null | `{ field: null }` |\r\n| `IS_NOT_NULL` | Is not null | `{ $ne: null }` |\r\n| `EXISTS` | Field exists | `{ $exists: true }` |\r\n| `NOT_EXISTS` | Field doesn't exist | `{ $exists: false }` |\r\n\r\n> All operations also have `OPERATION_JOIN_*` variants that trigger `$lookup` for cross-collection queries.\r\n\r\n### Searching large collections\r\n\r\n`LIKE`, `STARTS_WITH` and `ENDS_WITH` are case-insensitive, which means MongoDB\r\n**always scans the whole collection** — fine on 10k documents, fatal on 5M.\r\nTwo index-backed alternatives:\r\n\r\n```typescript\r\n// Typeahead / prefix search — an anchored, case-sensitive regex is the only\r\n// string pattern a normal index can serve.\r\nSearchCustom.of(\"firstName\", CustomOperation.STARTS_WITH_CASE_SENSITIVE, \"Du\")\r\n\r\n// Free-text search — declare a text index on the entity first:\r\n//   @Index({ firstName: \"text\", lastName: \"text\" })\r\nSearchCustom.of(\"any\", CustomOperation.TEXT_SEARCH, \"dudi setiawan\")\r\n```\r\n\r\n`$text` searches every field covered by the text index, so the field name passed\r\nto `SearchCustom.of()` is ignored. MongoDB only allows `$text` in the first\r\npipeline stage, so a query can carry at most one text condition and it cannot\r\nsit inside an `OR` group.\r\n\r\n---\r\n\r\n## BaseEntity Fields\r\n\r\n| Field | Type | Description |\r\n|-------|------|--------------|\r\n| `_id` | `string` | MongoDB document ID |\r\n| `createdAt` | `Date` | Auto-managed by Mongoose |\r\n| `updatedAt` | `Date` | Auto-managed by Mongoose |\r\n| `createdBy` | `string \\| null` | Set via `actorId` on save |\r\n| `updatedBy` | `string \\| null` | Set via `actorId` on save/update |\r\n| `deletedAt` | `Date \\| null` | Set on soft delete, `null` = active |\r\n| `deletedBy` | `string \\| null` | Set on soft delete |\r\n| `expireAt` | `Date \\| null` | TTL expiry date |\r\n\r\n---\r\n\r\n## Real-World Usage\r\n\r\n### Express / NestJS App Bootstrap\r\n\r\n```typescript\r\n// src/database.ts\r\nimport { MongoConnection } from \"@alis-kit/mongoose\";\r\n\r\nexport async function connectDatabase() {\r\n  await MongoConnection.connect({\r\n    uri: process.env.MONGODB_URI || \"mongodb://localhost:27017/myapp\",\r\n    debug: process.env.NODE_ENV === \"development\",\r\n    options: {\r\n      maxPoolSize: 10,\r\n      minPoolSize: 2,\r\n      serverSelectionTimeoutMS: 5000,\r\n      socketTimeoutMS: 45000,\r\n    },\r\n    onConnected: () => console.log(\"MongoDB connected\"),\r\n    onError: (err) => console.error(\"MongoDB error:\", err.message),\r\n    onDisconnected: () => console.log(\"MongoDB disconnected\"),\r\n  });\r\n}\r\n\r\n// src/app.ts\r\nimport express from \"express\";\r\nimport { connectDatabase } from \"./database\";\r\nimport { MongoConnection } from \"@alis-kit/mongoose\";\r\n\r\nconst app = express();\r\n\r\n// Connect before starting server\r\nconnectDatabase().then(() => {\r\n  app.listen(3000, () => console.log(\"Server running on :3000\"));\r\n});\r\n\r\n// Health check endpoint\r\napp.get(\"/health\", (req, res) => {\r\n  res.json({\r\n    db: MongoConnection.isConnected(),\r\n    dbState: MongoConnection.getState(),\r\n  });\r\n});\r\n\r\n// Graceful shutdown\r\nprocess.on(\"SIGTERM\", async () => {\r\n  await MongoConnection.disconnect();\r\n  process.exit(0);\r\n});\r\n```\r\n\r\n---\r\n\r\n## Complete Example\r\n\r\nAn end-to-end e-commerce example combining entities, relations, TTL, indexes, and repositories.\r\n\r\n```typescript\r\nimport {\r\n  MongoConnection, Schema, VirtualField, Repository, Relation,\r\n  Index, TTL, BaseRepository, BaseEntity, BaseEntitySchema,\r\n  CustomBuilder, SearchCustom, CustomOperation, Pageable\r\n} from \"@alis-kit/mongoose\";\r\nimport { z } from \"zod\";\r\n\r\n// ── Connect ───────────────────────────────────────────────────────\r\nawait MongoConnection.connect({ uri: \"mongodb://localhost:27017/shop\" });\r\n\r\n// ── Entities ──────────────────────────────────────────────────────\r\n@Schema({ collection: \"categories\" })\r\nclass Category extends BaseEntity { name: string; }\r\n\r\n@Index({ sku: 1 }, { unique: true })\r\n@Index({ name: \"text\", description: \"text\" })\r\n@Schema({ collection: \"products\" })\r\nclass Product extends BaseEntity {\r\n  name: string;\r\n  sku: string;\r\n  price: number;\r\n  description: string;\r\n\r\n  @Relation({ collection: \"categories\", localField: \"categoryId\" })\r\n  category: ICategory | null;\r\n}\r\n\r\n@TTL(\"createdAt\", 900)  // OTP expires in 15 minutes\r\n@Schema({ collection: \"otps\" })\r\nclass OTP extends BaseEntity { code: string; userId: string; }\r\n\r\n// ── Types ─────────────────────────────────────────────────────────\r\nconst ICategorySchema = BaseEntitySchema.extend({\r\n  name: z.string(),\r\n});\r\ntype ICategory = z.infer<typeof ICategorySchema>;\r\n\r\nconst IProductSchema = BaseEntitySchema.extend({\r\n  name: z.string(),\r\n  sku: z.string(),\r\n  price: z.number(),\r\n  description: z.string(),\r\n  category: ICategorySchema.nullable(),\r\n});\r\ntype IProduct = z.infer<typeof IProductSchema>;\r\n\r\nconst IOTPSchema = BaseEntitySchema.extend({\r\n  code: z.string(),\r\n  userId: z.string(),\r\n});\r\ntype IOTP = z.infer<typeof IOTPSchema>;\r\n\r\n// ── Repositories ──────────────────────────────────────────────────\r\n@Repository(Category)\r\nclass CategoryRepository extends BaseRepository<ICategory> {}\r\n\r\n@Repository(Product)\r\nclass ProductRepository extends BaseRepository<IProduct> {\r\n  async searchProducts(keyword: string, minPrice?: number, maxPrice?: number) {\r\n    const builder = new CustomBuilder<IProduct>();\r\n    builder.with(SearchCustom.of(\"name\", CustomOperation.LIKE, keyword));\r\n    if (minPrice) builder.with(SearchCustom.of(\"price\", CustomOperation.GTE, minPrice));\r\n    if (maxPrice) builder.with(SearchCustom.of(\"price\", CustomOperation.LTE, maxPrice));\r\n    return this.findAll(builder.build(), Pageable.of(1, 20, \"price\", \"asc\"));\r\n  }\r\n}\r\n\r\n@Repository(OTP)\r\nclass OTPRepository extends BaseRepository<IOTP> {}\r\n\r\n// ── Usage ─────────────────────────────────────────────────────────\r\nconst categoryRepo = new CategoryRepository();\r\nconst productRepo = new ProductRepository();\r\nconst otpRepo = new OTPRepository();\r\n\r\n// Create category\r\nconst category = await categoryRepo.save(\r\n  { name: \"Electronics\" },\r\n  { actorId: \"admin_id\" }\r\n);\r\n\r\n// Create product with TTL\r\nconst product = await productRepo.save(\r\n  { name: \"Laptop\", sku: \"LPT-001\", price: 15000000, description: \"Gaming laptop\", categoryId: category._id },\r\n  { actorId: \"admin_id\" }\r\n);\r\n\r\n// findById — category auto-populated!\r\nconst found = await productRepo.findById(product._id);\r\n// found.category = { _id: \"...\", name: \"Electronics\" }\r\n\r\n// Search products\r\nconst results = await productRepo.searchProducts(\"laptop\", 1000000, 20000000);\r\n\r\n// Create OTP (expires in 15 minutes)\r\nconst otp = await otpRepo.save(\r\n  { code: \"123456\", userId: \"user1\" },\r\n  { ttl: 900 }\r\n);\r\n\r\n// Soft delete\r\nawait productRepo.softDelete(product._id, { actorId: \"admin_id\" });\r\n\r\n// Restore\r\nawait productRepo.restore(product._id, { actorId: \"admin_id\" });\r\n\r\n// Hard delete (permanent)\r\nawait productRepo.hardDelete(product._id);\r\n```\r\n\r\n---\r\n\r\n## Testing\r\n\r\n```bash\r\n# Run all tests\r\npnpm test\r\n\r\n# Watch mode\r\npnpm test:watch\r\n\r\n# Typecheck\r\npnpm lint\r\n```\r\n\r\n---\r\n\r\n## Architecture\r\n\r\nThis library follows the **Functional Core + Decorator Sugar** architecture:\r\n\r\n```\r\nsrc/\r\n├── core/          # Pure functions (logic lives here)\r\n│   ├── registry.ts\r\n│   ├── schema-builder.ts\r\n│   ├── match-expression.ts\r\n│   ├── query-pipeline.ts\r\n│   ├── relation-resolver.ts\r\n│   ├── connection-manager.ts\r\n│   ├── repository-factory.ts\r\n│   ├── audit.ts\r\n│   ├── performance.ts\r\n│   └── utils.ts\r\n├── decorators/    # Thin wrappers → call core/\r\n│   ├── Schema.ts\r\n│   ├── Repository.ts\r\n│   ├── Index.ts\r\n│   ├── TTL.ts\r\n│   ├── Relation.ts\r\n│   └── VirtualField.ts\r\n├── base/          # Base classes\r\n│   ├── BaseEntity.ts\r\n│   └── BaseRepository.ts\r\n├── builder/       # Query builder (already decoupled)\r\n│   ├── CustomBuilder.ts\r\n│   ├── CustomOperation.ts\r\n│   ├── MultipleSearch.ts\r\n│   └── SearchCustom.ts\r\n└── pageable/      # Pagination\r\n    ├── Pageable.ts\r\n    ├── PageResult.ts\r\n    └── SliceResult.ts\r\n```\r\n\r\n**Rules:**\r\n- All logic lives in `core/` — pure functions, no decorators\r\n- Decorators only call `core/` — no logic inside decorators\r\n- Runtime reflection via `Map` registries — no `reflect-metadata`\r\n\r\n---\r\n\r\n## License\r\n\r\nMIT","readmeFilename":"README.md"}