{"id":50840355,"url":"https://github.com/patrickjames242/in-memory-db-table","last_synced_at":"2026-06-14T06:08:33.949Z","repository":{"id":347161063,"uuid":"1193085301","full_name":"patrickjames242/in-memory-db-table","owner":"patrickjames242","description":null,"archived":false,"fork":false,"pushed_at":"2026-04-21T02:19:13.000Z","size":323,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-21T04:28:45.751Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/patrickjames242.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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":"2026-03-26T21:27:36.000Z","updated_at":"2026-04-21T02:18:52.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/patrickjames242/in-memory-db-table","commit_stats":null,"previous_names":["patrickjames242/in-memory-db-table"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/patrickjames242/in-memory-db-table","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patrickjames242%2Fin-memory-db-table","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patrickjames242%2Fin-memory-db-table/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patrickjames242%2Fin-memory-db-table/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patrickjames242%2Fin-memory-db-table/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/patrickjames242","download_url":"https://codeload.github.com/patrickjames242/in-memory-db-table/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patrickjames242%2Fin-memory-db-table/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34310843,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-14T02:00:07.365Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":[],"created_at":"2026-06-14T06:08:31.943Z","updated_at":"2026-06-14T06:08:33.942Z","avatar_url":"https://github.com/patrickjames242.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# in-memory-db-table\n\n`in-memory-db-table` is a small TypeScript library for\nMobX-backed in-memory tables with database-style indexed\nequality lookups.\n\nIt is useful for app state that behaves like a\nnormalized relational graph in memory, such as:\n\n- records keyed by a foreign key or status field\n- join tables for many-to-many relationships\n- filtered collections that must stay in sync as data is\n  inserted, updated, or removed\n- lookup-heavy UI state where exact-match reads are\n  common\n\nThe core idea is simple:\n\n- every row is stored by primary key `id`\n- you can opt into secondary indices for selected\n  columns\n- queries are exact-match filters on indexed columns\n- chained filters behave like `AND`\n- the underlying data is MobX-observable, so computed\n  values, reactions, and UI bindings can observe query\n  results\n\nThis package is intentionally small. It does not try to\nbe a full ORM, a SQL parser, or a normalized entity\nframework. It is a focused utility for “I want a fast,\nobservable, in-memory table with predictable lookup\nsemantics.”\n\n## Install\n\n```bash\nnpm install in-memory-db-table mobx\n```\n\n## Peer Dependencies\n\n- `mobx` `\u003e=6.0.0 \u003c7`\n\n## Who This Is For\n\nThis library is a good fit when you:\n\n- already keep client-side data in MobX state\n- want to normalize records by `id`\n- repeatedly answer questions like “give me all rows for\n  this foreign key”\n- want to chain exact-match filters across a small set of\n  indexed columns\n- want a lightweight abstraction instead of scanning\n  arrays manually all over the codebase\n\nThis library is especially useful for feature state that\nbehaves like a relational graph in the UI:\n\n- many-to-many join tables\n- lookup tables for ids -\u003e entities\n- filtered collections that must stay in sync as data is\n  inserted, updated, or removed\n\n## Mental Model\n\nThink of an `InMemoryDBTable` as a MobX-observable table\nwith:\n\n- a mandatory primary key: `id`\n- zero or more secondary equality indices\n- an immutable query builder for composing filters\n- snapshot-style read APIs\n- mutation APIs that keep indices in sync automatically\n\nIf you have ever modeled UI data with:\n\n- a `Map\u003cstring, T\u003e` for direct access\n- extra `Map\u003ccolumnValue, Set\u003cid\u003e\u003e` structures for\n  filtering\n- helper methods for “first”, “exists”, “count”, and\n  “delete matching rows”\n\nthis package formalizes that pattern into one reusable\nprimitive.\n\n## Quick Start\n\n```ts\nimport { autorun } from 'mobx';\nimport { InMemoryDBTable } from 'in-memory-db-table';\n\ntype CourseSection = {\n  id: string;\n  courseId: string;\n  roomId: string | null;\n  colorHex: string;\n};\n\nconst courseSections = new InMemoryDBTable\u003c\n  CourseSection,\n  'courseId' | 'roomId'\n\u003e([], ['courseId', 'roomId']);\n\ncourseSections.upsert([\n  {\n    id: 'section-1',\n    courseId: 'course-1',\n    roomId: 'room-a',\n    colorHex: '#2463eb',\n  },\n  {\n    id: 'section-2',\n    courseId: 'course-1',\n    roomId: null,\n    colorHex: '#1f8f5f',\n  },\n]);\n\nconst sameCourse = courseSections\n  .whereIndexedColumn('courseId', 'course-1')\n  .get();\n\nconsole.log(sameCourse.length); // 2\n\nconst dispose = autorun(() =\u003e {\n  console.log(\n    'Sections in room-a:',\n    courseSections\n      .whereIndexedColumn('roomId', 'room-a')\n      .count()\n  );\n});\n\ncourseSections.delete('section-1');\n\ndispose();\n```\n\n## Core API\n\n## `new InMemoryDBTable(records?, columnsToIndex?)`\n\nCreates a table.\n\n```ts\nconst table = new InMemoryDBTable\u003cUser, 'role' | 'teamId'\u003e(\n  [],\n  ['role', 'teamId']\n);\n```\n\n### Rules\n\n- `T` must include `id: string`\n- `columnsToIndex` should only include columns you plan\n  to query frequently\n- `id` is always available as an implicit primary-key\n  index\n- only configured indexed columns can be used with\n  `whereIndexedColumn(...)`\n\n### What gets stored internally\n\nThe table maintains:\n\n- a record map: `id -\u003e record`\n- one secondary index per configured column:\n  `columnValue -\u003e Set\u003cid\u003e`\n\nWhenever you insert, update, or delete rows, those index\nmaps are kept in sync for you.\n\n## `table.upsert(record)` / `table.upsert(records)`\n\nAdds or replaces rows by `id`.\n\n```ts\ntable.upsert({\n  id: 'teacher-1',\n  departmentId: 'science',\n  name: 'Ada Lovelace',\n});\n```\n\n```ts\ntable.upsert([\n  {\n    id: 'teacher-1',\n    departmentId: 'science',\n    name: 'Ada Lovelace',\n  },\n  {\n    id: 'teacher-2',\n    departmentId: 'math',\n    name: 'Grace Hopper',\n  },\n]);\n```\n\n### Update semantics\n\nIf a row with the same `id` already exists:\n\n- the old record is replaced\n- all configured secondary indices are updated\n- old index entries that no longer apply are removed\n\nThat behavior is important for UI state where a record’s\nforeign key can change over time. For example, if a row\nmoves from `teacherId = a` to `teacherId = b`, queries for\n`a` stop returning it and queries for `b` start returning\nit immediately.\n\n## `table.delete(id)` / `table.delete(ids)`\n\nDeletes one or more rows by primary key.\n\n```ts\ntable.delete('entry-1');\ntable.delete(['entry-2', 'entry-3']);\n```\n\nMissing ids are ignored. All secondary indices are\ncleaned up automatically.\n\n## `table.get()`\n\nReturns every row currently in the table.\n\n```ts\nconst allRows = table.get();\n```\n\nThis is useful when:\n\n- you want a full snapshot\n- the table is small enough that filtering in memory is\n  acceptable\n- you are hydrating another derived structure\n\n## `table.get(id)`\n\nReturns a single row or `null`.\n\n```ts\nconst row = table.get('section-1');\n```\n\nThis is the direct primary-key lookup path.\n\n## `table.get(ids)`\n\nReturns the rows for the provided ids, in the same order\nas the input, while skipping ids that are missing.\n\n```ts\nconst teachers = teachersTable.get([\n  'teacher-3',\n  'teacher-1',\n  'missing-teacher',\n]);\n```\n\nThat usage pattern shows up frequently when one table\nstores only relationship rows and another table stores the\nentity rows. A common pattern looks like:\n\n1. query a join table for `teacherId`s or `classId`s\n2. feed those ids into the entity table\n3. get back the matching loaded entities in a stable order\n\n## `table.whereIndexedColumn(column, value)`\n\nStarts an indexed query.\n\n```ts\nconst query = table.whereIndexedColumn(\n  'teacherId',\n  'teacher-1'\n);\n```\n\nThe returned query is immutable. Each additional filter\nreturns a new query instance.\n\n```ts\nconst results = table\n  .whereIndexedColumn('teacherId', 'teacher-1')\n  .whereIndexedColumn('room', 'room-a')\n  .get();\n```\n\nThis behaves like:\n\n```sql\nWHERE teacher_id = 'teacher-1'\n  AND room = 'room-a'\n```\n\n## `table.whereIndexedColumnIn(column, values)`\n\nStarts an indexed query that matches any of the provided\nvalues for a single indexed column.\n\n```ts\nconst query = table.whereIndexedColumnIn(\n  'teacherId',\n  ['teacher-1', 'teacher-3']\n);\n```\n\nYou can chain additional indexed filters after it.\n\n```ts\nconst results = table\n  .whereIndexedColumnIn('teacherId', [\n    'teacher-1',\n    'teacher-2',\n  ])\n  .whereIndexedColumn('room', 'room-a')\n  .get();\n```\n\nThis behaves like:\n\n```sql\nWHERE teacher_id IN ('teacher-1', 'teacher-2')\n  AND room = 'room-a'\n```\n\nFor `id` queries, missing ids are ignored the same way\nthey are with `table.get(ids)`.\n\n### Important limitation\n\nThis package supports exact-match lookups on indexed\ncolumns only. It does not support:\n\n- partial string matching\n- range queries\n- sorting operators\n- joins\n- arbitrary grouped OR conditions across different columns\n\nIf you need those, fetch the rows you want and derive the\nrest in normal JavaScript.\n\n## Query API\n\nOnce you have a query, you can use the following methods.\n\n## `query.get()`\n\nReturns the matching rows.\n\n```ts\nconst rows = table\n  .whereIndexedColumn('courseId', 'course-1')\n  .get();\n```\n\nInternally the query resolves the indexed candidate sets,\npicks the smallest one, and intersects the rest. That\nkeeps chained equality queries efficient without scanning\nevery row.\n\n## `query.get(column)`\n\nProjects a single column out of the matched rows.\n\n```ts\nconst teacherIds = courseSectionTeachers\n  .whereIndexedColumn('courseSectionId', 'section-1')\n  .get('teacherId');\n```\n\nThis is a common usage pattern for join-table style\nrecords. Query by one foreign key, then project the\nopposite side of the relationship directly.\n\nExamples:\n\n- “Give me every `teacherId` attached to this\n  `courseSectionId`.”\n- “Give me every `classId` attached to this\n  `courseSectionId`.”\n- “Give me every `conflictId` attached to this period.”\n\n## `query.get(column, true)`\n\nProjects a column and removes duplicates.\n\n```ts\nconst uniqueRoomIds = entries\n  .whereIndexedColumn('teacherId', 'teacher-1')\n  .get('roomId', true);\n```\n\n## `query.exists()`\n\nReturns `true` if any row matches.\n\n```ts\nconst hasConflict = conflicts\n  .whereIndexedColumn('id', conflictId)\n  .whereIndexedColumn('periodId', periodId)\n  .exists();\n```\n\nThis pattern is useful for fast guard clauses and cheap\nboolean checks in computed values.\n\n## `query.count()`\n\nCounts matching rows without allocating the result array.\n\n```ts\nconst count = conflictsTable\n  .whereIndexedColumn('periodId', periodId)\n  .count();\n```\n\nThis is a strong fit for:\n\n- badge counts\n- summary pills\n- empty-state checks\n- rendering optimizations where you only need the total\n\n## `query.first()`\n\nReturns the first matching row or `null`.\n\n```ts\nconst row = table\n  .whereIndexedColumn('teacherId', 'teacher-2')\n  .first();\n```\n\nUse this when the logical cardinality is “zero or one,”\nor when any single match is sufficient.\n\n## `query.delete()`\n\nDeletes every row that matches the query.\n\n```ts\ncourseSectionTeachers\n  .whereIndexedColumn('courseSectionId', 'section-1')\n  .delete();\n```\n\nThis is especially convenient for join-table replacement\nflows:\n\n1. delete the existing relationship rows for an owner\n2. insert the replacement rows\n\nThat is a common pattern in feature state when the server\nreturns the new authoritative list for a relationship.\n\n## `table.uniqueColumnValues(column)`\n\nReturns a `Set` of unique values for an indexed column, or\nfor `id`.\n\n```ts\nconst dayNumbers = periods.uniqueColumnValues(\n  'day_of_the_week'\n);\n```\n\nThis was added for UI patterns where you want to build\nfilters or grouped views from the current contents of the\ntable without rescanning every record manually.\n\nExamples:\n\n- list every teacher that currently appears\n- list every day value represented in period rows\n- build facet-like filter controls from loaded data\n\n## MobX Behavior\n\nThe table and its indices are backed by MobX observable\nmaps and sets.\n\nThat means MobX reactions can observe:\n\n- full-table reads\n- indexed query counts\n- query existence checks\n- query results used in computed values or `autorun`\n\nExample:\n\n```ts\nimport { autorun } from 'mobx';\n\nconst dispose = autorun(() =\u003e {\n  const teacherOneCount = classes\n    .whereIndexedColumn('teacherId', 'teacher-1')\n    .count();\n\n  console.log(teacherOneCount);\n});\n```\n\nWhen matching rows are inserted, updated, or deleted, the\nreaction re-runs because the underlying observable state\nchanged.\n\n## Real Usage Patterns\n\nMultiple tables can be composed together to represent a\nnormalized client-side data graph. These examples show\ncommon ways to use the library in that style.\n\n## 1. Entity tables\n\nStore full entities by id, optionally with a few useful\nsecondary indices.\n\n```ts\ntype CourseSection = {\n  id: string;\n  courseId: string;\n  roomId: string | null;\n  colorHex: string;\n};\n\nconst courseSections = new InMemoryDBTable\u003c\n  CourseSection,\n  'courseId'\n\u003e([], ['courseId']);\n```\n\nUse cases:\n\n- get a section by id\n- get all sections for a course\n- update a section in place\n\n## 2. Join tables\n\nStore relationship rows and project the opposite id back\nout of the query.\n\n```ts\ntype CourseSectionTeacher = {\n  id: string;\n  courseSectionId: string;\n  teacherId: string;\n};\n\nconst courseSectionTeachers = new InMemoryDBTable\u003c\n  CourseSectionTeacher,\n  'courseSectionId' | 'teacherId'\n\u003e([], ['courseSectionId', 'teacherId']);\n\nconst teacherIds = courseSectionTeachers\n  .whereIndexedColumn('courseSectionId', 'section-1')\n  .get('teacherId');\n```\n\nThis lets feature-level state stay very explicit and easy\nto reason about.\n\n## 3. Composite filtering\n\nChain multiple indexed columns to narrow a result set.\n\n```ts\nconst entriesInPeriodForSection = periodEntries\n  .whereIndexedColumn('courseSectionId', 'section-1')\n  .whereIndexedColumn('periodId', 'period-3')\n  .get();\n```\n\nThis is effectively a composite lookup without requiring\na dedicated combined index declaration.\n\n## 4. Fast existence checks across normalized data\n\nUse one query to pull relationship ids, then use another\nquery to validate context.\n\n```ts\nconst hasConflictInPeriod = conflictCourseSections\n  .whereIndexedColumn('courseSectionId', 'section-1')\n  .get('conflictId')\n  .some((conflictId) =\u003e\n    conflicts\n      .whereIndexedColumn('id', conflictId)\n      .whereIndexedColumn('periodId', 'period-3')\n      .exists()\n  );\n```\n\nThis keeps the data normalized while still giving\nfeature-specific selectors readable building blocks.\n\n## 5. Deriving entities from relationship rows\n\nFetch relationship ids first, then load the entities.\n\n```ts\nconst classIds = courseSectionClasses\n  .whereIndexedColumn('courseSectionId', 'section-1')\n  .get('classId');\n\nconst classesForSection = classesTable.get(classIds);\n```\n\nThis pattern is one of the main reasons the `get(ids)`\noverload exists.\n\n## Design Constraints\n\nThis library deliberately makes a few tradeoffs:\n\n- only `id` is treated as the primary key\n- indices are equality-only\n- secondary indices are opt-in\n- query ordering follows the iteration order of the\n  underlying matching id set\n- there is no cross-table abstraction; composition is done\n  in your own selectors and state objects\n\nThose constraints keep the implementation small and the\nruntime behavior predictable.\n\n## TypeScript Notes\n\nThe generic parameters are:\n\n```ts\nInMemoryDBTable\u003cT, IndexedColumns\u003e\n```\n\nWhere:\n\n- `T` is the record shape and must include `id: string`\n- `IndexedColumns` is a union of non-`id` keys you want to\n  allow in `whereIndexedColumn(...)`\n\nExample:\n\n```ts\ntype PeriodEntry = {\n  id: string;\n  courseSectionId: string;\n  periodId: string;\n  orderNum: number;\n};\n\nconst entries = new InMemoryDBTable\u003c\n  PeriodEntry,\n  'courseSectionId' | 'periodId'\n\u003e([], ['courseSectionId', 'periodId']);\n```\n\nIf you try to query a column that is not part of\n`IndexedColumns` (or `id`), TypeScript will reject it.\n\n## Testing\n\nThe package includes Jest tests covering:\n\n- primary-key reads\n- single-column and multi-column indexed queries\n- index updates after upserts\n- index cleanup after deletes\n- column projection\n- distinct projection\n- unique value extraction\n- query helpers like `exists`, `count`, `first`, and\n  `delete`\n- MobX reaction behavior\n\nRun them with:\n\n```bash\nnpm test\n```\n\n## Build\n\nBuild the package with:\n\n```bash\nnpm run build\n```\n\nThe package is bundled with `tsup` and emits:\n\n- ESM output\n- CommonJS output\n- type declarations\n- source maps\n\n## When Not To Use This\n\nThis is probably the wrong abstraction if:\n\n- your data is naturally just one small array\n- you need server-synchronized caching semantics like\n  TanStack Query\n- you need relational writes, joins, or ad hoc querying\n- you need sorted indices or range scans\n- you are not using MobX and do not care about observable\n  data structures\n\n## API Summary\n\n```ts\nconst table = new InMemoryDBTable\u003cT, IndexedColumns\u003e(\n  records?,\n  indexedColumns?\n);\n\ntable.upsert(record);\ntable.upsert(records);\n\ntable.delete(id);\ntable.delete(ids);\n\ntable.get();\ntable.get(id);\ntable.get(ids);\n\ntable.whereIndexedColumn(column, value);\ntable.whereIndexedColumnIn(column, values);\ntable.uniqueColumnValues(column);\n\nquery.whereIndexedColumn(column, value);\nquery.whereIndexedColumnIn(column, values);\nquery.get();\nquery.get(column, distinct?);\nquery.exists();\nquery.count();\nquery.first();\nquery.delete();\n```\n\n## License\n\n`UNLICENSED`\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpatrickjames242%2Fin-memory-db-table","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpatrickjames242%2Fin-memory-db-table","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpatrickjames242%2Fin-memory-db-table/lists"}