{"_id":"@adinet/indigodb","_rev":"5-db580d29fa98bdceb60362fb76ae5da5","name":"@adinet/indigodb","dist-tags":{"latest":"3.1.0"},"versions":{"1.0.0":{"name":"@adinet/indigodb","version":"1.0.0","keywords":["ORM","PostgreSQL","MongoDB","Real-time","WebSockets","TypeScript"],"author":{"name":"Sergio Galaz","email":"sergiogalaz60@gmail.com"},"license":"MIT","_id":"@adinet/indigodb@1.0.0","maintainers":[{"name":"chechoo","email":"sergiogalaz60@gmail.com"}],"homepage":"https://github.com/Adinet-CL/indigodb#readme","bugs":{"url":"https://github.com/Adinet-CL/indigodb/issues"},"dist":{"shasum":"0854985ec66dab4588a1a30bbd450228de0a5a1e","tarball":"https://registry.npmjs.org/@adinet/indigodb/-/indigodb-1.0.0.tgz","fileCount":11,"integrity":"sha512-gEILa//WRvngBccIYIijRht8hxDGy3lt9Fy2EPB4Poajzto2UeCPzVZEv6fZ40xA7XZeKsvVwpKeKvE5MUK3vA==","signatures":[{"sig":"MEYCIQD4o8/WErY2aAhT3zHXDklAucHdtRK4tcwXAiqgWTIwiQIhAKgu49FUFC7sIo59pXseUiUBI9Xp+jbZnzy0CrkLSzSZ","keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA"}],"unpackedSize":29716},"main":"dist/index.js","types":"dist/index.d.ts","gitHead":"cb6c2508837d257c3e3a7c4ab2ebfe337106db0b","scripts":{"test":"jest","build":"tsc","prepublishOnly":"npm run build"},"_npmUser":{"name":"chechoo","email":"sergiogalaz60@gmail.com"},"repository":{"url":"git+https://github.com/Adinet-CL/indigodb.git","type":"git"},"_npmVersion":"8.15.0","description":"ORM for PostgreSQL and MongoDB with real-time support","directories":{},"_nodeVersion":"16.17.0","dependencies":{"pg":"^8.7.1","dotenv":"^16.4.5","events":"^3.3.0","mongodb":"^6.11.0","mongoose":"^6.0.12"},"_hasShrinkwrap":false,"devDependencies":{"ws":"^8.18.0","jest":"^29.0.0","ts-jest":"^29.0.0","@types/pg":"^8.11.10","@types/ws":"^8.5.13","typescript":"^4.4.4","@types/jest":"^29.0.0","@types/node":"^16.11.7","@types/mongodb":"^4.0.7"},"_npmOperationalInternal":{"tmp":"tmp/indigodb_1.0.0_1732458623082_0.3365155900854515","host":"s3://npm-registry-packages"}},"1.0.2":{"name":"@adinet/indigodb","version":"1.0.2","keywords":["ORM","PostgreSQL","MongoDB","Real-time","WebSockets","TypeScript"],"author":{"name":"Sergio Galaz","email":"sergiogalaz60@gmail.com"},"license":"MIT","_id":"@adinet/indigodb@1.0.2","maintainers":[{"name":"chechoo","email":"sergiogalaz60@gmail.com"}],"homepage":"https://github.com/Adinet-CL/indigodb#readme","bugs":{"url":"https://github.com/Adinet-CL/indigodb/issues"},"dist":{"shasum":"50ca3cf2451436f7ab5036a9b4e6f1de88b4b620","tarball":"https://registry.npmjs.org/@adinet/indigodb/-/indigodb-1.0.2.tgz","fileCount":14,"integrity":"sha512-5puYYy5wwz1x2nK9ePHitI8aHXlsVKim2sOWRLXe551AqgfRKtJUV+AGmg0l68PksmzqIafHMudzTR5oTjKGpg==","signatures":[{"sig":"MEUCICaBHzAjDE4fkrhp4/6/Zg7/wXSzWtQY7DSHeuSmzNDWAiEAlwtu5X4J7wB3IqOa9zORT0EEFfK0J8YPZ59Y5gSuYds=","keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA"}],"unpackedSize":28229},"main":"dist/index.js","types":"dist/index.d.ts","gitHead":"51c7e3adf976fc13e0cf812a3924454485cc029e","scripts":{"test":"jest","build":"tsc","prepublishOnly":"npm run build"},"_npmUser":{"name":"chechoo","email":"sergiogalaz60@gmail.com"},"repository":{"url":"git+https://github.com/Adinet-CL/indigodb.git","type":"git"},"_npmVersion":"8.15.0","description":"ORM for PostgreSQL and MongoDB with real-time support","directories":{},"_nodeVersion":"16.17.0","dependencies":{"pg":"^8.7.1","dotenv":"^16.4.5","events":"^3.3.0","mongodb":"^6.11.0","mongoose":"^6.0.12"},"_hasShrinkwrap":false,"devDependencies":{"ws":"^8.18.0","jest":"^29.0.0","ts-jest":"^29.0.0","@types/pg":"^8.11.10","@types/ws":"^8.5.13","typescript":"^4.4.4","@types/jest":"^29.0.0","@types/node":"^16.11.7","@types/mongodb":"^4.0.7"},"_npmOperationalInternal":{"tmp":"tmp/indigodb_1.0.2_1732459325558_0.7619339820792939","host":"s3://npm-registry-packages"}},"2.0.0":{"name":"@adinet/indigodb","version":"2.0.0","keywords":["ORM","PostgreSQL","MongoDB","Real-time","WebSockets","TypeScript"],"author":{"name":"Sergio Galaz","email":"sergiogalaz60@gmail.com"},"license":"MIT","_id":"@adinet/indigodb@2.0.0","maintainers":[{"name":"chechoo","email":"sergiogalaz60@gmail.com"}],"homepage":"https://github.com/Adinet-CL/indigodb#readme","bugs":{"url":"https://github.com/Adinet-CL/indigodb/issues"},"dist":{"shasum":"c64d71b4d8429e0a1510582e2b857a1b37c34864","tarball":"https://registry.npmjs.org/@adinet/indigodb/-/indigodb-2.0.0.tgz","fileCount":66,"integrity":"sha512-xlvpCQZ6r09kiL040iUwJ0f5/x8zbjzi2d3CynwFOU3L/P5eOO9V7DIq1baNjmZ1jX4Tocqm7HbG5ONurnKGEA==","signatures":[{"sig":"MEQCIGkqdbMbAzkgtq7CTmpVDflqPYrV9BtGqczMLYAxi+gpAiBZenS5D19KFyUsCIIkefX6QjjdVfSNzdSWaEHfTR7PUw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":86292},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=18"},"gitHead":"dbbffcb5cf0aa137c03d9bb7c5080b8a9b98e288","scripts":{"test":"jest","build":"tsc -p tsconfig.build.json","prepublishOnly":"npm run build","test:integration":"INDIGODB_INTEGRATION=1 jest tests/integration --runInBand"},"_npmUser":{"name":"chechoo","email":"sergiogalaz60@gmail.com"},"repository":{"url":"git+https://github.com/Adinet-CL/indigodb.git","type":"git"},"_npmVersion":"11.12.1","description":"Lightweight ORM for PostgreSQL and MongoDB with real-time updates over WebSockets","directories":{},"_nodeVersion":"22.13.1","dependencies":{"pg":"^8.13.0","ws":"^8.18.0","mongodb":"^6.11.0"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^29.0.0","dotenv":"^16.4.5","ts-jest":"^29.0.0","@types/pg":"^8.11.10","@types/ws":"^8.5.13","typescript":"^5.6.0","@types/jest":"^29.0.0","@types/node":"^20.0.0"},"_npmOperationalInternal":{"tmp":"tmp/indigodb_2.0.0_1784473639068_0.9493393715631016","host":"s3://npm-registry-packages-npm-production"}},"3.0.0":{"name":"@adinet/indigodb","version":"3.0.0","keywords":["ORM","PostgreSQL","MongoDB","Real-time","WebSockets","TypeScript"],"author":{"name":"Sergio Galaz","email":"sergiogalaz60@gmail.com"},"license":"MIT","_id":"@adinet/indigodb@3.0.0","maintainers":[{"name":"chechoo","email":"sergiogalaz60@gmail.com"}],"homepage":"https://github.com/Adinet-CL/indigodb#readme","bugs":{"url":"https://github.com/Adinet-CL/indigodb/issues"},"bin":{"indigodb-migrate":"dist/cli/migrate.js"},"dist":{"shasum":"8c8579527ea0f0afac7d1ab1f9331657787bd440","tarball":"https://registry.npmjs.org/@adinet/indigodb/-/indigodb-3.0.0.tgz","fileCount":103,"integrity":"sha512-aWC5G9YVzVZWx8OBN0VKR3WyMns13s9MiFh2irmP2BMYqSanwA9nW7Wx2fNVTSr+axEuKhhqW7RgEjjCZQCoIA==","signatures":[{"sig":"MEUCIDtRlOFO4UsPaBAqYsK0qj6vb4RYOB3qOZz8cGzQdcOzAiEA8fu+wcTx7W2gmEfRldAp5XDCHzdX+pUj8zXKfYEHhOs=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":230918},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./client":{"types":"./dist/client/realtimeClient.d.ts","default":"./dist/client/realtimeClient.js"}},"gitHead":"75b3fa2841134593e929cb86c4e76b8c3e10bda1","scripts":{"docs":"typedoc","lint":"eslint . && prettier --check src tests","test":"jest","build":"tsc -p tsconfig.build.json","format":"prettier --write src tests","prepublishOnly":"npm run build","test:integration":"INDIGODB_INTEGRATION=1 jest tests/integration --runInBand"},"_npmUser":{"name":"chechoo","email":"sergiogalaz60@gmail.com"},"repository":{"url":"git+https://github.com/Adinet-CL/indigodb.git","type":"git"},"_npmVersion":"11.12.1","description":"Lightweight ORM for PostgreSQL and MongoDB with real-time updates over WebSockets","directories":{},"_nodeVersion":"22.13.1","dependencies":{"pg":"^8.13.0","ws":"^8.18.0","mongodb":"^6.11.0"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^29.0.0","dotenv":"^16.4.5","eslint":"^10.7.0","ts-jest":"^29.0.0","typedoc":"^0.28.20","prettier":"^3.9.5","@types/pg":"^8.11.10","@types/ws":"^8.5.13","typescript":"^5.6.0","@types/jest":"^29.0.0","@types/node":"^20.0.0","typescript-eslint":"^8.64.0"},"_npmOperationalInternal":{"tmp":"tmp/indigodb_3.0.0_1784493365625_0.6822441068550826","host":"s3://npm-registry-packages-npm-production"}},"3.1.0":{"name":"@adinet/indigodb","version":"3.1.0","description":"Lightweight ORM for PostgreSQL and MongoDB with real-time updates over WebSockets","main":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./client":{"types":"./dist/client/realtimeClient.d.ts","default":"./dist/client/realtimeClient.js"}},"bin":{"indigodb-migrate":"dist/cli/migrate.js"},"scripts":{"build":"tsc -p tsconfig.build.json","test":"jest","test:integration":"INDIGODB_INTEGRATION=1 jest tests/integration --runInBand","lint":"eslint . && prettier --check src tests","format":"prettier --write src tests","docs":"typedoc","prepublishOnly":"npm run build"},"keywords":["ORM","PostgreSQL","MongoDB","Real-time","WebSockets","TypeScript"],"author":{"name":"Sergio Galaz","email":"sergiogalaz60@gmail.com"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/Adinet-CL/indigodb.git"},"bugs":{"url":"https://github.com/Adinet-CL/indigodb/issues"},"homepage":"https://github.com/Adinet-CL/indigodb#readme","engines":{"node":">=18"},"dependencies":{"mongodb":"^6.11.0","pg":"^8.13.0","ws":"^8.18.0"},"devDependencies":{"@types/jest":"^29.0.0","@types/node":"^20.0.0","@types/pg":"^8.11.10","@types/ws":"^8.5.13","dotenv":"^16.4.5","eslint":"^10.7.0","jest":"^29.0.0","prettier":"^3.9.5","ts-jest":"^29.0.0","typedoc":"^0.28.20","typescript":"^5.6.0","typescript-eslint":"^8.64.0"},"gitHead":"69bd34994b8761f03b7c99b9871d8acee31c8e05","_id":"@adinet/indigodb@3.1.0","_nodeVersion":"22.13.1","_npmVersion":"11.12.1","dist":{"integrity":"sha512-uCu6bj514P0AtJQBHBUeItuyNKkzA7Kagc8cebdAliTPgOZM8HdPc4XXHUgUloKRh2bKUbrx/h9xvTpH7YNkUw==","shasum":"8d2c6f5b8585e2a4369d62f2f36830b4821fcdde","tarball":"https://registry.npmjs.org/@adinet/indigodb/-/indigodb-3.1.0.tgz","fileCount":103,"unpackedSize":242856,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCpZNsDP5R04tjUaLn5uC5s/KHqpE2ZUkBcP615/VTP0QIhALoZH2JLXbfp7tX3/IsIjJud1XLWSZwt8l09u1vCm+be"}]},"_npmUser":{"name":"chechoo","email":"sergiogalaz60@gmail.com"},"directories":{},"maintainers":[{"name":"chechoo","email":"sergiogalaz60@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/indigodb_3.1.0_1784510692592_0.6332253901589684"},"_hasShrinkwrap":false}},"time":{"created":"2024-11-24T14:30:22.992Z","modified":"2026-07-20T01:24:52.891Z","1.0.0":"2024-11-24T14:30:23.294Z","1.0.2":"2024-11-24T14:42:05.724Z","2.0.0":"2026-07-19T15:07:19.243Z","3.0.0":"2026-07-19T20:36:05.789Z","3.1.0":"2026-07-20T01:24:52.733Z"},"bugs":{"url":"https://github.com/Adinet-CL/indigodb/issues"},"author":{"name":"Sergio Galaz","email":"sergiogalaz60@gmail.com"},"license":"MIT","homepage":"https://github.com/Adinet-CL/indigodb#readme","keywords":["ORM","PostgreSQL","MongoDB","Real-time","WebSockets","TypeScript"],"repository":{"type":"git","url":"git+https://github.com/Adinet-CL/indigodb.git"},"description":"Lightweight ORM for PostgreSQL and MongoDB with real-time updates over WebSockets","maintainers":[{"name":"chechoo","email":"sergiogalaz60@gmail.com"}],"readme":"# IndigoDB\n\n[![CI](https://github.com/Adinet-CL/indigodb/actions/workflows/ci.yml/badge.svg)](https://github.com/Adinet-CL/indigodb/actions/workflows/ci.yml)\n[![npm version](https://img.shields.io/npm/v/%40adinet%2Findigodb)](https://www.npmjs.com/package/@adinet/indigodb)\n[![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)\n\nIndigoDB is a lightweight ORM for Node.js that works against **either PostgreSQL or MongoDB**, with real-time change notifications pushed to clients over a built-in WebSocket server. It's inspired by Firebase's real-time database behavior: define a model, run CRUD, and connected clients are notified of every insert/update/delete automatically.\n\n> **v2+ is a full rewrite** with a new instance-based API, opt-in real-time, typed models, and a pluggable adapter architecture. See the [Migration guide](#migration-from-v1) if you are upgrading from v1.\n\n## Features\n\n- **Dual database support** — one API over PostgreSQL and MongoDB, swapped by config.\n- **Rich query engine** — Mongo-style operators (`$gt`, `$in`, `$like`, `$or`, ...), pagination, projection, bulk operations, and a `raw()` escape hatch.\n- **Relations** — `hasMany` / `belongsTo` with batched eager loading via `include`.\n- **Transactions** — `db.transaction()` with automatic commit/rollback on both backends.\n- **Migrations** — `indigodb-migrate` CLI + programmatic `MigrationRunner`.\n- **Real-time updates (opt-in)** — Postgres triggers + `LISTEN/NOTIFY` and MongoDB change streams are fanned out to WebSocket clients, with filtered subscriptions, pluggable auth, and a dependency-free frontend client.\n- **Schema features** — `required`, `default`, indexes, automatic timestamps, lifecycle hooks.\n- **Fully typed** — `defineModel<T>()` returns a typed `Model<T>`; no `any` leaking into your code.\n- **Safe by default** — table/column identifiers are validated (anti SQL-injection) and all values are parameterized.\n- **Explicit lifecycle** — `connect()` / `close()` cleanly open and release every resource (pool, listener, change streams, WebSocket server).\n- **Injectable logger** — the library is silent unless you pass a `Logger`.\n\n## Installation\n\n```bash\nnpm install @adinet/indigodb\n```\n\n`pg`, `mongodb`, and `ws` are regular runtime dependencies — no extra peer installs required.\n\n## Quick start\n\n```typescript\nimport { IndigoDB, DataTypes } from \"@adinet/indigodb\";\n\ninterface User {\n  id: number;\n  name: string;\n  email: string;\n}\n\nconst db = new IndigoDB({\n  database: {\n    type: \"postgresql\",\n    host: \"localhost\",\n    port: 5432,\n    user: \"db_user\",\n    password: \"db_password\",\n    database: \"db_name\",\n  },\n  realtime: { enabled: true, port: 8080 }, // omit to skip the WebSocket server\n});\n\nawait db.connect();\n\nconst Users = await db.defineModel<User>(\"users\", {\n  id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },\n  name: { type: DataTypes.STRING },\n  email: { type: DataTypes.STRING, unique: true },\n});\n\nconst user = await Users.create({ name: \"John Doe\", email: \"john@example.com\" });\nconst all = await Users.findAll();\nconst byId = await Users.findById(user.id);\nawait Users.update(user.id, { name: \"Jane Doe\" });\nawait Users.delete(user.id);\n\n// In-process subscription to every change (works even without WebSockets):\ndb.on(\"change\", (event) => {\n  console.log(event); // { model, operation: \"INSERT\" | \"UPDATE\" | \"DELETE\", data }\n});\n\nawait db.close(); // releases the pool, listener, change streams and WS server\n```\n\n### MongoDB\n\n```typescript\nconst db = new IndigoDB({\n  database: {\n    type: \"mongodb\",\n    connectionString: \"mongodb://localhost:27017/mydb\",\n  },\n  realtime: { enabled: true, port: 8080 },\n});\n```\n\nMongoDB documents use `_id` as the primary key automatically; string ids that look like an `ObjectId` are converted for you. Change streams require the MongoDB deployment to be a **replica set**.\n\n## Real-time frontend integration\n\n### Plain WebSocket\n\nConnect to the WebSocket server and listen for updates:\n\n```typescript\nconst socket = new WebSocket(\"ws://localhost:8080\");\n\nsocket.onmessage = (event) => {\n  const { event: name, data } = JSON.parse(event.data);\n  // name === \"databaseUpdate\"\n  // data === { model, operation, data }\n  console.log(\"Real-time update:\", data);\n};\n```\n\nThe payload shape is identical whether the change originated in PostgreSQL or MongoDB, so a single frontend handler covers both backends. By default a client receives **every** change event across every model.\n\n### Filtered subscriptions\n\nSend a `subscribe` message right after connecting to narrow what you receive — by model, by a `Where` filter (same operator syntax as [Querying](#querying)), or both. A later `subscribe` message replaces the previous filter:\n\n```typescript\nsocket.onopen = () => {\n  socket.send(JSON.stringify({\n    type: \"subscribe\",\n    models: [\"orders\"],\n    where: { status: \"urgent\" },\n  }));\n};\n```\n\n### `@adinet/indigodb/client`\n\nA small, dependency-free client (works in any environment with a global `WebSocket` — browsers, or Node 22+) handles connecting, sending the subscribe filter, re-subscribing after a reconnect, and exponential backoff:\n\n```typescript\nimport { RealtimeClient } from \"@adinet/indigodb/client\";\n\nconst client = new RealtimeClient({\n  url: \"ws://localhost:8080\",\n  models: [\"orders\"],\n  where: { status: \"urgent\" },\n});\nclient.connect();\n\nconst unsubscribe = client.on((event) => {\n  console.log(event.model, event.operation, event.data);\n});\n```\n\n### WebSocket authentication\n\nPass `realtime.authenticate` to validate connections (token in a header, query string, cookie — whatever your app uses) before they're accepted; refusing a connection closes the socket with code `4001`:\n\n```typescript\nconst db = new IndigoDB({\n  database: { /* ... */ },\n  realtime: {\n    enabled: true,\n    authenticate: (request) => {\n      const token = new URL(request.url ?? \"\", \"http://x\").searchParams.get(\"token\");\n      return token === process.env.REALTIME_TOKEN;\n    },\n  },\n});\n```\n\n## API reference\n\n### `new IndigoDB(config)`\n\n| Config field | Description |\n| --- | --- |\n| `database` | Discriminated by `type`. PostgreSQL: `{ type: \"postgresql\", host, port, user, password, database }` (or `connectionString`). MongoDB: `{ type: \"mongodb\", connectionString, database? }`. |\n| `realtime` | Optional. `{ enabled: boolean, port?: number, authenticate? }` — defaults to port `8080`. When omitted or `enabled: false`, **no WebSocket server is started**. `authenticate(request)` (optional) runs per connection; return `false` to refuse it. |\n| `logger` | Optional `Logger`. Defaults to a no-op; pass `consoleLogger` (exported) or your own. |\n\n### Methods\n\n- **`connect(): Promise<void>`** — connects the adapter (fails fast on bad credentials) and starts the WebSocket server if real-time is enabled.\n- **`defineModel<T>(name, schema, options?): Promise<Model<T>>`** — creates the table/collection (indexes, Postgres triggers) and resolves once it is ready. **Async** — always `await` it. `options.timestamps: true` adds managed `createdAt`/`updatedAt` columns.\n- **`on(\"change\", listener)`** — subscribe to `ChangeEvent`s in-process (inherited from `EventEmitter`).\n- **`transaction<R>(fn): Promise<R>`** — see [Transactions](#transactions).\n- **`close(): Promise<void>`** — stops the WebSocket server, closes change streams, and drains the connection pool / client.\n\n### Model methods\n\n| Method | Description |\n| --- | --- |\n| `create(data)` | Insert a record; returns the created row. |\n| `createMany(data[])` | Bulk insert (single multi-row INSERT / `insertMany`); returns the created rows. |\n| `findAll(where?, options?)` | Query with operators, `orderBy`, `limit`, `offset`, `select`, `include` (see [Relations](#relations)). |\n| `findOne(where?, options?)` | First match or `null`. |\n| `findById(id)` | Return a record by its primary key, or `null`. |\n| `count(where?)` | Number of matching records. |\n| `exists(where?)` | `true` if at least one record matches. |\n| `update(id, data)` | Update by primary key; returns the updated row or `null`. |\n| `updateMany(where, data)` | Bulk update; returns the number of affected records. |\n| `delete(id)` | Delete by primary key; returns the deleted row or `null`. |\n| `deleteMany(where)` | Bulk delete; returns the number of removed records. |\n\nThe primary key is taken from the column marked `primaryKey: true` in the schema (PostgreSQL), or defaults to `_id` (MongoDB).\n\n## Schema features\n\nBeyond `type` / `primaryKey` / `autoIncrement` / `unique`, columns support:\n\n```typescript\nconst Accounts = await db.defineModel<Account>(\n  \"accounts\",\n  {\n    id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },\n    email: { type: DataTypes.STRING, required: true, unique: true },\n    plan: { type: DataTypes.STRING, default: \"free\" }, // static value...\n    apiKey: { type: DataTypes.STRING, default: () => crypto.randomUUID() }, // ...or a factory\n    status: { type: DataTypes.STRING, index: true }, // non-unique index\n  },\n  { timestamps: true } // adds managed createdAt / updatedAt columns\n);\n```\n\n- **`required`** — rejects `create()`/`createMany()` calls missing the column (after defaults are applied) with a `ValidationError`.\n- **`default`** — a static value or a zero-argument factory invoked per row when the column is omitted.\n- **`index`** — creates a non-unique index; `unique` (already available) creates a unique index on both backends.\n- **`timestamps`** (model option) — `createdAt` is stamped on `create()`; `updatedAt` is stamped on `create()` and refreshed on every `update()` / `updateMany()`.\n- **`references`** — `{ model, column? }`, a foreign key hint (see [Relations](#relations)).\n\n## Relations\n\n```typescript\nconst Users = await db.defineModel<User>(\"users\", {\n  id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },\n  name: { type: DataTypes.STRING },\n});\n\n// Define the target of a reference BEFORE the model that references it —\n// PostgreSQL needs the referenced table to already exist.\nconst Posts = await db.defineModel<Post>(\"posts\", {\n  id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },\n  title: { type: DataTypes.STRING },\n  userId: { type: DataTypes.INTEGER, references: { model: \"users\" } },\n});\n\nUsers.hasMany(Posts, { foreignKey: \"userId\", as: \"posts\" });\nPosts.belongsTo(Users, { foreignKey: \"userId\", as: \"author\" });\n\nconst usersWithPosts = await Users.findAll({}, { include: [\"posts\"] });\nconsole.log(usersWithPosts[0].posts); // Post[]\n\nconst postsWithAuthor = await Posts.findAll({}, { include: [\"author\"] });\nconsole.log(postsWithAuthor[0].author); // User | null\n```\n\n- **`references`** (PostgreSQL) adds a `REFERENCES` constraint at table-creation time. On MongoDB it's documentation only — there's no native FK enforcement.\n- **`include`** runs **one batched query per association** (via `$in` against the target model, reusing its own `findAll()`) — never one query per row, and identical on both backends since no JOIN/`$lookup` is involved. Including an association that wasn't registered with `hasMany`/`belongsTo` throws `ConfigurationError`.\n\n## Lifecycle hooks\n\n```typescript\nAccounts.hooks.beforeCreate((data) => ({ email: (data.email as string).toLowerCase() }));\nAccounts.hooks.afterCreate((account) => sendWelcomeEmail(account.email));\nAccounts.hooks.beforeDelete((id) => auditLog.record(\"account.delete\", id));\n```\n\nAvailable hooks: `beforeCreate`, `afterCreate`, `beforeUpdate`, `afterUpdate`, `beforeDelete`, `afterDelete`. A `before*` hook may return a partial payload that gets merged into the pending data (returning nothing leaves it unchanged); `after*` hooks are for side effects and run with the persisted record. Hooks run for single-row `create()` / `update()` / `delete()` only — bulk operations (`createMany` / `updateMany` / `deleteMany`) skip them by default, so a bulk call never silently re-fetches every affected row.\n\n## Transactions\n\n```typescript\nawait db.transaction(async (tx) => {\n  const accounts = tx.getModel(Accounts);\n  const transfers = tx.getModel(Transfers);\n\n  const from = await accounts.update(fromId, { balance: fromBalance - amount });\n  const to = await accounts.update(toId, { balance: toBalance + amount });\n  await transfers.create({ fromId, toId, amount });\n\n  if (from!.balance < 0) throw new Error(\"insufficient funds\"); // rolls everything back\n});\n```\n\n`tx.getModel(model)` exchanges an already-`defineModel`'d instance for a clone bound to the transaction — same schema and **the same hooks registry**, so hooks registered on the original model still fire. Every query made through the clone runs inside the transaction; throwing anywhere in the callback rolls it back and rejects `db.transaction()` with that error, otherwise it commits automatically.\n\n- **PostgreSQL**: a dedicated pooled connection running `BEGIN` / `COMMIT` / `ROLLBACK`.\n- **MongoDB**: a `ClientSession` (requires a replica set — same constraint as change streams).\n\n## Querying\n\nFilters use Mongo-style operators on both backends — compiled to parameterized SQL on PostgreSQL, passed (almost) natively to MongoDB. A plain value still means equality:\n\n```typescript\nconst results = await Users.findAll(\n  {\n    age: { $gte: 18, $lt: 65 },          // comparisons\n    name: { $like: \"A%\" },               // SQL LIKE (regex-safe on MongoDB)\n    role: { $in: [\"admin\", \"editor\"] },  // membership\n    deletedAt: { $null: true },          // IS NULL\n    $or: [{ plan: \"pro\" }, { credits: { $gt: 0 } }],\n  },\n  {\n    orderBy: { name: \"asc\" },\n    limit: 20,\n    offset: 40,\n    select: [\"id\", \"name\", \"email\"],\n  }\n);\n```\n\nOperators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$like`, `$null`, plus `$or` / `$and` combinators. Every column name in a filter is validated against the model schema, and every value is parameterized — unknown columns or malformed operators throw `QueryError` / `UnknownColumnError` before touching the database.\n\n### Raw escape hatch\n\nWhen the query engine isn't enough:\n\n```typescript\n// PostgreSQL — parameterized SQL:\nconst { rows } = (await db.raw(\n  \"SELECT name, COUNT(*) FROM users GROUP BY name HAVING COUNT(*) > $1\",\n  [1]\n)) as { rows: unknown[] };\n\n// MongoDB — a command document:\nawait db.raw({ ping: 1 });\n```\n\n## Supported data types\n\n| IndigoDB type | PostgreSQL | MongoDB |\n| --- | --- | --- |\n| `INTEGER` | `INTEGER` | `Number` |\n| `BIGINT` | `BIGINT` | `Number` (JS precision limit applies — see below) |\n| `FLOAT` | `REAL` | `Number` |\n| `DOUBLE` | `DOUBLE PRECISION` | `Number` |\n| `DECIMAL` | `NUMERIC` (or `NUMERIC(precision, scale)`) | `String` (exact precision — see below) |\n| `STRING` | `VARCHAR(255)` (or `VARCHAR(length)`) | `String` |\n| `TEXT` | `TEXT` | `String` |\n| `BOOLEAN` | `BOOLEAN` | `Boolean` |\n| `DATE` | `TIMESTAMP` | `Date` |\n| `DATEONLY` | `DATE` | `Date` |\n| `UUID` | `UUID` | `String` |\n| `ENUM` | `TEXT` + `CHECK` constraint | `String`, validated in the ORM |\n| `BINARY` | `BYTEA` | `Buffer`, passed through unchanged |\n| `JSON` | `JSONB` | `Object` |\n\nSome types take extra options on the column definition:\n\n```typescript\nconst Accounts = await db.defineModel<Account>(\"accounts\", {\n  id: { type: DataTypes.UUID, primaryKey: true, default: () => crypto.randomUUID() },\n  code: { type: DataTypes.STRING, length: 12 },              // VARCHAR(12) instead of the default 255\n  balance: { type: DataTypes.DECIMAL, precision: 12, scale: 2 }, // NUMERIC(12, 2)\n  status: { type: DataTypes.ENUM, values: [\"active\", \"suspended\", \"closed\"] },\n});\n```\n\n- **`DECIMAL`** is returned as a `string` on both backends (PostgreSQL's driver already does this for `NUMERIC`; MongoDB has no fixed-point type, so IndigoDB stores it as a string too) — a JS `Number` would silently round money-style values. Parse with a decimal library if you need arithmetic.\n- **`BIGINT`** is still coerced through `Number`, so values beyond `Number.MAX_SAFE_INTEGER` (2^53) lose precision — fine for most IDs/counters, but avoid it for values that can legitimately exceed that range.\n- **`ENUM`** requires `values: string[]`; anything outside that list is rejected with a `ValidationError` on `create()`/`update()` (and the bulk variants) on **both** backends — PostgreSQL additionally enforces it at the database level with a `CHECK` constraint.\n- Misconfigured columns (`ENUM` without `values`, `length`/`precision`/`scale` used on the wrong type, a non-positive `length`) throw `ConfigurationError` at `defineModel()` time.\n\n## Architecture\n\nIndigoDB is intentionally small and built around a few classic patterns so new backends and transports are easy to add:\n\n- **Adapter** — `DatabaseAdapter` has one implementation per backend (`PostgresAdapter`, `MongoAdapter`). `IndigoDB` never contains `if (type === ...)` CRUD branches.\n- **Template Method** — `BaseModel<T>` defines the CRUD contract and centralizes identifier/schema validation and primary-key resolution; each backend model fills in the specifics.\n- **Observer** — adapters emit a uniform `ChangeEvent`; `IndigoDB` re-emits it and forwards it to the real-time gateway.\n- **Strategy** — `RealtimeGateway` abstracts the transport; `WebSocketGateway` is the default, and real-time is fully optional.\n\n```\nadapter.emitChange() ──▶ IndigoDB.emit(\"change\") ──▶ your listener\n                                    └────────────▶ gateway.broadcast() ──▶ WebSocket clients\n```\n\n- **PostgreSQL** detects changes with a per-table trigger that calls `pg_notify` on the `indigodb_changes` channel; a dedicated `LISTEN` client (separate from the query `Pool`) receives them.\n- **MongoDB** detects changes with a `collection.watch()` change stream (requires a replica set).\n\n## Schema migrations\n\n`CREATE TABLE IF NOT EXISTS` (run by `defineModel`) never alters an existing table, so schema changes on a live database need real migrations. IndigoDB ships a small runner plus a CLI:\n\n```bash\nnpx indigodb-migrate create \"add users table\"   # scaffolds migrations/<timestamp>_add_users_table.js\nnpx indigodb-migrate up                          # applies every pending migration\nnpx indigodb-migrate down                        # reverts the most recently applied one\nnpx indigodb-migrate status                      # { applied, pending }\n```\n\nThe CLI reads `indigodb.config.js` (or `--config <path>`) from the working directory:\n\n```javascript\n// indigodb.config.js\nmodule.exports = {\n  database: { type: \"postgresql\", host: \"localhost\", database: \"myapp\" },\n  migrationsDir: \"./migrations\", // optional, defaults to \"./migrations\"\n};\n```\n\nA migration file exports `up`/`down` functions that receive a `MigrationContext` — the same `raw()` escape hatch as `db.raw()`:\n\n```javascript\n// migrations/1700000000000_add_users_table.js\nmodule.exports = {\n  async up(ctx) {\n    await ctx.raw(\"CREATE TABLE users (id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE)\");\n  },\n  async down(ctx) {\n    await ctx.raw(\"DROP TABLE users\");\n  },\n};\n```\n\nApplied migrations are tracked in a history table/collection (default name `indigodb_migrations`) defined the same way any other model is — no backend-specific bookkeeping. You can also drive it programmatically:\n\n```typescript\nimport { MigrationRunner } from \"@adinet/indigodb\";\n\nconst runner = new MigrationRunner(db, { directory: \"./migrations\" });\nawait runner.up();\n```\n\n## Testing\n\nThe default suite is fully mocked and needs **no database**:\n\n```bash\nnpm test\n```\n\nOpt-in integration tests run against live databases (PostgreSQL, and MongoDB as a replica set). Copy `.env.example` to `.env`, fill in your connection details, then:\n\n```bash\nnpm run test:integration\n```\n\nCI runs the unit suite on Node 18/20/22 and the full integration suite against real Postgres and Mongo (single-node replica set) containers on every PR.\n\n## Development\n\n```bash\nnpm run lint     # ESLint + Prettier check\nnpm run format   # Prettier write\nnpm run docs     # Generate API docs (typedoc) into docs/\n```\n\n## Migration from v1\n\nv2 is a breaking change. Key differences:\n\n| v1 | v2 |\n| --- | --- |\n| `import { initialize, defineModel } from \"indigodb\"` (hidden singleton) | `import { IndigoDB } from \"@adinet/indigodb\"; const db = new IndigoDB(config)` |\n| `initialize({ databaseType, host, ... })` | `new IndigoDB({ database: { type, host, ... } })` + `await db.connect()` |\n| `defineModel()` was synchronous and returned `any` | `await db.defineModel<T>()` returns a typed `Model<T>` |\n| WebSocket server always started | `realtime` is opt-in |\n| Postgres records required a hardcoded `_id` column | primary key comes from the `primaryKey: true` column in your schema |\n| No way to shut down (tests hung) | `await db.close()` releases everything |\n\n## Roadmap\n\nSee [ROADMAP.md](./ROADMAP.md) for the full gap analysis and release plan: schema features + hooks (v2.2), transactions (v2.3), migrations (v2.4), advanced real-time (v2.5), and relations (v3.0).\n\n## Contributing\n\n1. Fork the repository.\n2. Create a branch (`feature/my-feature`).\n3. Commit your changes.\n4. Push and open a pull request.\n\n## License\n\nMIT — see [LICENSE](./LICENSE).\n","readmeFilename":"README.md"}