{"_id":"@andycosow/jardb","_rev":"2-091dd25480174590c361acefa7285271","name":"@andycosow/jardb","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.0":{"name":"@andycosow/jardb","version":"1.0.0","keywords":["database","nosql","mongodb","sqlite","embedded","local","json","schema-less","jardb"],"author":{"name":"Andrius Kasovskis","email":"kasowskis@gmail.com"},"license":"MIT","_id":"@andycosow/jardb@1.0.0","maintainers":[{"name":"andycosow","email":"kasowskis@gmail.com"}],"dist":{"shasum":"e173c119146192c7b64137728ff7b7d142c67226","tarball":"https://registry.npmjs.org/@andycosow/jardb/-/jardb-1.0.0.tgz","fileCount":4,"integrity":"sha512-Vaw21tmDe5mEqHTg2TDpLpcVm6K/eOHH0nQ5iBnFGF7tRDaogW0cktSK/luMjLxdZ1kB2dVBQPgee35boIJVSQ==","signatures":[{"sig":"MEYCIQCDoEBWAhzTZdBQpvIrMzWJrYU2oWALoVmvYR3hFXvflQIhAP453iPCz6ev5Gwi2reYWRlhmB/mvYGL5bcXbgcmawi0","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":57196},"main":"src/index.js","type":"module","engines":{"node":">=18.0.0"},"exports":{".":{"import":"./src/index.js"}},"gitHead":"339ffa5ec54110d4c493e412943f976e251c5c90","scripts":{"test":"node --test tests/"},"_npmUser":{"name":"andycosow","email":"kasowskis@gmail.com"},"_npmVersion":"11.13.0","description":"A lightweight, embedded NoSQL database for Node.js with MongoDB-like syntax, powered by SQLite.","directories":{},"_nodeVersion":"22.12.0","dependencies":{"better-sqlite3":"^11.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/jardb_1.0.0_1787679891962_0.011488681862475536","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@andycosow/jardb","version":"1.0.1","description":"A lightweight, embedded NoSQL database for Node.js with MongoDB-like syntax, powered by SQLite.","main":"src/index.js","type":"module","exports":{".":{"import":"./src/index.js"}},"scripts":{"test":"node --test tests/"},"keywords":["database","nosql","mongodb","sqlite","embedded","local","json","schema-less","jardb"],"author":{"name":"Andrius Kasovskis","email":"kasowskis@gmail.com"},"license":"MIT","dependencies":{"better-sqlite3":"^11.0.0"},"engines":{"node":">=18.0.0"},"publishConfig":{"access":"public"},"gitHead":"339ffa5ec54110d4c493e412943f976e251c5c90","_id":"@andycosow/jardb@1.0.1","_nodeVersion":"22.12.0","_npmVersion":"11.13.0","dist":{"integrity":"sha512-T2TlmZ/9oC67IABPq6llbqNGq2rM50QrFUgNB23F3KlhOoch7SayZH8p7gWF7rnZ+zzh1nNy6h6f5U7uQKCvPQ==","shasum":"3687d9d1a83b21bdf02ef2700122def9640a1fa9","tarball":"https://registry.npmjs.org/@andycosow/jardb/-/jardb-1.0.1.tgz","fileCount":4,"unpackedSize":57181,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDB/rAz+RACe2VLJBs9/2+bA0UPs5+BU9gZ5qw8hU69ZQIgTfoz0cx2nDfOr3wezO2alamwHDJxM72wWtdYksXhadU="}]},"_npmUser":{"name":"andycosow","email":"kasowskis@gmail.com"},"directories":{},"maintainers":[{"name":"andycosow","email":"kasowskis@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/jardb_1.0.1_1787680429634_0.4557978579848485"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-25T17:44:51.804Z","modified":"2026-08-25T17:53:49.913Z","1.0.0":"2026-08-25T17:44:52.109Z","1.0.1":"2026-08-25T17:53:49.765Z"},"author":{"name":"Andrius Kasovskis","email":"kasowskis@gmail.com"},"license":"MIT","keywords":["database","nosql","mongodb","sqlite","embedded","local","json","schema-less","jardb"],"description":"A lightweight, embedded NoSQL database for Node.js with MongoDB-like syntax, powered by SQLite.","maintainers":[{"name":"andycosow","email":"kasowskis@gmail.com"}],"readme":"# jarDB\n\nA lightweight document-oriented database built on top of SQLite using [`better-sqlite3`](https://www.npmjs.com/package/better-sqlite3).\n\n`jarDB` provides a MongoDB-like API for storing JSON documents while retaining SQLite's simplicity, local persistence, transactions, indexing, and SQL-backed performance.\n\nIt is designed for small to medium Node.js applications that need a simple embedded document database without running a separate database server.\n\n## Features\n\n* 💾 Persistent SQLite storage\n* 📄 JSON document storage\n* 🗂️ Collection-based API\n* 🔍 MongoDB-style query operators\n* ✏️ Atomic document updates\n* 🔄 Upsert support\n* 🗑️ Single and bulk deletes\n* 📊 Basic aggregation pipelines\n* ⚡ SQLite JSON expression indexes\n* 🔎 Regular-expression queries\n* ↕️ Sorting, pagination, and projections\n* 🔐 Sanitized collection and field identifiers\n* 🧵 SQLite WAL journal mode\n* 🆔 Automatic UUID document IDs\n* 🔒 Prepared statements for database values\n* 📦 No external database server required\n\n## Installation\n\nInstall `jarDB` and its SQLite dependency:\n\n```bash\nnpm install @andycosow/jardb\n```\n\n## Requirements\n\n* Node.js with ES module support\n* SQLite support provided by `better-sqlite3`\n* A Node.js version compatible with the installed `better-sqlite3` release\n\n## Basic Usage\n\n```js\nimport { jarDB } from \"@andycosow/jardb\";\n\nconst db = new jarDB(\"./data/app.db\");\n\nconst users = db.collection(\"users\");\n\nconst result = users.insertOne({\n  name: \"Alice\",\n  age: 28,\n  email: \"alice@example.com\",\n});\n\nconsole.log(result);\n\nconst user = users.findOne({\n  email: \"alice@example.com\",\n});\n\nconsole.log(user);\n\ndb.close();\n```\n\nA generated document looks similar to:\n\n```js\n{\n  _id: \"8d7d6d3a-...\",\n  name: \"Alice\",\n  age: 28,\n  email: \"alice@example.com\"\n}\n```\n\nIf `_id` is not supplied, `jarDB` automatically generates a UUID.\n\n---\n\n# Database API\n\n## `new jarDB(dbPath)`\n\nCreates or opens a SQLite database.\n\n```js\nconst db = new jarDB(\"./data/app.db\");\n```\n\n### Parameters\n\n| Parameter | Type     | Default         | Description               |\n| --------- | -------- | --------------- | ------------------------- |\n| `dbPath`  | `string` | `\"./jar-db.db\"` | SQLite database file path |\n\nThe database automatically enables SQLite WAL mode:\n\n```sql\nPRAGMA journal_mode = WAL;\n```\n\n---\n\n## `db.collection(name)`\n\nCreates a collection if it does not already exist and returns the collection instance.\n\n```js\nconst users = db.collection(\"users\");\nconst products = db.collection(\"products\");\n```\n\nCollection names may contain:\n\n* Letters\n* Numbers\n* `_`\n* `.`\n\nExamples:\n\n```js\ndb.collection(\"users\");\ndb.collection(\"app.users\");\ndb.collection(\"user_profiles\");\n```\n\nInvalid identifiers throw a `JarDBError`.\n\n---\n\n## `db.listCollections()`\n\nReturns the names of all SQLite tables in the database.\n\n```js\nconst collections = db.listCollections();\n\nconsole.log(collections);\n```\n\nExample:\n\n```js\n[\n  \"users\",\n  \"products\",\n  \"orders\"\n]\n```\n\n---\n\n## `db.dropCollection(name)`\n\nDrops a collection completely.\n\n```js\ndb.dropCollection(\"users\");\n```\n\nThis permanently removes the underlying SQLite table and all documents stored in it.\n\n---\n\n## `db.close()`\n\nCloses the SQLite database connection.\n\n```js\ndb.close();\n```\n\nAlways close the database when your application no longer needs it.\n\n---\n\n# Collection API\n\nOnce a collection has been created:\n\n```js\nconst users = db.collection(\"users\");\n```\n\nyou can use the CRUD and query methods below.\n\n---\n\n# Insert Documents\n\n## `insertOne(document)`\n\nInserts one document.\n\n```js\nconst result = users.insertOne({\n  name: \"Alice\",\n  age: 28,\n  active: true,\n});\n```\n\nReturns:\n\n```js\n{\n  acknowledged: true,\n  insertedId: \"generated-or-provided-id\"\n}\n```\n\n### Custom `_id`\n\nYou can provide your own ID:\n\n```js\nusers.insertOne({\n  _id: \"user-001\",\n  name: \"Alice\",\n  age: 28,\n});\n```\n\nDuplicate IDs result in a `JarDBError` with code:\n\n```text\nDUPLICATE_ID\n```\n\n---\n\n## `insertMany(documents)`\n\nInserts multiple documents in a SQLite transaction.\n\n```js\nconst result = users.insertMany([\n  {\n    name: \"Alice\",\n    age: 28,\n  },\n  {\n    name: \"Bob\",\n    age: 35,\n  },\n  {\n    name: \"Charlie\",\n    age: 42,\n  },\n]);\n```\n\nReturns an array:\n\n```js\n[\n  {\n    acknowledged: true,\n    insertedId: \"...\"\n  },\n  {\n    acknowledged: true,\n    insertedId: \"...\"\n  },\n  {\n    acknowledged: true,\n    insertedId: \"...\"\n  }\n]\n```\n\nBecause the operation uses a transaction, the batch is handled atomically by SQLite.\n\n---\n\n# Find Documents\n\n## `find(filter, options)`\n\nFinds documents matching a filter.\n\n```js\nconst users = db.collection(\"users\");\n\nconst results = users.find({\n  age: 30,\n});\n```\n\nIf no filter is supplied, all documents are returned:\n\n```js\nconst users = users.find();\n```\n\n---\n\n# Equality Queries\n\nSimple values perform equality matching.\n\n```js\nusers.find({\n  name: \"Alice\",\n});\n```\n\nMultiple fields are combined with `AND`:\n\n```js\nusers.find({\n  active: true,\n  age: 28,\n});\n```\n\n---\n\n# Nested Fields\n\nFields can be accessed using dot notation.\n\nFor documents such as:\n\n```js\n{\n  name: \"Alice\",\n  address: {\n    city: \"Nairobi\",\n    country: \"Kenya\"\n  }\n}\n```\n\nyou can query:\n\n```js\nusers.find({\n  \"address.city\": \"Nairobi\",\n});\n```\n\nNested fields can also be sorted and indexed.\n\n---\n\n# Query Operators\n\n`jarDB` supports the following query operators.\n\n| Operator  | Description                      |\n| --------- | -------------------------------- |\n| `$eq`     | Equal                            |\n| `$ne`     | Not equal                        |\n| `$gt`     | Greater than                     |\n| `$gte`    | Greater than or equal            |\n| `$lt`     | Less than                        |\n| `$lte`    | Less than or equal               |\n| `$in`     | Value exists in an array         |\n| `$nin`    | Value does not exist in an array |\n| `$regex`  | Regular expression matching      |\n| `$exists` | Tests whether a field exists     |\n| `$not`    | Negates a condition              |\n| `$or`     | Logical OR                       |\n| `$and`    | Logical AND                      |\n\n## `$eq`\n\n```js\nusers.find({\n  age: {\n    $eq: 30,\n  },\n});\n```\n\n## `$ne`\n\n```js\nusers.find({\n  status: {\n    $ne: \"inactive\",\n  },\n});\n```\n\n## `$gt`\n\n```js\nusers.find({\n  age: {\n    $gt: 18,\n  },\n});\n```\n\n## `$gte`\n\n```js\nusers.find({\n  age: {\n    $gte: 18,\n  },\n});\n```\n\n## `$lt`\n\n```js\nusers.find({\n  age: {\n    $lt: 65,\n  },\n});\n```\n\n## `$lte`\n\n```js\nusers.find({\n  age: {\n    $lte: 65,\n  },\n});\n```\n\n---\n\n# `$in`\n\nMatches values contained in an array.\n\n```js\nusers.find({\n  role: {\n    $in: [\"admin\", \"moderator\"],\n  },\n});\n```\n\nYou can also use an array directly as shorthand:\n\n```js\nusers.find({\n  role: [\"admin\", \"moderator\"],\n});\n```\n\n---\n\n# `$nin`\n\nMatches values that are not contained in an array.\n\n```js\nusers.find({\n  role: {\n    $nin: [\"banned\", \"suspended\"],\n  },\n});\n```\n\n---\n\n# `$regex`\n\nPerforms regular-expression matching.\n\n```js\nusers.find({\n  name: {\n    $regex: \"^Ali\",\n  },\n});\n```\n\nFor example:\n\n```js\nusers.find({\n  email: {\n    $regex: \"@example\\\\.com$\",\n  },\n});\n```\n\nThe regular expression is evaluated using JavaScript's `RegExp`.\n\n> Note: Although the query syntax recognizes `$options`, the current implementation does not actually pass `$options` flags into the `RegExp` constructor. Case-insensitive matching therefore should not be assumed to work through `$options: \"i\"`.\n\n---\n\n# `$exists`\n\nFind documents where a field exists:\n\n```js\nusers.find({\n  email: {\n    $exists: true,\n  },\n});\n```\n\nFind documents where it does not exist:\n\n```js\nusers.find({\n  email: {\n    $exists: false,\n  },\n});\n```\n\n---\n\n# `$not`\n\nNegates a condition.\n\n```js\nusers.find({\n  age: {\n    $not: {\n      $gt: 18,\n    },\n  },\n});\n```\n\n---\n\n# Logical Operators\n\n## `$or`\n\n```js\nusers.find({\n  $or: [\n    { role: \"admin\" },\n    { role: \"moderator\" },\n  ],\n});\n```\n\n## `$and`\n\n```js\nusers.find({\n  $and: [\n    { active: true },\n    { age: { $gte: 18 } },\n  ],\n});\n```\n\n---\n\n# Null Queries\n\nTo find documents where a field is `NULL`/missing:\n\n```js\nusers.find({\n  deletedAt: null,\n});\n```\n\n---\n\n# `findOne()`\n\nReturns the first matching document or `null`.\n\n```js\nconst user = users.findOne({\n  email: \"alice@example.com\",\n});\n```\n\nExample:\n\n```js\nif (user) {\n  console.log(user.name);\n}\n```\n\n---\n\n# Count Documents\n\n## `count(filter)`\n\nCounts documents matching a filter.\n\n```js\nconst count = users.count({\n  active: true,\n});\n\nconsole.log(count);\n```\n\nCount everything:\n\n```js\nconst total = users.count();\n```\n\n---\n\n# Sorting\n\nUse the `sort` option.\n\n```js\nconst users = users.find(\n  {},\n  {\n    sort: {\n      age: 1,\n    },\n  }\n);\n```\n\nUse `1` for ascending and `-1` for descending:\n\n```js\nusers.find(\n  {},\n  {\n    sort: {\n      age: -1,\n    },\n  }\n);\n```\n\nMultiple sort fields are supported:\n\n```js\nusers.find(\n  {},\n  {\n    sort: {\n      country: 1,\n      age: -1,\n    },\n  }\n);\n```\n\nNested fields can also be sorted:\n\n```js\nusers.find(\n  {},\n  {\n    sort: {\n      \"profile.score\": -1,\n    },\n  }\n);\n```\n\n---\n\n# Pagination\n\nUse `skip` and `limit`.\n\n```js\nconst page = users.find(\n  {\n    active: true,\n  },\n  {\n    skip: 20,\n    limit: 10,\n  }\n);\n```\n\nA common pagination formula is:\n\n```js\nconst page = 3;\nconst pageSize = 20;\n\nconst results = users.find(\n  {},\n  {\n    skip: (page - 1) * pageSize,\n    limit: pageSize,\n  }\n);\n```\n\n---\n\n# Projections\n\nProjections control which fields are returned.\n\n## Inclusion\n\n```js\nconst users = users.find(\n  {},\n  {\n    projection: {\n      name: 1,\n      email: 1,\n    },\n  }\n);\n```\n\n## Exclusion\n\n```js\nconst users = users.find(\n  {},\n  {\n    projection: {\n      password: 0,\n      secret: 0,\n    },\n  }\n);\n```\n\nNested fields are supported:\n\n```js\nusers.find(\n  {},\n  {\n    projection: {\n      \"profile.bio\": 1,\n      name: 1,\n    },\n  }\n);\n```\n\n---\n\n# Updating Documents\n\n`jarDB` supports atomic update operators.\n\nSupported update operators:\n\n| Operator  | Description                |\n| --------- | -------------------------- |\n| `$set`    | Sets a field               |\n| `$unset`  | Removes a field            |\n| `$inc`    | Increments a numeric field |\n| `$rename` | Renames a field            |\n\n---\n\n# `$set`\n\n```js\nusers.updateOne(\n  {\n    _id: \"user-001\",\n  },\n  {\n    $set: {\n      name: \"Alice Smith\",\n      active: true,\n    },\n  }\n);\n```\n\n---\n\n# `$unset`\n\nRemove fields:\n\n```js\nusers.updateOne(\n  {\n    _id: \"user-001\",\n  },\n  {\n    $unset: {\n      temporaryToken: true,\n    },\n  }\n);\n```\n\nYou can also provide an array:\n\n```js\nusers.updateOne(\n  {\n    _id: \"user-001\",\n  },\n  {\n    $unset: [\"temporaryToken\", \"oldField\"],\n  }\n);\n```\n\n---\n\n# `$inc`\n\nIncrement a numeric field:\n\n```js\nusers.updateOne(\n  {\n    _id: \"user-001\",\n  },\n  {\n    $inc: {\n      loginCount: 1,\n    },\n  }\n);\n```\n\nMultiple increments:\n\n```js\nusers.updateOne(\n  {\n    _id: \"user-001\",\n  },\n  {\n    $inc: {\n      points: 10,\n      loginCount: 1,\n    },\n  }\n);\n```\n\nIf the field does not exist, `$inc` starts from `0`.\n\n---\n\n# `$rename`\n\nRename a field:\n\n```js\nusers.updateOne(\n  {\n    _id: \"user-001\",\n  },\n  {\n    $rename: {\n      username: \"displayName\",\n    },\n  }\n);\n```\n\n---\n\n# Update Shorthand\n\nFields without an explicit update operator are treated as `$set`.\n\n```js\nusers.updateOne(\n  {\n    _id: \"user-001\",\n  },\n  {\n    name: \"Alice\",\n    active: true,\n  }\n);\n```\n\nThis is equivalent to:\n\n```js\nusers.updateOne(\n  {\n    _id: \"user-001\",\n  },\n  {\n    $set: {\n      name: \"Alice\",\n      active: true,\n    },\n  }\n);\n```\n\n---\n\n# `updateOne()`\n\nUpdates the first matching document.\n\n```js\nconst result = users.updateOne(\n  {\n    email: \"alice@example.com\",\n  },\n  {\n    $set: {\n      active: false,\n    },\n  }\n);\n```\n\nReturns:\n\n```js\n{\n  matchedCount: 1,\n  modifiedCount: 1\n}\n```\n\nAn empty filter is rejected by `updateOne()` to help prevent accidental full-collection updates.\n\n---\n\n# Upsert\n\nUse `upsert: true` to insert a document if no matching document exists.\n\n```js\nconst result = users.updateOne(\n  {\n    email: \"new@example.com\",\n  },\n  {\n    $set: {\n      name: \"New User\",\n      active: true,\n    },\n  },\n  {\n    upsert: true,\n  }\n);\n```\n\nIf no document matches, the result contains:\n\n```js\n{\n  matchedCount: 0,\n  modifiedCount: 0,\n  upsertedId: \"...\"\n}\n```\n\nThe current upsert implementation uses simple equality fields from the filter when constructing the new document. Complex operator-based filters should not be relied upon as complete upsert documents.\n\n---\n\n# `updateMany()`\n\nUpdates every matching document.\n\n```js\nusers.updateMany(\n  {\n    active: true,\n  },\n  {\n    $inc: {\n      loginCount: 1,\n    },\n  }\n);\n```\n\nAn empty filter updates every document in the collection.\n\nUse this carefully.\n\n---\n\n# Find and Update\n\n## `findOneAndUpdate()`\n\nFinds a document, updates it, and returns the resulting document.\n\n```js\nconst result = users.findOneAndUpdate(\n  {\n    email: \"alice@example.com\",\n  },\n  {\n    $set: {\n      active: true,\n    },\n  }\n);\n\nconsole.log(result.value);\n```\n\nSuccessful result:\n\n```js\n{\n  value: {\n    _id: \"...\",\n    email: \"alice@example.com\",\n    active: true\n  },\n  lastErrorObject: {\n    updatedExisting: true\n  }\n}\n```\n\nIf no document exists:\n\n```js\n{\n  value: null\n}\n```\n\nWith upsert:\n\n```js\nconst result = users.findOneAndUpdate(\n  {\n    email: \"new@example.com\",\n  },\n  {\n    $set: {\n      name: \"New User\",\n    },\n  },\n  {\n    upsert: true,\n  }\n);\n```\n\n---\n\n# Deleting Documents\n\n## `deleteOne()`\n\nDeletes the first matching document.\n\n```js\nconst result = users.deleteOne({\n  _id: \"user-001\",\n});\n```\n\nReturns:\n\n```js\n{\n  acknowledged: true,\n  deletedCount: 1\n}\n```\n\n`deleteOne()` requires a non-empty filter.\n\n---\n\n## `deleteMany()`\n\nDeletes all matching documents.\n\n```js\nconst result = users.deleteMany({\n  active: false,\n});\n```\n\nYou can intentionally delete every document with an empty filter:\n\n```js\nusers.deleteMany({});\n```\n\nUse this carefully because it removes the entire collection contents.\n\n---\n\n# Find and Delete\n\n## `findOneAndDelete()`\n\nFinds and deletes one document.\n\n```js\nconst result = users.findOneAndDelete({\n  email: \"alice@example.com\",\n});\n\nconsole.log(result.value);\n```\n\nIf no document is found:\n\n```js\n{\n  value: null\n}\n```\n\n---\n\n# Indexes\n\nFor frequently queried fields, create an index.\n\n```js\nusers.createIndex(\"email\");\n```\n\nNested fields are supported:\n\n```js\nusers.createIndex(\"profile.country\");\n```\n\nThe index is implemented using SQLite's JSON expression indexes.\n\nExample:\n\n```js\nusers.createIndex(\"email\");\n\nusers.find({\n  email: \"alice@example.com\",\n});\n```\n\nCreating the same index more than once through the same collection instance is ignored.\n\n### Important\n\nThe index name and field path are generated from the supplied field name. Field names are validated to contain only alphanumeric characters, underscores, and dots.\n\n---\n\n# Aggregation\n\n`aggregate()` provides a limited MongoDB-style aggregation pipeline backed by SQLite.\n\nSupported stages include:\n\n* `$match`\n* `$group`\n* `$project`\n* `$sort`\n* `$limit`\n* `$skip`\n\nExample:\n\n```js\nconst result = users.aggregate([\n  {\n    $match: {\n      active: true,\n    },\n  },\n  {\n    $group: {\n      _id: \"country\",\n      total: {\n        $count: true,\n      },\n      averageAge: {\n        $avg: \"age\",\n      },\n    },\n  },\n]);\n```\n\n---\n\n# `$match`\n\nFilters documents.\n\n```js\nusers.aggregate([\n  {\n    $match: {\n      active: true,\n    },\n  },\n]);\n```\n\nThe same query operators available to `find()` can be used in `$match`.\n\n---\n\n# `$group`\n\nGroups documents by a field.\n\n```js\nconst result = users.aggregate([\n  {\n    $group: {\n      _id: \"country\",\n      total: {\n        $count: true,\n      },\n    },\n  },\n]);\n```\n\nSupported aggregation operators:\n\n| Operator | Description            |\n| -------- | ---------------------- |\n| `$sum`   | Sum numeric values     |\n| `$avg`   | Average numeric values |\n| `$max`   | Maximum value          |\n| `$min`   | Minimum value          |\n| `$count` | Count documents        |\n\nExample:\n\n```js\nconst result = users.aggregate([\n  {\n    $group: {\n      _id: \"country\",\n      totalUsers: {\n        $count: true,\n      },\n      totalAge: {\n        $sum: \"age\",\n      },\n      averageAge: {\n        $avg: \"age\",\n      },\n      oldest: {\n        $max: \"age\",\n      },\n      youngest: {\n        $min: \"age\",\n      },\n    },\n  },\n]);\n```\n\n---\n\n# Grouping Without a Group Key\n\nUse `_id: null` to aggregate the entire collection:\n\n```js\nconst result = users.aggregate([\n  {\n    $group: {\n      _id: null,\n      totalUsers: {\n        $count: true,\n      },\n      averageAge: {\n        $avg: \"age\",\n      },\n    },\n  },\n]);\n```\n\n---\n\n# `$sort` in Aggregation\n\n```js\nconst result = users.aggregate([\n  {\n    $group: {\n      _id: \"country\",\n      total: {\n        $count: true,\n      },\n    },\n  },\n  {\n    $sort: {\n      total: -1,\n    },\n  },\n]);\n```\n\nUse:\n\n* `1` for ascending\n* `-1` for descending\n\n---\n\n# `$limit`\n\n```js\nconst result = users.aggregate([\n  {\n    $sort: {\n      age: -1,\n    },\n  },\n  {\n    $limit: 10,\n  },\n]);\n```\n\n---\n\n# `$skip`\n\n```js\nconst result = users.aggregate([\n  {\n    $skip: 20,\n  },\n  {\n    $limit: 10,\n  },\n]);\n```\n\n---\n\n# `$project`\n\nThe aggregation API recognizes `$project` stages.\n\n```js\nconst result = users.aggregate([\n  {\n    $match: {\n      active: true,\n    },\n  },\n  {\n    $project: {\n      name: 1,\n      age: 1,\n    },\n  },\n]);\n```\n\n> Current implementation note: `$project` is recognized, but projection after a `$group` stage is not fully transformed into SQL and should not be treated as equivalent to MongoDB's complete `$project` behavior.\n\n---\n\n# Dropping a Collection\n\nA collection can drop itself:\n\n```js\nconst users = db.collection(\"users\");\n\nusers.drop();\n```\n\nThis removes the underlying SQLite table.\n\nThe index state maintained by the collection instance is also cleared.\n\n---\n\n# Error Handling\n\n`jarDB` provides a custom `JarDBError` class.\n\nErrors contain:\n\n```js\n{\n  name: \"JarDBError\",\n  code: \"...\"\n}\n```\n\nExample:\n\n```js\ntry {\n  users.updateOne(\n    {},\n    {\n      $set: {\n        name: \"Alice\",\n      },\n    }\n  );\n} catch (error) {\n  console.error(error.name);\n  console.error(error.code);\n  console.error(error.message);\n}\n```\n\nCommon error codes include:\n\n| Code                   | Meaning                         |\n| ---------------------- | ------------------------------- |\n| `JAR_DB_ERROR`         | Generic jarDB error             |\n| `INVALID_IDENTIFIER`   | Invalid collection identifier   |\n| `INVALID_FIELD_PATH`   | Invalid field path              |\n| `INVALID_OPERATOR`     | Invalid operator argument       |\n| `UNSUPPORTED_OPERATOR` | Unsupported query operator      |\n| `INVALID_QUERY`        | Invalid query value             |\n| `DUPLICATE_ID`         | Duplicate document `_id`        |\n| `INVALID_UPDATE`       | Invalid update operation        |\n| `INVALID_INCREMENT`    | Invalid `$inc` value            |\n| `INVALID_DELETE`       | Invalid `deleteOne()` operation |\n\n---\n\n# Complete Example\n\n```js\nimport { jarDB } from \"jardb\";\n\nconst db = new jarDB(\"./data/app.db\");\n\nconst users = db.collection(\"users\");\n\n// Create an index\nusers.createIndex(\"email\");\n\n// Insert documents\nusers.insertMany([\n  {\n    name: \"Alice\",\n    age: 28,\n    email: \"alice@example.com\",\n    country: \"Kenya\",\n    active: true,\n  },\n  {\n    name: \"Bob\",\n    age: 35,\n    email: \"bob@example.com\",\n    country: \"Kenya\",\n    active: true,\n  },\n  {\n    name: \"Charlie\",\n    age: 17,\n    email: \"charlie@example.com\",\n    country: \"Uganda\",\n    active: false,\n  },\n]);\n\n// Find\nconst adults = users.find({\n  age: {\n    $gte: 18,\n  },\n});\n\nconsole.log(adults);\n\n// Find one\nconst alice = users.findOne({\n  email: \"alice@example.com\",\n});\n\nconsole.log(alice);\n\n// Update\nusers.updateOne(\n  {\n    email: \"alice@example.com\",\n  },\n  {\n    $set: {\n      active: false,\n    },\n    $inc: {\n      loginCount: 1,\n    },\n  }\n);\n\n// Update many\nusers.updateMany(\n  {\n    country: \"Kenya\",\n  },\n  {\n    $inc: {\n      points: 10,\n    },\n  }\n);\n\n// Count\nconst activeUsers = users.count({\n  active: true,\n});\n\nconsole.log(activeUsers);\n\n// Aggregation\nconst statistics = users.aggregate([\n  {\n    $group: {\n      _id: \"country\",\n      total: {\n        $count: true,\n      },\n      averageAge: {\n        $avg: \"age\",\n      },\n    },\n  },\n]);\n\nconsole.log(statistics);\n\n// Delete\nusers.deleteOne({\n  email: \"charlie@example.com\",\n});\n\n// Close database\ndb.close();\n```\n\n---\n\n# API Summary\n\n## Database\n\n| Method                 | Description                |\n| ---------------------- | -------------------------- |\n| `new jarDB(path)`      | Open/create a database     |\n| `collection(name)`     | Get or create a collection |\n| `listCollections()`    | List database tables       |\n| `dropCollection(name)` | Drop a collection          |\n| `close()`              | Close the database         |\n\n## Collection\n\n| Method                                      | Description                    |\n| ------------------------------------------- | ------------------------------ |\n| `insertOne(doc)`                            | Insert one document            |\n| `insertMany(docs)`                          | Insert multiple documents      |\n| `find(filter, options)`                     | Find matching documents        |\n| `findOne(filter)`                           | Find one document              |\n| `count(filter)`                             | Count matching documents       |\n| `updateOne(filter, update, options)`        | Update one document            |\n| `updateMany(filter, update)`                | Update multiple documents      |\n| `findOneAndUpdate(filter, update, options)` | Find and update                |\n| `deleteOne(filter)`                         | Delete one document            |\n| `deleteMany(filter)`                        | Delete multiple documents      |\n| `findOneAndDelete(filter)`                  | Find and delete                |\n| `createIndex(field)`                        | Create a JSON expression index |\n| `aggregate(pipeline)`                       | Run an aggregation pipeline    |\n| `drop()`                                    | Drop the collection            |\n\n---\n\n# Query Operator Summary\n\n```text\n$eq\n$ne\n$gt\n$gte\n$lt\n$lte\n$in\n$nin\n$regex\n$exists\n$not\n$or\n$and\n```\n\n# Update Operator Summary\n\n```text\n$set\n$unset\n$inc\n$rename\n```\n\n# Aggregation Operator Summary\n\n```text\n$match\n$group\n$project\n$sort\n$limit\n$skip\n```\n\nGroup aggregation operators:\n\n```text\n$sum\n$avg\n$max\n$min\n$count\n```\n\n---\n\n# Security and Identifier Validation\n\nCollection names and field paths are validated before being inserted into SQL statements.\n\nAllowed characters are:\n\n```text\na-z\nA-Z\n0-9\n_\n.\n```\n\nFor example:\n\n```js\ndb.collection(\"user_profiles\");\ndb.collection(\"app.users\");\n```\n\nare valid.\n\nNames containing SQL syntax or other special characters are rejected.\n\nDocument values are passed to SQLite using parameters rather than being directly interpolated into SQL.\n\n---\n\n# Storage Model\n\nDocuments are stored in SQLite tables using two columns:\n\n```sql\n_id TEXT PRIMARY KEY\ndata TEXT NOT NULL\n```\n\nThe complete JavaScript document is serialized as JSON into the `data` column.\n\nFor example:\n\n```js\n{\n  _id: \"123\",\n  name: \"Alice\",\n  age: 28\n}\n```\n\nis stored conceptually as:\n\n```text\n_id  = \"123\"\ndata = '{\"_id\":\"123\",\"name\":\"Alice\",\"age\":28}'\n```\n\nSQLite's JSON functions such as `json_extract`, `json_set`, and `json_remove` are used for querying and updating document fields.\n\n---\n\n# When to Use jarDB\n\n`jarDB` is a good fit for:\n\n* Local applications\n* CLI tools\n* Desktop applications\n* Prototypes\n* Small APIs\n* Embedded applications\n* Development environments\n* Medium-sized applications with straightforward document storage\n* Applications that do not need a database server\n\nIt is especially useful when you want a document-oriented API but still want SQLite's single-file database architecture.\n\n# Limitations\n\n`jarDB` is intentionally lightweight and does not attempt to implement the complete MongoDB API.\n\nIn particular:\n\n* Aggregation supports a limited set of stages/operators.\n* `$project` has limited behavior in aggregation pipelines.\n* `$options` for `$regex` is recognized but is not currently applied as JavaScript `RegExp` flags.\n* Query/update field paths are restricted to alphanumeric characters, `_`, and `.`.\n* The database is embedded and local rather than a network database server.\n* Advanced MongoDB features such as transactions exposed through a MongoDB-style API, change streams, geospatial queries, and full aggregation semantics are not provided.\n* The implementation stores complete documents as JSON, so query performance for unindexed fields depends on SQLite JSON extraction.\n\nFor high-scale distributed applications, a server-based database may be more appropriate.\n\n---\n\n# License\n\nAdd your project's license information here.\n\nFor example:\n\n```text\nMIT License\n```\n\n---\n\n# Contributing\n\nContributions, bug reports, feature requests, and pull requests are welcome.\n\nBefore submitting a change, consider adding tests for:\n\n* CRUD operations\n* Query operators\n* Nested fields\n* Updates\n* Upserts\n* Aggregation\n* Indexing\n* Error handling\n\n---\n\n# Quick Reference\n\n```js\nimport { jarDB } from \"jardb\";\n\nconst db = new jarDB(\"./app.db\");\nconst users = db.collection(\"users\");\n\n// Insert\nusers.insertOne({\n  name: \"Alice\",\n  age: 28,\n});\n\n// Query\nusers.find({\n  age: {\n    $gte: 18,\n  },\n});\n\n// Query + sort + pagination\nusers.find(\n  {\n    active: true,\n  },\n  {\n    sort: {\n      age: -1,\n    },\n    skip: 0,\n    limit: 20,\n  }\n);\n\n// Update\nusers.updateOne(\n  {\n    name: \"Alice\",\n  },\n  {\n    $set: {\n      active: true,\n    },\n    $inc: {\n      points: 10,\n    },\n  }\n);\n\n// Delete\nusers.deleteOne({\n  name: \"Alice\",\n});\n\n// Aggregate\nusers.aggregate([\n  {\n    $group: {\n      _id: \"country\",\n      count: {\n        $count: true,\n      },\n    },\n  },\n]);\n\n// Close\ndb.close();\n```\n\n## License\n\nThis project is distributed under the license specified by the package repository.\n","readmeFilename":"README.md"}