{"id":13687262,"url":"https://github.com/oliver-oloughlin/kvdex","last_synced_at":"2026-04-16T00:03:42.375Z","repository":{"id":158231442,"uuid":"633515159","full_name":"oliver-oloughlin/kvdex","owner":"oliver-oloughlin","description":"A high-level abstraction layer for Deno KV with zero third-party dependencies by default 🦕🗝️","archived":false,"fork":false,"pushed_at":"2026-04-11T23:16:36.000Z","size":1622,"stargazers_count":243,"open_issues_count":0,"forks_count":9,"subscribers_count":3,"default_branch":"main","last_synced_at":"2026-04-12T01:22:44.943Z","etag":null,"topics":["database","db","deno","deno-kv","denokv","jsr","kv","kv-store","kvdex","kvstore","orm","orm-library","orm-like","queues"],"latest_commit_sha":null,"homepage":"https://jsr.io/@olli/kvdex","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/oliver-oloughlin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-04-27T17:05:30.000Z","updated_at":"2026-04-11T23:09:14.000Z","dependencies_parsed_at":"2023-07-07T15:15:31.853Z","dependency_job_id":"e312e56e-a78c-484a-97c3-dcd2acfcdfa9","html_url":"https://github.com/oliver-oloughlin/kvdex","commit_stats":{"total_commits":415,"total_committers":6,"mean_commits":69.16666666666667,"dds":0.04337349397590362,"last_synced_commit":"a209dafdb7912b868d801cf03114e1bd70a24de9"},"previous_names":["oliver-oloughlin/kvdb"],"tags_count":158,"template":false,"template_full_name":null,"purl":"pkg:github/oliver-oloughlin/kvdex","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oliver-oloughlin%2Fkvdex","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oliver-oloughlin%2Fkvdex/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oliver-oloughlin%2Fkvdex/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oliver-oloughlin%2Fkvdex/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/oliver-oloughlin","download_url":"https://codeload.github.com/oliver-oloughlin/kvdex/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oliver-oloughlin%2Fkvdex/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31865080,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-15T15:24:51.572Z","status":"ssl_error","status_checked_at":"2026-04-15T15:24:39.138Z","response_time":63,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["database","db","deno","deno-kv","denokv","jsr","kv","kv-store","kvdex","kvstore","orm","orm-library","orm-like","queues"],"created_at":"2024-08-02T15:00:51.221Z","updated_at":"2026-04-16T00:03:42.369Z","avatar_url":"https://github.com/oliver-oloughlin.png","language":"TypeScript","funding_links":[],"categories":["Wrapper / ORM"],"sub_categories":[],"readme":"# kvdex\n\n[![Score](https://jsr.io/badges/@olli/kvdex/score?labelColor=004d26\u0026logoColor=white\u0026color=00334d)](https://jsr.io/@olli/kvdex/score)\n[![Total Downloads](https://jsr.io/badges/@olli/kvdex/total-downloads?labelColor=004d26\u0026logoColor=white\u0026color=00334d)](https://jsr.io/@olli/kvdex)\n[![Weekly Downloads](https://jsr.io/badges/@olli/kvdex/weekly-downloads?labelColor=004d26\u0026logoColor=white\u0026color=00334d)](https://jsr.io/@olli/kvdex)\n[![Version](https://jsr.io/badges/@olli/kvdex?labelColor=004d26\u0026logoColor=white\u0026color=00334d)](https://jsr.io/@olli/kvdex)\n[![License](https://img.shields.io/github/license/oliver-oloughlin/kvdex?labelColor=004d26\u0026logoColor=white\u0026color=00334d)](https://github.com/oliver-oloughlin/kvdex/blob/main/LICENSE)\n\n`kvdex` is a high-level abstraction layer for Deno KV with zero third-party\ndependencies by default. Its goal is to enhance the experience of using Deno's\nKV store through additional features such as indexing, strongly typed\ncollections and serialization/compression, while maintaining as much of the\nnative functionality as possible, like atomic operations, real-time data updates\nand queue listeners. Also works with other runtimes such as Node.js and Bun, and\nhas compatibility options for the browser. `kvdex` accepts\n[Standard Schema](https://github.com/standard-schema/standard-schema) compliant\nmodels, which means you can bring your favourite validation library, or use the\nbuilt-in inference-only model.\n\n_Supported Deno versions:_ **^2.3.0**\n\n## Highlights\n\n- Strongly typed CRUD operations for selected and ranged documents.\n- Primary (unique) and secondary (non-unique) indexing.\n- Support for\n  [Standard Schema](https://github.com/standard-schema/standard-schema) models\n  ([Zod](https://zod.dev), [ArkType](https://arktype.io),\n  [Valibot](https://valibot.dev) and more).\n- Serialized, compressed and segmented storage for large objects.\n- Support for atomic operations.\n- Listen to real-time data updates.\n- Support for pagination and filtering.\n- Message queues at database and collection level with topics.\n- Support for different KV-backends, such as `Map`, `localStorage` and\n  `IndexedDB`.\n\n## Table of Contents\n\n- [kvdex](#kvdex)\n  - [Highlights](#highlights)\n  - [Table of Contents](#table-of-contents)\n  - [Models](#models)\n  - [Database](#database)\n  - [Collection Options](#collection-options)\n    - [`idGenerator`](#idgenerator)\n    - [`indices`](#indices)\n    - [`encoder`](#encoder)\n    - [`history`](#history)\n  - [Collection Methods](#collection-methods)\n    - [find()](#find)\n    - [findByPrimaryIndex()](#findbyprimaryindex)\n    - [findBySecondaryIndex()](#findbysecondaryindex)\n    - [findMany()](#findmany)\n    - [findHistory()](#findhistory)\n    - [findUndelivered()](#findundelivered)\n    - [add()](#add)\n    - [addMany()](#addmany)\n    - [set()](#set)\n    - [update()](#update)\n    - [updateByPrimaryIndex()](#updatebyprimaryindex)\n    - [updateBySecondaryIndex()](#updatebysecondaryindex)\n    - [updateMany()](#updatemany)\n    - [updateManyBySecondaryOrder()](#updatemanybysecondaryorder)\n    - [updateOne()](#updateone)\n    - [updateOneBySecondaryIndex()](#updateonebysecondaryindex)\n    - [updateOneBySecondaryOrder()](#updateonebysecondaryorder)\n    - [upsert()](#upsert)\n    - [upsertByPrimaryIndex()](#upsertbyprimaryindex)\n    - [delete()](#delete)\n    - [deleteByPrimaryIndex()](#deletebyprimaryindex)\n    - [deleteBySecondaryIndex()](#deletebysecondaryindex)\n    - [deleteMany()](#deletemany)\n    - [deleteManyBySecondaryOrder()](#deletemanybysecondaryorder)\n    - [deleteHistory()](#deletehistory)\n    - [deleteUndelivered()](#deleteundelivered)\n    - [getMany()](#getmany)\n    - [getManyBySecondaryOrder()](#getmanybysecondaryorder)\n    - [getOne()](#getone)\n    - [getOneBySecondaryIndex()](#getonebysecondaryindex)\n    - [getOneBySecondaryOrder()](#getonebysecondaryorder)\n    - [forEach()](#foreach)\n    - [forEachBySecondaryIndex()](#foreachbysecondaryindex)\n    - [forEachBySecondaryOrder()](#foreachbysecondaryorder)\n    - [map()](#map)\n    - [mapBySecondaryIndex()](#mapbysecondaryindex)\n    - [mapBySecondaryOrder()](#mapbysecondaryorder)\n    - [count()](#count)\n    - [countBySecondaryIndex()](#countbysecondaryindex)\n    - [countBySecondaryOrder()](#countbysecondaryorder)\n    - [enqueue()](#enqueue)\n    - [listenQueue()](#listenqueue)\n    - [watch()](#watch)\n    - [watchMany()](#watchmany)\n  - [Database Methods](#database-methods)\n    - [countAll()](#countall)\n    - [deleteAll()](#deleteall)\n    - [wipe()](#wipe)\n    - [deleteUndelivered()](#deleteundelivered-1)\n    - [findUndelivered()](#findundelivered-1)\n    - [enqueue()](#enqueue-1)\n    - [listenQueue()](#listenqueue-1)\n    - [setInterval()](#setinterval)\n    - [loop()](#loop)\n    - [atomic()](#atomic)\n  - [Atomic Operations](#atomic-operations)\n    - [Without checking](#without-checking)\n    - [With checking](#with-checking)\n  - [Document Methods](#document-methods)\n    - [flat()](#flat)\n  - [Extensions](#extensions)\n    - [Encoding](#encoding)\n      - [JSON](#json)\n      - [V8](#v8)\n      - [Brotli](#brotli)\n      - [Brotli Sync](#brotli-sync)\n    - [Zod](#zod)\n      - [Schemas](#schemas)\n    - [Migrate](#migrate)\n      - [Script](#script)\n      - [Function](#function)\n    - [KV](#kv)\n      - [Map](#map-1)\n  - [Blob Storage](#blob-storage)\n  - [Development](#development)\n  - [License](#license)\n\n## Models\n\nCollections are typed using models. Standard models can be defined using the\n`model()` function. Alternatively, any\n[Standard Schema](https://github.com/standard-schema/standard-schema) compliant\nmodel can be used, meaning validation libraries such as [Zod](https://zod.dev)\nis supported without being a dependency. The standard model uses type casting\nonly, and does not perform any runtime validation. Asymmetric models can be\ncreated by passing a transform function which maps from an input type to an\noutput type. Asymmetric models are useful for storing derived values or filling\ndefault values. It is up to the developer to choose the strategy that fits their\nuse case the best.\n\n**_NOTE_:** When using interfaces instead of types, they must extend the\n`KvValue` type.\n\nUsing the standard model strategy:\n\n```ts\nimport { model } from \"@olli/kvdex\";\n\ntype User = {\n  username: string;\n  age: number;\n  activities: string[];\n  address?: {\n    country: string;\n    city: string;\n    street: string;\n    houseNumber: number | null;\n  };\n};\n\n// Normal model (equal input and output)\nconst UserModel = model\u003cUser\u003e();\n\n// Asymmetric model (mapped output)\nconst UserModel = model((user: User) =\u003e ({\n  upperCaseUsername: user.username.toUpperCase(),\n  ageInDecades: user.age / 10,\n  createdAt: new Date(),\n}));\n```\n\nUsing [Zod](https://zod.dev) instead:\n\n```ts\nimport { z } from \"npm:zod\";\n\ntype User = z.infer\u003ctypeof UserModel\u003e;\n\nconst UserModel = z.object({\n  username: z.string(),\n  age: z.number(),\n  activities: z.array(z.string()),\n  address: z.object({\n    country: z.string(),\n    city: z.string(),\n    street: z.string(),\n    houseNumber: z.number().nullable(),\n  }).optional(),\n});\n```\n\n## Database\n\n`kvdex()` is used for creating a new database instance. It takes an options\nobject which expects a Deno KV instance and a schema definition.\n\n```ts\nimport { collection, kvdex, model } from \"@olli/kvdex\";\nimport { jsonEncoder } from \"@olli/kvdex/encoding/json\";\n\nconst kv = await Deno.openKv();\n\nconst db = kvdex({\n  kv: kv,\n  schema: {\n    numbers: collection(model\u003cnumber\u003e()),\n    serializedStrings: collection(model\u003cstring\u003e(), {\n      encoder: jsonEncoder(),\n    }),\n    users: collection(UserModel, {\n      history: true,\n      indices: {\n        username: \"primary\", // unique\n        age: \"secondary\", // non-unique\n      },\n    }),\n    // Nested collections\n    nested: {\n      strings: collection(model\u003cstring\u003e()),\n    },\n  },\n});\n```\n\nThe schema definition contains collection builders, or nested schema\ndefinitions. Collections can hold any type adhering to KvValue.\n\n**_Note:_** Index values are always serialized, using the JSON-encoder by\ndefault, or alternatively your provided encoder.\n\n## Collection Options\n\nThese are all the options available for the `collection()` method, used when\ndefining collections of documents. All collection options are optional.\n\n### `idGenerator`\n\nOverride the default id generator, which is used to automatically generate an id\nwhen adding a new document. The id generator gets called with the data being\nadded, which can be useful to create derived ids. The default id generator uses\n[`ulid()`](https://deno.land/std/ulid/mod.ts) from Deno's\n[standard library.](https://deno.land/std/ulid/mod.ts)\n\nId created from the data being added:\n\n```ts\nimport { collection, kvdex, model } from \"@olli/kvdex\";\n\nconst kv = await Deno.openKv();\n\nconst db = kvdex({\n  kv: kv,\n  schema: {\n    users: collection(model\u003cUser\u003e(), {\n      idGenerator: (user) =\u003e user.username,\n    }),\n  },\n});\n```\n\nUsing randomly generated UUIDs:\n\n```ts\nimport { collection, kvdex, model } from \"@olli/kvdex\";\n\nconst kv = await Deno.openKv();\n\nconst db = kvdex({\n  kv: kv,\n  schema: {\n    users: collection(model\u003cUser\u003e(), {\n      idGenerator: () =\u003e crypto.randomUUID(),\n    }),\n  },\n});\n```\n\nUsing multi-part ids:\n\n```ts\nimport { collection, kvdex, model } from \"@olli/kvdex\";\n\nconst kv = await Deno.openKv();\n\nconst db = kvdex({\n  kv: kv,\n  schema: {\n    users: collection(model\u003cUser\u003e(), {\n      idGenerator: () =\u003e [crypto.randomUUID(), Math.random()],\n    }),\n  },\n});\n```\n\n### `indices`\n\nDefine indices for collections of objects. Used to optimize operations by\nquerying data based on index values.\n\n**_NOTE:_** Index values are always serialized.\n\n```ts\nimport { collection, kvdex, model } from \"@olli/kvdex\";\n\nconst kv = await Deno.openKv();\n\nconst db = kvdex({\n  kv: kv,\n  schema: {\n    users: collection(model\u003cUser\u003e(), {\n      indices: {\n        username: \"primary\", // unique\n        age: \"secondary\", // non-unique\n      },\n    }),\n  },\n});\n```\n\n### `encoder`\n\nSpecify serialization and compression for the collection. This lets large\nobjects that exceed the native size limit of 64kb to be stored, by serializing,\ncompressing and dividing the value across multiple key/value entries. When an\nencoder is specified, there is a tradeoff between speed and storage efficiency.\nFor storing objects larger than the atomic operation size limit, see\n[Blob Storage](#blob-storage).\n\n```ts\nimport { kvdex, collection, model } from \"@olli/kvdex\"\nimport { jsonEncoder } from \"@olli/kvdex/encoding/json\"\nimport { v8Encoder } from \"@olli/kvdex/encoding/v8\"\nimport { brotliCompressor } from \"@olli/kvdex/encoding/brotli\"\n\nconst kv = await Deno.openKv()\n\nconst db = kvdex({\n  kv: kv,\n  schema: {\n    users: collection(model\u003cUser\u003e(), {\n      // JSON-encoder without compression (best runtime compatibility)\n      encoder: jsonEncoder(),\n\n      // JSON-encoder + Brotli compression (requires node:zlib built-in)\n      encoder: jsonEncoder({ compressor: brotliCompressor() }),\n\n      // V8-encoder without brotli compression (requires node:v8 built-in)\n      encoder: v8Encoder(),\n\n      // V8-encoder + brotli compression (requires node:v8 and node:zlib built-in)\n      encoder: v8Encoder({ compressor: brotliCompressor() }),\n\n      // Set custom serialize, deserialize, compress and decompress functions\n      encoder: {\n        serializer: {\n          serialize: ...,\n          deserialize: ...,\n        },\n        // optional\n        compressor: {\n          compress: ...,\n          decompress: ...,\n        },\n      },\n    }),\n  }\n})\n```\n\n### `history`\n\nSet to `true` to enable version history. Default is `false`.\n\n```ts\nimport { collection, kvdex, model } from \"@olli/kvdex\";\n\nconst kv = await Deno.openKv();\n\nconst db = kvdex({\n  kv: kv,\n  schema: {\n    users: collection(model\u003cUser\u003e(), {\n      history: true,\n    }),\n  },\n});\n```\n\n## Collection Methods\n\n### find()\n\nRetrieve a single document with the given id from the KV store. The id must\nadhere to the type KvId. This method takes an optional options argument that can\nbe used to set the consistency mode.\n\n```ts\nconst userDoc1 = await db.users.find(123);\n\nconst userDoc2 = await db.users.find(123n);\n\nconst userDoc3 = await db.users.find(\"oliver\", {\n  consistency: \"eventual\", // \"strong\" by default\n});\n```\n\n### findByPrimaryIndex()\n\nFind a document by a primary index.\n\n```ts\n// Finds a user document with the username = \"oliver\"\nconst userByUsername = await db.users.findByPrimaryIndex(\"username\", \"oliver\");\n```\n\n### findBySecondaryIndex()\n\nFind documents by a secondary index. Secondary indices are not unique, and\ntherefore the result is an array of documents. The method takes an optional\noptions argument that can be used for filtering of documents, and pagination.\n\n```ts\n// Returns all users with age = 24\nconst { result } = await db.users.findBySecondaryIndex(\"age\", 24);\n\n// Returns all users with age = 24 AND username that starts with \"o\"\nconst { result } = await db.users.findBySecondaryIndex(\"age\", 24, {\n  filter: (doc) =\u003e doc.value.username.startsWith(\"o\"),\n});\n```\n\n### findMany()\n\nRetrieve multiple documents with the given array of ids from the KV store. The\nids must adhere to the type KvId. This method takes an optional options argument\nthat can be used to set the consistency mode.\n\n```ts\nconst userDocs1 = await db.users.findMany([\"abc\", 123, 123n]);\n\nconst userDocs2 = await db.users.findMany([\"abc\", 123, 123n], {\n  consistency: \"eventual\", // \"strong\" by default\n});\n```\n\n### findHistory()\n\nRetrieve the version history of a document by id. A history entry contains a\ntimestamp, type of either \"write\" or \"delete\", and a copy of the document value\nif the type is \"write\".\n\n```ts\nconst { result } = await db.users.findHistory(\"user_id\");\n```\n\n```ts\nconst { result } = await db.users.findHistory(\"user_id\", {\n  filter: (entry) =\u003e entry.type === \"write\",\n});\n```\n\n### findUndelivered()\n\nRetrieve a document entry that was not delivered during an enqueue() operation\nin the collection queue. This method takes an optional options argument that can\nbe used to set the consistency mode.\n\n```ts\nconst doc1 = await db.users.findUndelivered(\"undelivered_id\");\n\nconst doc2 = await db.users.findUndelivered(\"undelivered_id\", {\n  consistency: \"eventual\", // \"strong\" by default\n});\n```\n\n### add()\n\nAdd a new document to the KV store with an auto-generated id (ulid by default).\nUpon completion, a CommitResult object will be returned with the document id,\nversionstamp and ok flag.\n\n```ts\nconst result = await db.users.add({\n  username: \"oliver\",\n  age: 24,\n  activities: [\"skiing\", \"running\"],\n  address: {\n    country: \"Norway\",\n    city: \"Bergen\",\n    street: \"Sesame\",\n    houseNumber: null,\n  },\n});\n```\n\n### addMany()\n\nAdd multiple document entries to the KV store with auto-generated ids (ulid by\ndefault). Upon completion, a list of CommitResult objects will be returned.\n\n```ts\n// Adds 5 new document entries to the KV store.\nconst result1 = await db.numbers.addMany([1, 2, 3, 4, 5]);\n\n// Only adds the first entry, as \"username\" is defined as a primary index and cannot have duplicates\nconst result2 = await db.users.addMany([\n  {\n    username: \"oli\",\n    age: 24,\n  },\n  {\n    username: \"oli\",\n    age: 56,\n  },\n]);\n```\n\n### set()\n\nSet a document entry in the KV store with a given id of type KvId. Upon\ncompletion, a CommitResult object will be returned with the document id,\nversionstamp and ok flag.\n\n```ts\n// Add a new document if the id is not already in use\nconst result1 = await db.numbers.set(\"id\", 1024);\n\n// Overwrite any existing document with the same id\nconst result2 = await db.numbers.set(\"id\", 2048, { overwrite: true });\n\nif (result1.ok) {\n  console.log(result.id); // id\n}\n```\n\n### update()\n\nUpdates the value of an exisiting document in the KV store by id. By default,\nthe `merge` strategy is used when available, falling back to `replace` for\nprimitive types and built-in objects (Date, RegExp, etc.). For plain objects,\nthe `merge-shallow` strategy is also supported.\n\n```ts\n// Updates the document with a new value\nconst result = await db.numbers.update(\"num1\", 42);\n\n// Partial update using merge, only updates the age field\nconst result = await db.users.update(\n  \"oliver\",\n  { age: 30 },\n  { strategy: \"merge\" },\n);\n```\n\n### updateByPrimaryIndex()\n\nUpdate a document by a primary index.\n\n```ts\n// Updates a user with username = \"oliver\" to have age = 56\nconst result = await db.users.updateByPrimaryIndex(\n  \"username\",\n  \"oliver\",\n  { age: 56 },\n);\n\n// Updates a user document using shallow merge\nconst result = await db.users.updateByPrimaryIndex(\n  \"username\",\n  \"anders\",\n  { age: 89 },\n  { strategy: \"merge-shallow\" },\n);\n```\n\n### updateBySecondaryIndex()\n\nUpdate documents by a secondary index. Takes an optional options argument that\ncan be used for filtering of documents to be updated, and pagination. If no\noptions are given, all documents by the given index value will we updated.\n\n```ts\n// Updates all user documents with age = 24 and sets age = 67\nconst { result } = await db.users.updateBySecondaryIndex(\"age\", 24, {\n  age: 67,\n});\n\n// Updates all users where age = 24 and username starts with \"o\", using shallow merge\nconst { result } = await db.users.updateBySecondaryIndex(\n  \"age\",\n  24,\n  { age: 67 },\n  {\n    filter: (doc) =\u003e doc.value.username.startsWith(\"o\"),\n    strategy: \"merge-shallow\",\n  },\n);\n```\n\n### updateMany()\n\nUpdate the value of multiple existing documents in the KV store. It takes an\noptional options argument that can be used for filtering of documents to be\nupdated, and pagination. If no options are given, \"updateMany\" will update all\ndocuments in the collection.\n\n```ts\n// Updates all user documents and sets age = 67\nconst { result } = await db.users.updateMany({ age: 67 });\n\n// Updates all users where age \u003e 20, using shallow merge\nconst { result } = await db.users.updateMany({ age: 67 }, {\n  filter: (doc) =\u003e doc.value.age \u003e 20,\n  strategy: \"merge-shallow\",\n});\n\n// Only updates first user document and fails the rest when username is a primary index\nconst { result } = await db.users.updateMany({ username: \"oliver\" });\n```\n\n### updateManyBySecondaryOrder()\n\nUpdate the value of multiple existing documents in the collection by a secondary\norder.\n\n```ts\n// Updates the first 10 users ordered by age and sets username = \"anon\"\nawait db.users.updateManyBySecondaryOrder(\"age\", { username: \"anon\" });\n```\n\n### updateOne()\n\nUpdate the first matching document from the KV store. It optionally takes the\nsame `options` argument as `updateMany()`. If no options are given,\n`updateOne()` will update the first document in the collection.\n\n```ts\n// Updates the first user document and sets age = 67\nconst result = await db.users.updateOne({ age: 67 });\n```\n\n```ts\n// Updates the first user where age \u003e 20, using shallow merge\nconst result = await db.users.updateOne({ age: 67 }, {\n  filter: (doc) =\u003e doc.value.age \u003e 20,\n  strategy: \"merge-shallow\",\n});\n```\n\n### updateOneBySecondaryIndex()\n\nUpdate the first matching document from the KV store by a secondary index. It\noptionally takes the same `options` argument as `updateMany()`. If no options\nare given, `updateOneBySecondaryIndex()` will update the first document in the\ncollection by the given index value.\n\n```ts\n// Updates the first user document where age = 20 and sets age = 67\nconst result = await db.users.updateOneBySecondaryIndex(\"age\", 20, { age: 67 });\n```\n\n```ts\n// Updates the first user where age = 20 and username starts with \"a\", using shallow merge\nconst result = await db.users.updateOneBySecondaryIndex(\n  \"age\",\n  20,\n  { age: 67 },\n  {\n    filter: (doc) =\u003e doc.value.username.startsWith(\"a\"),\n    strategy: \"merge-shallow\",\n  },\n);\n```\n\n### updateOneBySecondaryOrder()\n\nUpdate the value of one existing document in the collection by a secondary\norder.\n\n```ts\n// Updates the first user ordered by age and sets username = \"anon\"\nconst result = await db.users.updateOneBySecondaryOrder(\"age\", {\n  username: \"anon\",\n});\n```\n\n### upsert()\n\nUpdate an existing document by id, or set a new document entry if no matching\ndocument exists.\n\n```ts\nconst result = await db.users.upsert({\n  id: \"user_id\",\n  update: { username: \"Chris\" },\n  set: {\n    username: \"Chris\",\n    age: 54,\n    activities: [\"bowling\"],\n    address: {\n      country: \"USA\",\n      city: \"Las Vegas\",\n      street: \"St. Boulevard\",\n      houseNumber: 23,\n    },\n  },\n});\n```\n\n### upsertByPrimaryIndex()\n\nUpdate an existing document by a primary index, or set a new entry if no\nmatching document exists. An id can be optionally specified which will be used\nwhen creating a new document entry.\n\n```ts\nconst result = await db.users.upsertByPrimaryIndex({\n  index: [\"username\", \"Jack\"],\n  update: { username: \"Chris\" },\n  set: {\n    username: \"Chris\",\n    age: 54,\n    activities: [\"bowling\"],\n    address: {\n      country: \"USA\",\n      city: \"Las Vegas\",\n      street: \"St. Boulevard\",\n      houseNumber: 23,\n    },\n  },\n});\n```\n\n### delete()\n\nDelete one or more documents with the given ids from the KV store.\n\n```ts\nawait db.users.delete(\"f897e3cf-bd6d-44ac-8c36-d7ab97a82d77\");\n\nawait db.users.delete(\"user1\", \"user2\", \"user3\");\n```\n\n### deleteByPrimaryIndex()\n\nDelete a document by a primary index.\n\n```ts\n// Deletes user with username = \"oliver\"\nawait db.users.deleteByPrimaryIndex(\"username\", \"oliver\");\n```\n\n### deleteBySecondaryIndex()\n\nDelete documents by a secondary index. The method takes an optional options\nargument that can be used for filtering of documents, and pagination.\n\n```ts\n// Deletes all users with age = 24\nawait db.users.deleteBySecondaryIndex(\"age\", 24);\n\n// Deletes all users with age = 24 AND username that starts with \"o\"\nawait db.users.deleteBySecondaryIndex(\"age\", 24, {\n  filter: (doc) =\u003e doc.value.username.startsWith(\"o\"),\n});\n```\n\n### deleteMany()\n\nDelete multiple documents from the KV store without specifying ids. It takes an\noptional options argument that can be used for filtering of documents to be\ndeleted, and pagination. If no options are given, \"deleteMany\" will delete all\ndocuments in the collection.\n\n```ts\n// Deletes all user documents\nawait db.users.deleteMany();\n\n// Deletes all user documents where the user's age is above 20\nawait db.users.deleteMany({\n  filter: (doc) =\u003e doc.value.age \u003e 20,\n});\n\n// Deletes the first 10 user documents in the KV store\nawait db.users.deleteMany({\n  limit: 10,\n});\n\n// Deletes the last 10 user documents in the KV store\nawait db.users.deleteMany({\n  limit: 10,\n  reverse: true,\n});\n```\n\n### deleteManyBySecondaryOrder()\n\nDelete multiple documents from the KV store by a secondary order. The method\ntakes an optional options argument that can be used for filtering of documents,\nand pagination. If no options are provided, all documents in the collection are\ndeleted.\n\n```ts\n// Deletes the first 10 users ordered by age\nawait db.users.deleteManyBySecondaryOrder(\"age\", { limit: 10 });\n```\n\n### deleteHistory()\n\nDelete the version history of a document by id.\n\n```ts\nawait db.users.deleteHistory(\"user_id\");\n```\n\n### deleteUndelivered()\n\nDelete an undelivered document entry by id from the collection queue.\n\n```ts\nawait db.users.deleteUndelivered(\"id\");\n```\n\n### getMany()\n\nRetrieve multiple documents from the KV store. It takes an optional `options`\nargument that can be used for filtering of documents to be retrieved, and\npagination. If no options are given, `getMany()` will retrieve all documents in\nthe collection.\n\n```ts\n// Retrieves all user documents\nconst { result } = await db.users.getMany();\n\n// Retrieves all user documents where the user's age is above or equal to 18\nconst { result } = await db.users.getMany({\n  filter: (doc) =\u003e doc.value.age \u003e= 18,\n});\n\n// Retrieves the first 10 user documents in the KV store\nconst { result } = await db.users.getMany({\n  limit: 10,\n});\n\n// Retrieves the last 10 user documents in the KV store\nconst { result } = await db.users.getMany({\n  limit: 10,\n  reverse: true,\n});\n```\n\n### getManyBySecondaryOrder()\n\nRetrieves multiple documents from the KV store in the specified secondary order\nand according to the given options. If no options are provided, all documents\nare retrieved.\n\n```ts\n// Get all users ordered by age\nconst { result } = await db.users.getManyBySecondaryOrder(\"age\");\n\n// Only get users with username that starts with \"a\", ordered by age\nconst { result } = await db.users.getManyBySecondaryOrder(\"age\", {\n  filter: (doc) =\u003e doc.value.username.startsWith(\"a\"),\n});\n```\n\n### getOne()\n\nRetrieve the first matching document from the KV store. It optionally takes the\nsame `options` argument as `getMany()`. If no options are given, `getOne()` will\nretrieve the first document in the collection.\n\n```ts\n// Retrieves the first user document\nconst user = await db.users.getOne();\n\n// Retrieves the first user where the user's age is above or equal to 18\nconst user = await db.users.getOne({\n  filter: (doc) =\u003e doc.value.age \u003e 18,\n});\n```\n\n### getOneBySecondaryIndex()\n\nRetrieve the first matching document from the KV store by a secondary index. It\noptionally takes the same `options` argument as `getMany()`. If no options are\ngiven, `getOneBySecondaryIndex()` will retrieve the first document in the\ncollection by the given index value.\n\n```ts\n// Retrieves the first user document where age = 20\nconst user = await db.users.getOneBySecondaryIndex(\"age\", 20);\n\n// Retrieves the first user where age = 20 and username starts with \"a\"\nconst user = await db.users.getOneBySecondaryIndex(\"age\", 20, {\n  filter: (doc) =\u003e doc.value.username.startsWith(\"a\"),\n});\n```\n\n### getOneBySecondaryOrder()\n\nRetrieves one document from the KV store by a secondary order and according to\nthe given options. If no options are provided, the first document in the\ncollection by the given order is retrieved.\n\n```ts\n// Get the first user ordered by age\nconst user = await db.users.getOneBySecondaryOrder(\"age\");\n```\n\n### forEach()\n\nExecute a callback function for multiple documents in the KV store. Takes an\noptional options argument that can be used for filtering of documents and\npagination. If no options are given, the callback function will be executed for\nall documents in the collection.\n\n```ts\n// Log the username of every user document\nawait db.users.forEach((doc) =\u003e console.log(doc.value.username));\n\n// Log the username of every user that has \"swimming\" as an activity\nawait db.users.forEach((doc) =\u003e console.log(doc.value.username), {\n  filter: (doc) =\u003e doc.value.activities.includes(\"swimming\"),\n});\n\n// Log the usernames of the first 10 user documents in the KV store\nawait db.users.forEach((doc) =\u003e console.log(doc.value.username), {\n  limit: 10,\n});\n\n// Log the usernames of the last 10 user documents in the KV store\nawait db.users.forEach((doc) =\u003e console.log(doc.value.username), {\n  limit: 10,\n  reverse: true,\n});\n```\n\n### forEachBySecondaryIndex()\n\nExecute a callback function for documents by a secondary index. Takes an\noptional options argument that can be used for filtering of documents and\npagination. If no options are given, the callback function will be executed for\nall documents in the collection matching the index.\n\n```ts\n// Prints the username of all users where age = 20\nawait db.users.forEachBySecondaryIndex(\n  \"age\",\n  20,\n  (doc) =\u003e console.log(doc.value.username),\n);\n```\n\n### forEachBySecondaryOrder()\n\nExecutes a callback function for every document by a secondary order and\naccording to the given options. If no options are provided, the callback\nfunction is executed for all documents.\n\n```ts\n// Prints the username of all users ordered by age\nawait db.users.forEachBySecondaryOrder(\n  \"age\",\n  (doc) =\u003e console.log(doc.value.username),\n);\n```\n\n### map()\n\nExecute a callback function for multiple documents in the KV store and retrieve\nthe results. It takes an optional options argument that can be used for\nfiltering of documents and pagination. If no options are given, the callback\nfunction will be executed for all documents in the collection.\n\n```ts\n// Get a list of all the ids of the user documents\nconst { result } = await db.users.map((doc) =\u003e doc.id);\n\n// Get a list of all usernames of users with age \u003e 20\nconst { result } = await db.users.map((doc) =\u003e doc.value.username, {\n  filter: (doc) =\u003e doc.value.age \u003e 20,\n});\n\n// Get a list of the usernames of the first 10 users in the KV store\nconst { result } = await db.users.map((doc) =\u003e doc.value.username, {\n  limit: 10,\n});\n\n// Get a list of the usernames of the last 10 users in the KV store\nconst { result } = await db.users.map((doc) =\u003e doc.value.username, {\n  limit: 10,\n  reverse: true,\n});\n```\n\n### mapBySecondaryIndex()\n\nExecutes a callback function for documents by a secondary index and retrieves\nthe results. It takes an optional options argument that can be used for\nfiltering of documents and pagination. If no options are given, the callback\nfunction will be executed for all documents matching the index.\n\n```ts\n// Returns a list of usernames of all users where age = 20\nconst { result } = await db.users.mapBySecondaryIndex(\n  \"age\",\n  20,\n  (doc) =\u003e doc.value.username,\n);\n```\n\n### mapBySecondaryOrder()\n\nExecutes a callback function for every document by a secondary order and\naccording to the given options. If no options are provided, the callback\nfunction is executed for all documents. The results from the callback function\nare returned as a list.\n\n```ts\n// Returns a list of usernames of all users ordered by age\nconst { result } = await db.users.mapBySecondaryOrder(\n  \"age\",\n  (doc) =\u003e doc.value.username,\n);\n```\n\n### count()\n\nCount the number of documents in a collection. Takes an optional options\nargument that can be used for filtering of documents. If no options are given,\nit will count all documents in the collection.\n\n```ts\n// Returns the total number of user documents in the KV store\nconst count = await db.users.count();\n\n// Returns the number of users with age \u003e 20\nconst count = await db.users.count({\n  filter: (doc) =\u003e doc.value.age \u003e 20,\n});\n```\n\n### countBySecondaryIndex()\n\nCounts the number of documents in the collection by a secondary index. Takes an\noptional options argument that can be used for filtering of documents. If no\noptions are given, it will count all documents matching the index.\n\n```ts\n// Counts all users where age = 20\nconst count = await db.users.countBySecondaryIndex(\"age\", 20);\n```\n\n### countBySecondaryOrder()\n\nCounts the number of documents in the collection by a secondary order.\n\n```ts\n// Counts how many of the first 10 users ordered by age that are under the age of 18\nconst count = await db.users.countBySecondaryOrder(\"age\", {\n  limit: 10,\n  filter: (doc) =\u003e doc.value.age \u003c 18,\n});\n```\n\n### enqueue()\n\nAdd data to the collection queue to be delivered to the queue listener via\n`db.collection.listenQueue()`. The data will only be received by queue listeners\non the specified collection and topic. The method takes an optional options\nargument that can be used to set a delivery delay and topic.\n\n```ts\n// Immediate delivery\nawait db.users.enqueue(\"some data\");\n\n// Delay of 2 seconds before delivery\nawait db.users.enqueue(\"cake\", {\n  delay: 2_000,\n  topic: \"food\",\n});\n```\n\n### listenQueue()\n\nListen for data from the collection queue that was enqueued with\n`db.collection.enqueue()`. Will only receive data that was enqueued to the\nspecific collection queue and topic. Expects a handler function as argument, as\nwell as optional options that can be used to set the topic.\n\n```ts\n// Prints the data to console when recevied\ndb.users.listenQueue((data) =\u003e console.log(data));\n\n// Sends post request when data is received\ndb.users.listenQueue(async (data) =\u003e {\n  const dataBody = JSON.stringify(data);\n\n  const res = await fetch(\"...\", {\n    method: \"POST\",\n    body: dataBody,\n  });\n\n  console.log(\"POSTED:\", dataBody, res.ok);\n}, { topic: \"posts\" });\n```\n\n### watch()\n\nListen for live changes to a single document by id.\n\n```ts\n// Updates the document value every second\nsetInterval(() =\u003e db.numbers.set(\"id\", Math.random()), 1_000);\n\n// Listen for any updates to the document value\ndb.numbers.watch(\"id\", (doc) =\u003e {\n  // Document will be null if the latest update was a delete operation\n  console.log(doc?.value);\n});\n```\n\nWatchers can also be stopped.\n\n```ts\nconst { promise, cancel } = db.numbers.watch(\"id\", (doc) =\u003e {\n  // ...\n});\n\nawait cancel();\nawait promise;\n```\n\n### watchMany()\n\nListen for live changes to an array of specified documents by id.\n\n```ts\n// Delayed setting of document values\nsetTimeout(() =\u003e db.numbers.set(\"id1\", 10), 1_000);\nsetTimeout(() =\u003e db.numbers.set(\"id2\", 20), 2_000);\nsetTimeout(() =\u003e db.numbers.set(\"id3\", 30), 3_000);\n\n// Listen for any updates to the document values\ndb.numbers.watchMany([\"id1\", \"id2\", \"id3\"], (docs) =\u003e {\n  // Prints for each update to any of the documents\n  console.log(docs[0]?.value); // 10, 10, 10\n  console.log(docs[1]?.value); // null, 20, 20\n  console.log(docs[2]?.value); // null, null, 30\n});\n```\n\nWatchers can also be stopped.\n\n```ts\nconst { promise, cancel } = db.numbers.watchMany(\n  [\"id1\", \"id2\", \"id3\"],\n  (docs) =\u003e {\n    // ...\n  },\n);\n\nawait cancel();\nawait promise;\n```\n\n## Database Methods\n\nThese are methods which can be found at the top level of your database object,\nand perform operations across multiple collections or unrelated to collections.\n\n### countAll()\n\nCount the total number of documents across all collections. It takes an optional\noptions argument that can be used to set the consistency mode.\n\n```ts\n// Gets the total number of documents in the KV store across all collections\nconst count = await db.countAll();\n```\n\n### deleteAll()\n\nDelete all documents across all collections.\n\n```ts\nawait db.deleteAll();\n```\n\n### wipe()\n\nDelete all kvdex entries, including undelivered and history entries.\n\n```ts\nawait db.wipe();\n```\n\n### deleteUndelivered()\n\nDelete an undelivered document entry by id from the database queue.\n\n```ts\nawait db.deleteUndelivered(\"id\");\n```\n\n### findUndelivered()\n\nRetrieve a document entry that was not delivered during an enqueue() operation\nin the database queue. This method takes an optional options argument that can\nbe used to set the consistency mode.\n\n```ts\nconst doc1 = await db.findUndelivered(\"undelivered_id\");\n\nconst doc2 = await db.findUndelivered(\"undelivered_id\", {\n  consistency: \"eventual\", // \"strong\" by default\n});\n```\n\n### enqueue()\n\nAdd data to the database queue to be delivered to the queue listener via\n`db.listenQueue()`. The data will only be received by queue listeners on the\ndatabase queue and specified topic. The method takes an optional options\nargument that can be used to set a delivery delay and topic.\n\n```ts\n// Immediate delivery\nawait db.enqueue(\"some data\");\n\n// Delay of 2 seconds before delivery\nawait db.enqueue(\"cake\", {\n  delay: 2_000,\n  topic: \"food\",\n});\n```\n\n### listenQueue()\n\nListen for data from the database queue that was enqueued with `db.enqueue()`.\nWill only receive data that was enqueued to the database queue and specified\ntopic. Expects a handler function as argument, as well as optional options that\ncan be used to set the topic.\n\n```ts\n// Prints the data to console when recevied\ndb.listenQueue((data) =\u003e console.log(data));\n\n// Sends post request when data is received in the \"posts\" topic\ndb.listenQueue(async (data) =\u003e {\n  const dataBody = JSON.stringify(data);\n\n  const res = await fetch(\"...\", {\n    method: \"POST\",\n    body: dataBody,\n  });\n\n  console.log(\"POSTED:\", dataBody, res.ok);\n}, { topic: \"posts\" });\n```\n\n### setInterval()\n\nCreate an interval built on queues that can run indefinitely or as long as a\nwhile condition is met. Interval time is given in milliseconds, and can be set\nby either a static number or dynamically by a function. There is an enforced\nminimum start delay of 1 second to ensure the queue listener is registered\nbefore the first delivery.\n\n```ts\n// Will repeat indefinitely with 1 second interval\ndb.setInterval(() =\u003e console.log(\"Hello World!\"), 1_000);\n\n// First callback starts after a 10 second delay, after that there is a random interval between 0 and 5 seconds\ndb.setInterval(\n  () =\u003e console.log(\"I terminate after running 10 times\"),\n  () =\u003e Math.random() * 5_000,\n  {\n    // Delay before the first callback is invoked\n    startDelay: 10_000,\n\n    // Count starts at 0 and is given before the current callback is run\n    while: ({ count }) =\u003e count \u003c 10,\n  },\n);\n```\n\n### loop()\n\nCreate a loop built on queues that can run indefinitely or as long as a while\ncondition is met. In contrast to `setInterval()`, the callback function in a\nloop is run sequentially, meaning the next callback is not enqueued until the\nprevious task finishes. There is an enforced minimum start delay of 1 second to\nensure the queue listener is registered before the first delivery.\n\n```ts\n// Sequentially prints \"Hello World!\" indefinitely with no delay between each iteration\ndb.loop(() =\u003e console.log(\"Hello World!\"));\n\n// Sequentially prints \"Hello World!\" 10 times, with a 3 second delay between each iteration\ndb.loop(() =\u003e console.log(\"Hello World!\"), {\n  delay: 3_000,\n  while: ({ count }) =\u003e count \u003c 10,\n});\n```\n\n### atomic()\n\nInitiate an atomic operation. The method takes a selector function as argument\nfor selecting the initial collection context.\n\n```ts\ndb.atomic((schema) =\u003e schema.users);\n```\n\n## Atomic Operations\n\nAtomic operations allow for executing multiple mutations as a single atomic\ntransaction. This means that documents can be checked for changes before\ncommitting the mutations, in which case the operation will fail. It also ensures\nthat either all mutations succeed, or they all fail.\n\nTo initiate an atomic operation, call \"atomic\" on the database object. The\nmethod expects a selector for selecting the collection that the subsequent\nmutation actions will be performed on. Mutations can be performed on documents\nfrom multiple collections in a single atomic operation by calling \"select\" at\nany point in the building chain to switch the collection context. To execute the\noperation, call \"commit\" at the end of the chain. A committed atomic operation\nreturns a promise resolving to either a Deno.KvCommitResult object if\nsuccessful, or Deno.KvCommitError if not.\n\n**_NOTE_:** Atomic operations are not available for serialized collections. For\nindexable collections, any operations performing deletes will not be truly\natomic in the sense that it performs a single isolated operation. This is\nbecause the document data must be read before performing the initial delete\noperation, to then perform another delete operation for the index entries. If\nthe initial operation fails, the index entries will not be deleted. To avoid\ncollisions and errors related to indexing, an atomic operation will always fail\nif it is trying to delete and write to the same indexable collection. It will\nalso fail if trying to set/add a document with colliding index entries, or if\ntrying to set a document with the `overwrite` option.\n\n### Without checking\n\n```ts\n// Deletes and adds an entry to the numbers collection\nconst result1 = await db\n  .atomic((schema) =\u003e schema.numbers)\n  .delete(\"id_1\")\n  .set(\"id_2\", 100)\n  .commit();\n\n// Adds 2 new entries to the numbers collection and 1 new entry to the users collection\nconst result2 = await db\n  .atomic((schema) =\u003e schema.numbers)\n  .add(1)\n  .add(2)\n  .select((schema) =\u003e schema.users)\n  .set(\"user_1\", {\n    username: \"oliver\",\n    age: 24,\n    activities: [\"skiing\", \"running\"],\n    address: {\n      country: \"Norway\",\n      city: \"Bergen\",\n      street: \"Sesame\",\n      houseNumber: 42,\n    },\n  })\n  .commit();\n\n// Will fail and return Deno.KvCommitError because it is trying\n// to both add and delete from an indexable collection\nconst result3 = await db\n  .atomic((schema) =\u003e schema.users)\n  .delete(\"user_1\")\n  .set(\"user_1\", {\n    username: \"oliver\",\n    age: 24,\n    activities: [\"skiing\", \"running\"],\n    address: {\n      country: \"Norway\",\n      city: \"Bergen\",\n      street: \"Sesame\",\n      houseNumber: 42,\n    },\n  })\n  .commit();\n```\n\n### With checking\n\n```ts\n// Only adds 10 to the value when it has not been changed since being read\nlet result = null;\nconst id = \"id\";\n\nwhile (!result || !result.ok) {\n  const doc = await db.numbers.find(id);\n  if (!doc) break;\n\n  result = await db\n    .atomic((schema) =\u003e schema.numbers)\n    .check({\n      id,\n      versionstamp: doc.versionstamp,\n    })\n    .set(id, doc.value + 10)\n    .commit();\n}\n```\n\n## Document Methods\n\nThese are methods on the Document object which perform actions/mutations on the\ndocument contents.\n\n### flat()\n\nFlatten top layer of document data. Returns an object containing the id,\nversionstamp and value entries for documents of type Model, else simply returns\nthe document data.\n\n```ts\n// We assume the document exists in the KV store\nconst doc = await db.users.find(123n);\nconst flattened = doc.flat();\n\n// Document:\n// {\n//   id,\n//   versionstamp,\n//   value\n// }\n\n// Flattened:\n// {\n//   id,\n//   versionstamp,\n//   ...value\n// }\n```\n\n## Extensions\n\nAdditional features outside of the basic functionality provided by `kvdex`.\nWhile the core functionalities are free of third-party dependencies, extended\nfeatures may rely on third-party dependenices or runtime-specific APIs to\nenhance integration.\n\n### Encoding\n\nUtilities for encoding data.\n\n#### JSON\n\nJSON-encoder and utilities for stringifying and serializing data.\n\n```ts\nimport { jsonEncoder } from \"@olli/kvdex/encoding/json\";\n\n// With default options (no compression)\nconst encoder = jsonEncoder();\n```\n\n```ts\nimport { jsonEncoder } from \"@olli/kvdex/encoding/json\";\nimport { brotliCompressor } from \"@olli/kvdex/encoding/brotli\";\n\n// With brotli compression\nconst encoder = jsonEncoder({ compressor: brotliCompressor() });\n```\n\n```ts\nimport { jsonParse, jsonStringify } from \"@olli/kvdex/encoding/json\";\n\n// Stringify value\nconst json = jsonStringify({\n  foo: \"bar\",\n  big: 100n,\n});\n\n// Parse value\nconst value = jsonParse(json);\n```\n\n```ts\nimport { jsonDeserialize, jsonSerialize } from \"@olli/kvdex/encoding/json\";\n\n// Serialize value as Uint8Array\nconst serialized = jsonSerialize({\n  foo: \"bar\",\n  big: 100n,\n});\n\n// Deserialize value from Uint8Array\nconst value = jsonDeserialize(serialized);\n```\n\n#### V8\n\nV8-encoder and serialization utilities. Relies on the `node:v8` built-in.\n\n```ts\nimport { v8Encoder } from \"@olli/kvdex/encoding/v8\";\nimport { brotliCompressor } from \"@olli/kvdex/encoding/brotli\";\n\n// V8-encoder without compression\nconst encoder = v8Encoder();\n\n// V8-encoder with brotli compression\nconst encoder = v8Encoder({ compressor: brotliCompressor() });\n```\n\n```ts\nimport { v8Deserialize, v8Serialize } from \"@olli/kvdex/encoding/v8\";\n\n// Serialize value as Uint8Array\nconst serialized = v8Serialize({\n  foo: \"bar\",\n  big: 100n,\n});\n\n// Deserialize value from Uint8Array\nconst value = v8Deserialize(serialized);\n```\n\n#### Brotli\n\nEasy to configure brotli compression for use with the `encoder` option for\ncollections. Relies on the `node:zlib` built-in.\n\n```ts\nimport { brotliCompressor } from \"@olli/kvdex/encoding/brotli\";\n\n// With default options\nconst compressor = brotliCompressor();\n\n// Explicitly set quality level (default is 1)\nconst compressor = brotliCompressor({ quality: 2 });\n```\n\n#### Brotli Sync\n\nEasy to configure synchronous brotli compression for use with the `encoder`\noption for collections. Relies on the `node:zlib` built-in.\n\n```ts\nimport { brotliCompressorSync } from \"@olli/kvdex/encoding/brotli-sync\";\n\n// With default options\nconst compressor = brotliCompressorSync();\n\n// Explicitly set quality level (default is 1)\nconst compressor = brotliCompressorSync({ quality: 2 });\n```\n\n### Zod\n\nExtended support for Zod. Includes schemas for some of the KV-types.\n\n#### Schemas\n\nThe zod extension provides the following schemas, matching their equivalent KV\ntypes:\n\n- `KvIdSchema`\n- `KvKeyPartSchema`\n- `KvKeySchema`\n- `KvValueSchema`\n- `KvArraySchema`\n- `KvObjectSchema`\n\nUsing the `KvIdSchema` to validate model IDs:\n\n```ts\nimport { z } from \"npm:zod\";\nimport { KvIdSchema } from \"@olli/kvdex/zod\";\n\n// Validates that 'postIds' is an array of valid KvIds\nconst UserSchema = z.object({\n  username: z.string(),\n  postIds: z.array(KvIdSchema),\n});\n\n// Validates that 'userId' is a valid KvId\nconst PostSchema = z.object({\n  text: z.string(),\n  userId: KvIdSchema,\n});\n```\n\n### Migrate\n\nA helper script and function for migrating entries from a source KV instance to\na target KV instance. Only migrates `kvdex` entries by default, but optionally\nallows for migrating all entries.\n\n#### Script\n\nRun the migrate script and provide --source and --target arguments. Optionally\npass --all to migrate all entries.\n\n```console\ndeno run -A --unstable-kv jsr:@olli/kvdex/migrate --source=./source.sqlite3 --target=./target.sqlite3\n```\n\n#### Function\n\nUse the migrate function and pass a source KV instance and a target KV instance.\nOptionally pass `all: true` to migrate all entries.\n\n```ts\nimport { migrate } from \"@olli/kvdex/migrate\";\n\nconst source = await Deno.openKv(\"./source.sqlite3\");\nconst target = await Deno.openKv(\"./target.sqlite3\");\n\nawait migrate({\n  source,\n  target,\n});\n```\n\n### KV\n\nSupport for alternative KV backends, such as `Map` and `localStorage`. Can be\nused to employ `kvdex` in the browser or other environments where Deno's KV\nstore is not available, or to adapt other database backends.\n\n#### Map\n\nSupport for `Map` as KV backend.\n\n- Provides a storage adapter, extending backend support to the `Storage`\n  interface (e.g. `localStorage`).\n- Provides an `IndexedDB` adapter, enabling the use of `IndexedDB` as a KV\n  backend.\n\nCreate an in-memory database using `Map` as the KV backend:\n\n```ts\nimport { kvdex } from \"@olli/kvdex\";\nimport { MapKv } from \"@olli/kvdex/kv/map\";\n\n// Equivalent to `new MapKv({ map: new Map() })`\nconst kv = new MapKv();\nconst db = kvdex({ kv });\n```\n\nCreate a persistent database using `localStorage` as the KV backend:\n\n```ts\nimport { kvdex } from \"@olli/kvdex\";\nimport { MapKv, StorageAdapter } from \"@olli/kvdex/kv/map\";\n\n// Equivalent to `new StorageAdapter(localStorage)`\nconst map = new StorageAdapter();\nconst kv = new MapKv({ map });\nconst db = kvdex({ kv });\n```\n\nCreate a persistent database using `IndexedDB` as the KV backend:\n\n```ts\nimport { kvdex } from \"@olli/kvdex\";\nimport { indexedDbAdapter, MapKv } from \"@olli/kvdex/kv/map\";\n\n// Opens an IndexedDB database with default name and store\nconst map = await indexedDbAdapter();\nconst kv = new MapKv({ map });\nconst db = kvdex({ kv });\n```\n\n## Blob Storage\n\nTo store large blob sizes, and bypass the data limit of a single atomic\noperation, a combination of serialized collections and batched atomic operations\ncan be used. By default, batching is disabled to ensure consistency and improve\nperformance.\n\n```ts\nimport { collection, kvdex, model } from \"@olli/kvdex\"\nimport { jsonEncoder } from \"@olli/kvdex/encoding/json\"\n\nconst kv = await Deno.openKv()\nconst db = kvdex({\n  kv: kv,\n  schema: {\n    blobs: collection(model\u003cUint8Array\u003e(), { encoder: jsonEncoder() }),\n  }\n})\n\nconst blob = // read from disk, etc.\n\nconst result = await db.blobs.add(blob, { batched: true })\n```\n\n## Development\n\nAny contributions are welcomed and appreciated. How to contribute:\n\n- Clone this repository\n- Add feature / Refactor\n- Add or refactor tests as needed\n- Ensure code quality (check + lint + format + test) using `deno task prep`\n- Open Pull Request\n\nThis project aims at having as high test coverage as possible to improve code\nquality and to avoid breaking features when refactoring. Therefore it is\nencouraged that any feature contributions are also accompanied by relevant unit\ntests to ensure those features remain stable.\n\nThe goal of kvdex is to provide a type safe, higher-level API to Deno KV, while\nretaining as much of the native functionality as possible. Additionally, the\ncore functionality (excluding extensions) should not rely on any third-party\ndependencies. Please keep this in mind when making any contributions.\n\n## License\n\nPublished under\n[MIT License](https://github.com/oliver-oloughlin/kvdex/blob/main/LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foliver-oloughlin%2Fkvdex","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Foliver-oloughlin%2Fkvdex","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foliver-oloughlin%2Fkvdex/lists"}