From 1c4b2bc49157b9179c0b49a078e50b3c1df520c7 Mon Sep 17 00:00:00 2001 From: 3x-haust Date: Sun, 14 Jun 2026 01:23:23 +0900 Subject: [PATCH 01/31] feat: add gitdb runtime benchmarks and site --- .github/workflows/pages.yml | 31 ++ .gitignore | 2 + README.md | 118 ++++-- docs/ARCHITECTURE.md | 40 +- docs/BENCHMARKS.md | 46 ++- docs/README.ko.md | 113 ++++-- package.json | 26 +- scripts/benchmark-compare.mjs | 105 ++++++ scripts/benchmark-report.mjs | 219 +++++++++++ scripts/benchmark.mjs | 30 +- site/app.js | 151 ++++++++ site/assets/runtime-map.svg | 56 +++ site/benchmark.json | 92 +++++ site/benchmark.md | 5 + site/index.html | 199 ++++++++++ site/styles.css | 532 +++++++++++++++++++++++++++ src/errors.ts | 4 + src/github/github-plaintext-store.ts | 22 +- src/index.ts | 13 +- src/orm/index.ts | 188 ++++++++++ src/sql/database.ts | 78 ++++ src/sql/engine.ts | 207 +++++++---- src/sql/transaction.ts | 41 +++ src/storage/local-plaintext-store.ts | 18 +- src/storage/plaintext-codec.ts | 6 +- src/types.ts | 1 + tests/benchmark-script.test.ts | 43 +++ tests/github-plaintext-store.test.ts | 130 +++++++ tests/orm.test.ts | 62 ++++ tests/package-metadata.test.ts | 40 ++ tests/plaintext-store.test.ts | 103 +++++- tests/site.test.ts | 43 +++ tests/sql-engine.test.ts | 201 +++++++++- 33 files changed, 2801 insertions(+), 164 deletions(-) create mode 100644 .github/workflows/pages.yml create mode 100644 scripts/benchmark-compare.mjs create mode 100644 scripts/benchmark-report.mjs create mode 100644 site/app.js create mode 100644 site/assets/runtime-map.svg create mode 100644 site/benchmark.json create mode 100644 site/benchmark.md create mode 100644 site/index.html create mode 100644 site/styles.css create mode 100644 src/orm/index.ts create mode 100644 src/sql/database.ts create mode 100644 src/sql/transaction.ts create mode 100644 tests/benchmark-script.test.ts create mode 100644 tests/github-plaintext-store.test.ts create mode 100644 tests/orm.test.ts create mode 100644 tests/package-metadata.test.ts create mode 100644 tests/site.test.ts diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..6ec49ad --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,31 @@ +name: Deploy site + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + id-token: write + pages: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/configure-pages@v5 + - uses: actions/upload-pages-artifact@v3 + with: + path: site + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 96507ce..4a80b7a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ examples/express-prisma/generated .DS_Store coverage *.tsbuildinfo +.omo +plans diff --git a/README.md b/README.md index 188cafc..cec129b 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,33 @@ # GitDB -English | [한국어](docs/README.ko.md) +English | [한국어](docs/README.ko.md) | [Website](https://3x-haust.github.io/gitdb/) -GitDB turns a GitHub repository into a project-scoped database. +GitDB turns a GitHub repository into a project-scoped database runtime. -It exposes a PostgreSQL-compatible local TCP endpoint, accepts SQL from tools -such as Prisma and `pg`, executes the query in GitDB's engine, and persists the -database into a dedicated GitHub repository. Public repositories can be used as -human-readable data dashboards or as encrypted object stores. +The database hot path is local: GitDB opens a runtime process, executes queries +in its SQL engine, serializes writes through a transaction executor, and persists +state as a manifest, mutation log, and visible snapshots. GitHub is the durable +and auditable storage layer, not the place GitDB visits for every `SELECT`. + +GitDB now exposes two application entry points: a first-party TypeORM-style +`DataSource`/repository API for new apps, and a PostgreSQL-compatible local TCP +facade for tools such as Prisma and `pg`. ```text -Express / Prisma / pg +GitDB ORM / Prisma / pg | | postgresql://127.0.0.1:7432/main v -GitDB PostgreSQL facade +GitDB local runtime | - | in-memory SQL engine + manifest/log replay + | transaction executor + SQL engine + manifest/log replay v GitHub repository ``` -GitDB is not SQLite-over-GitHub, and it does not upload `.db` files. The GitHub -repository is the durable database store. +GitDB is not SQLite-over-GitHub, and it does not upload `.db` files. The +PostgreSQL facade is only a compatibility layer; the storage-engine work is the +core of the project. ## Why GitDB @@ -33,8 +38,8 @@ primitives for project data. Use it when you want: - A database repository per project, for example `my-app-db` -- SQL and ORM access without writing custom Prisma, TypeORM, Drizzle, or Kysely - providers +- A first-party repository API for typed CRUD without adopting a separate ORM +- PostgreSQL-compatible access for existing Prisma, `pg`, or raw SQL clients - Public data that can be inspected and edited from GitHub's web UI - Encrypted data in public or private repositories - Auditable commits for agent memory, demos, content tools, config tools, and @@ -47,8 +52,9 @@ transactions, or full PostgreSQL compatibility today. | Area | Current behavior | | --- | --- | -| ORM access | PostgreSQL-style local endpoint, so existing PostgreSQL clients can connect | +| ORM access | First-party `DataSource`/repository API plus PostgreSQL-style local endpoint | | SQL | `CREATE TABLE`, `INSERT`, `DELETE`, `SELECT`, joins, grouping, ordering, aggregates, and common raw-query flows | +| Runtime guarantees | Single-process transaction queue, manifest-gated log replay, and checkpointed visible snapshots | | GitHub storage | Dedicated repository per database, created on first write when permissions allow | | Public plaintext mode | `table/schema.json` and `table/data.json` are visible and editable in GitHub | | Encrypted mode | AES-256-GCM encrypted manifest and mutation log files | @@ -114,6 +120,34 @@ pnpm install pnpm build ``` +Use the first-party ORM-style repository API: + +```ts +import { LocalPlaintextStore, createGitDbDataSource, defineEntity } from "@3xhaust/gitdb" + +type Person = { + readonly id: string + readonly name: string + readonly team_id: string +} + +const PersonEntity = defineEntity({ + columns: { id: "STRING", name: "STRING", team_id: "STRING" }, + primaryKey: "id", + tableName: "people", +}) + +const dataSource = await createGitDbDataSource({ + entities: [PersonEntity], + store: new LocalPlaintextStore({ root: ".gitdb" }), + synchronize: true, +}) + +const people = dataSource.getRepository(PersonEntity) +await people.save({ id: "p1", name: "Lin", team_id: "storage" }) +const storagePeople = await people.find({ where: { team_id: "storage" } }) +``` + Run the local PostgreSQL facade with encrypted local storage: ```bash @@ -251,20 +285,27 @@ managed mode where that runtime is trusted to process plaintext query results. ## Architecture -GitDB is split into three layers: +GitDB is split into four layers: -1. PostgreSQL-compatible facade - - Opens a local TCP endpoint. - - Lets existing clients use a normal PostgreSQL connection string. - - Avoids ORM-specific driver/provider work. +1. First-party ORM API + - Provides `DataSource`, `Repository`, `save`, `find`, `findOne`, `delete`, + and explicit transaction access. + - Keeps new apps close to GitDB's runtime instead of forcing a PostgreSQL + compatibility hop. -2. SQL engine +2. PostgreSQL-compatible facade + - Opens a local TCP endpoint for existing clients. + - Lets Prisma, `pg`, and raw SQL clients use a normal PostgreSQL connection + string. + +3. SQL engine - Owns schema, mutation execution, query execution, joins, grouping, and result rows. + - Serializes local write transactions before persistence. - Targets the PostgreSQL subset produced by common Node.js clients and ORM raw-query flows. -3. Storage providers +4. Storage providers - Local encrypted store for development and tests. - Local plaintext store for visible snapshots. - GitHub encrypted/plaintext stores for remote durability. @@ -279,6 +320,13 @@ Run local benchmarks: pnpm benchmark ``` +Compare the current runtime with the previous documented run and refresh the +website evidence: + +```bash +GITDB_BENCH_ROWS=250 pnpm benchmark:compare +``` + Run the GitHub write benchmark: ```bash @@ -289,17 +337,20 @@ Latest measured run: | Scenario | Rows | Write ms | Writes/s | Join ms | Reopen ms | | --- | ---: | ---: | ---: | ---: | ---: | -| local plaintext visible snapshots | 250 | 1443.90 | 173.14 | 32.97 | 140.05 | -| local encrypted mutation log | 250 | 761.99 | 328.09 | 4.31 | 322.51 | -| postgres facade over local encrypted | 250 | 987.93 | 253.05 | 19.74 | 0.00 | -| github plaintext contents api | 2 | 14157.49 | 0.14 | 6.70 | 1812.62 | - -Interpretation: local execution is already usable for experiments and -low-frequency workloads. Direct per-mutation GitHub Contents API writes are too -slow for the hot path. In the current implementation, live queries execute in an -in-memory SQL engine restored from a manifest, mutation log, or visible -snapshot. WAL, indexes, and batched Git commits are planned storage-engine work, -not current guarantees. +| local plaintext throttled visible snapshots | 250 | 97.55 | 2562.73 | 3.28 | 47.42 | +| local encrypted mutation log | 250 | 97.23 | 2571.34 | 0.87 | 45.26 | +| postgres facade over local encrypted | 250 | 148.46 | 1683.91 | 2.31 | 0.00 | + +Compared with the previous documented run, local plaintext writes improved from +173.14 writes/s to 2562.73 writes/s, or 14.80x. Local encrypted writes improved +from 328.09 writes/s to 2571.34 writes/s, or 7.84x. + +Interpretation: local execution is usable for examples, demos, and low-frequency +workloads. Single-statement mutations no longer clone the whole in-memory +database before every write; persistence failures restore committed manifest +state. Direct per-mutation GitHub Contents API writes are still too slow for the +hot path. WAL, indexes, compaction, and batched Git commits remain +storage-engine work, not current full-OLTP guarantees. See [docs/BENCHMARKS.md](docs/BENCHMARKS.md). @@ -352,6 +403,9 @@ pnpm check pnpm test pnpm build pnpm benchmark +pnpm benchmark:evaluate +pnpm pack:dry-run +pnpm publish:dry-run pnpm start:facade pnpm example ``` diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7fd9779..08ac519 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,6 +1,17 @@ # Architecture -GitDB has four layers. +GitDB has five layers. + +## First-Party ORM API + +New applications can use GitDB directly through `createGitDbDataSource`, +`defineEntity`, and repository methods such as `save`, `find`, `findOne`, and +`delete`. This is intentionally closer to TypeORM's `DataSource` and repository +shape than to a PostgreSQL-only facade. + +The ORM API still executes through the same local runtime and transaction +executor as raw SQL. It is a convenience surface over the storage engine, not a +separate persistence path. ## PostgreSQL Facade @@ -19,19 +30,36 @@ execution so common ORM-generated SQL can run. Unsupported PostgreSQL features return explicit errors instead of falling back silently. +Mutations are serialized through a single-engine transaction queue. A +single-statement mutation uses the hot local database directly, then persists the +mutation segment and manifest. If persistence fails, the engine restores the last +committed manifest state before returning the error. Explicit multi-statement +transactions still run against an isolated in-memory database clone and swap the +live database only after all segments and the manifest persist successfully. The +manifest is updated in memory only after the durable manifest write succeeds, so +a later mutation cannot publish orphan segments from a failed transaction. + ## Storage Engine -GitDB persists schema and data changes as encrypted mutation segments: +GitDB persists schema and data changes as mutation segments plus a manifest: ```text -gitdb/v1/manifest.enc -gitdb/v1/log/.enc +gitdb/v1/manifest.json or manifest.enc +gitdb/v1/log/.json or .enc ``` -On startup, the engine reads the manifest, decrypts the mutation log, and replays -it into the hot query engine. This avoids GitHub round-trips during query +On startup, the engine restores a visible snapshot when its checkpoint sequence +matches the manifest. Otherwise it reads the manifest and replays the mutation +log into the hot query engine. This avoids GitHub round-trips during query execution. +Plaintext visible snapshots are dashboard artifacts and cold-start checkpoints. +They can be written every mutation for maximum visibility or throttled by +mutation interval for faster local workloads. Snapshot table files are written +before `snapshot.json`; the checkpoint is the final marker that lets the engine +trust a visible snapshot. Once a manifest has committed mutations, visible files +without a checkpoint are ignored and the mutation log is replayed instead. + ## GitHub Provider The GitHub provider uses the Repository Contents API to read and create/update diff --git a/docs/BENCHMARKS.md b/docs/BENCHMARKS.md index 4681a01..0eca8b6 100644 --- a/docs/BENCHMARKS.md +++ b/docs/BENCHMARKS.md @@ -6,26 +6,52 @@ GitHub is the durable sync layer, not the per-query hot path. ## Environment -- Date: 2026-06-09 KST -- Machine: Apple Silicon laptop, Node.js 20.19.1 -- Command: `pnpm benchmark` -- GitHub command: `GITDB_BENCH_GITHUB_ROWS=2 pnpm benchmark:github` +- Date: 2026-06-14 KST +- Machine: macOS 26.5.1 arm64, Node.js 24.11.0 +- Command: `GITDB_BENCH_ROWS=250 pnpm benchmark:compare` +- JSON evaluator: `GITDB_BENCH_OUTPUT=.gitdb/bench-current.json pnpm benchmark:evaluate` +- Baseline: `HEAD~1:docs/BENCHMARKS.md` +- Site evidence: `site/benchmark.json` - Workload: create `teams` and `people`, insert rows, execute a join, reopen the store where applicable. -## Results +## Current Local Results + +| Scenario | Rows | Write ms | Writes/s | Join ms | Reopen ms | +| --- | ---: | ---: | ---: | ---: | ---: | +| local plaintext throttled visible snapshots | 250 | 97.55 | 2562.73 | 3.28 | 47.42 | +| local encrypted mutation log | 250 | 97.23 | 2571.34 | 0.87 | 45.26 | +| postgres facade over local encrypted | 250 | 148.46 | 1683.91 | 2.31 | 0.00 | + +## Previous-Version Comparison + +| Scenario | Previous writes/s | Current writes/s | Change | Write ms change | Join ms change | +| --- | ---: | ---: | ---: | ---: | ---: | +| local plaintext throttled visible snapshots | 173.14 | 2562.73 | +1380.15% | +93.24% | +90.04% | +| local encrypted mutation log | 328.09 | 2571.34 | +683.73% | +87.24% | +79.91% | +| postgres facade over local encrypted | 253.05 | 1683.91 | +565.45% | +84.97% | +88.30% | + +## Historical GitHub Contents API + +This was not rerun during the local performance pass. It remains the historical +remote-write reference from 2026-06-09 KST: | Scenario | Rows | Write ms | Writes/s | Join ms | Reopen ms | | --- | ---: | ---: | ---: | ---: | ---: | -| local plaintext visible snapshots | 250 | 1443.90 | 173.14 | 32.97 | 140.05 | -| local encrypted mutation log | 250 | 761.99 | 328.09 | 4.31 | 322.51 | -| postgres facade over local encrypted | 250 | 987.93 | 253.05 | 19.74 | 0.00 | | github plaintext contents api | 2 | 14157.49 | 0.14 | 6.70 | 1812.62 | ## Interpretation -Local writes are already usable for development workloads. The PostgreSQL wire -facade adds modest overhead while preserving normal ORM connectivity. +Local writes are now fast enough for examples, demos, and low-frequency project +data. The hot-path improvement is storage-engine shaped: a single-statement +mutation no longer clones and restores the whole in-memory database before every +write. It mutates the live local engine, persists through the manifest-gated log, +and restores committed state if persistence fails. Explicit multi-statement +transactions still use an isolated transaction database for rollback. + +The first-party ORM API avoids the PostgreSQL wire hop for new apps, while the +facade remains useful for existing clients. Visible snapshots are checkpointed +instead of being rewritten on every local plaintext mutation. GitHub Contents API writes are not acceptable as the hot write path. The current GitHub plaintext mode writes multiple files per SQL mutation: log segment, diff --git a/docs/README.ko.md b/docs/README.ko.md index f3bf955..2b1d819 100644 --- a/docs/README.ko.md +++ b/docs/README.ko.md @@ -1,29 +1,34 @@ # GitDB -[English](../README.md) | 한국어 +[English](../README.md) | 한국어 | [Website](https://3x-haust.github.io/gitdb/) -GitDB는 GitHub repository 하나를 프로젝트 전용 데이터베이스처럼 쓰게 해주는 -GitHub-native database입니다. +GitDB는 GitHub repository 하나를 프로젝트 전용 데이터베이스 runtime으로 쓰게 +해주는 GitHub-native database입니다. -애플리케이션은 GitDB가 여는 PostgreSQL 호환 로컬 TCP endpoint에 접속합니다. -Prisma나 `pg` 같은 기존 PostgreSQL 클라이언트는 평범한 -`postgresql://127.0.0.1:7432/main` 주소로 SQL을 보내고, GitDB는 그 SQL을 자체 -엔진에서 실행한 뒤 전용 GitHub repository에 데이터베이스 상태를 저장합니다. +query hot path는 로컬입니다. GitDB runtime이 SQL engine에서 query를 실행하고, +write는 transaction executor를 통해 직렬화한 뒤 manifest, mutation log, visible +snapshot으로 상태를 저장합니다. GitHub는 매 `SELECT`마다 접근하는 곳이 아니라 +durable/auditable storage layer입니다. + +애플리케이션은 GitDB의 first-party TypeORM 스타일 `DataSource`/repository API를 +직접 쓰거나, Prisma와 `pg` 같은 기존 PostgreSQL client용 local TCP facade에 +접속할 수 있습니다. ```text -Express / Prisma / pg +GitDB ORM / Prisma / pg | | postgresql://127.0.0.1:7432/main v -GitDB PostgreSQL facade +GitDB local runtime | - | in-memory SQL engine + manifest/log replay + | transaction executor + SQL engine + manifest/log replay v GitHub repository ``` GitDB는 SQLite를 GitHub에 올리는 도구가 아닙니다. `.db` 파일을 업로드하지 -않습니다. GitHub repository 자체가 GitDB의 durable database store입니다. +않습니다. PostgreSQL facade는 호환 레이어이고, 핵심은 storage engine과 local +runtime입니다. ## 왜 GitDB인가 @@ -34,7 +39,8 @@ repo, access control이 있습니다. GitDB는 이 GitHub primitive를 프로젝 GitDB가 잘 맞는 경우: - 프로젝트마다 전용 데이터베이스 repo를 두고 싶을 때, 예: `my-app-db` -- Prisma, TypeORM, Drizzle, Kysely provider를 직접 만들고 싶지 않을 때 +- 별도 ORM provider 없이 typed repository API를 쓰고 싶을 때 +- Prisma, `pg`, raw SQL client를 PostgreSQL 호환 endpoint로 연결하고 싶을 때 - public demo 데이터를 GitHub 웹 UI에서 바로 보고 수정하고 싶을 때 - public/private repo에 데이터를 암호화해서 저장하고 싶을 때 - agent memory, demo, content tool, config tool, 저빈도 앱 데이터를 commit @@ -50,8 +56,9 @@ GitDB가 맞지 않는 경우: | 영역 | 현재 동작 | | --- | --- | -| ORM 접근 | PostgreSQL 스타일 로컬 endpoint를 열어 기존 PostgreSQL client가 접속 | +| ORM 접근 | First-party `DataSource`/repository API와 PostgreSQL 스타일 local endpoint | | SQL | `CREATE TABLE`, `INSERT`, `DELETE`, `SELECT`, join, group, order, aggregate, 일반적인 raw query 흐름 | +| Runtime 보장 | single-process transaction queue, manifest-gated log replay, checkpointed visible snapshot | | GitHub 저장소 | 데이터베이스마다 전용 repo 사용, 권한이 있으면 최초 write 시 repo 생성 | | Public plaintext mode | `table/schema.json`, `table/data.json`을 GitHub에서 직접 확인/수정 | | Encrypted mode | AES-256-GCM으로 암호화된 manifest/log 저장 | @@ -117,6 +124,34 @@ pnpm install pnpm build ``` +first-party ORM 스타일 repository API 사용: + +```ts +import { LocalPlaintextStore, createGitDbDataSource, defineEntity } from "@3xhaust/gitdb" + +type Person = { + readonly id: string + readonly name: string + readonly team_id: string +} + +const PersonEntity = defineEntity({ + columns: { id: "STRING", name: "STRING", team_id: "STRING" }, + primaryKey: "id", + tableName: "people", +}) + +const dataSource = await createGitDbDataSource({ + entities: [PersonEntity], + store: new LocalPlaintextStore({ root: ".gitdb" }), + synchronize: true, +}) + +const people = dataSource.getRepository(PersonEntity) +await people.save({ id: "p1", name: "Lin", team_id: "storage" }) +const storagePeople = await people.find({ where: { team_id: "storage" } }) +``` + encrypted local storage로 PostgreSQL facade 실행: ```bash @@ -254,20 +289,27 @@ plaintext를 처리할 수 있습니다. 이건 의도적으로 key를 맡기는 ## 아키텍처 -GitDB는 세 층으로 나뉩니다. +GitDB는 네 층으로 나뉩니다. -1. PostgreSQL-compatible facade - - 로컬 TCP endpoint를 엽니다. - - 기존 client가 평범한 PostgreSQL connection string으로 접속합니다. - - ORM별 provider/driver를 만들지 않아도 됩니다. +1. First-party ORM API + - `DataSource`, `Repository`, `save`, `find`, `findOne`, `delete`, + explicit transaction을 제공합니다. + - 새 앱은 PostgreSQL 호환 hop 없이 GitDB runtime에 더 가깝게 붙을 수 + 있습니다. -2. SQL engine +2. PostgreSQL-compatible facade + - 기존 client용 로컬 TCP endpoint를 엽니다. + - Prisma, `pg`, raw SQL client가 평범한 PostgreSQL connection string으로 + 접속합니다. + +3. SQL engine - schema, mutation execution, query execution, join, grouping, result row를 처리합니다. + - local write transaction을 직렬화합니다. - Node.js client와 ORM raw-query 흐름에서 자주 나오는 PostgreSQL subset을 우선 지원합니다. -3. Storage provider +4. Storage provider - 개발/테스트용 local encrypted store - visible snapshot용 local plaintext store - remote durability용 GitHub encrypted/plaintext store @@ -282,6 +324,12 @@ GitDB는 세 층으로 나뉩니다. pnpm benchmark ``` +현재 runtime과 이전 문서화된 run을 비교하고 웹사이트 evidence를 갱신: + +```bash +GITDB_BENCH_ROWS=250 pnpm benchmark:compare +``` + GitHub write 벤치마크: ```bash @@ -292,16 +340,20 @@ GITDB_BENCH_GITHUB_ROWS=2 pnpm benchmark:github | Scenario | Rows | Write ms | Writes/s | Join ms | Reopen ms | | --- | ---: | ---: | ---: | ---: | ---: | -| local plaintext visible snapshots | 250 | 1443.90 | 173.14 | 32.97 | 140.05 | -| local encrypted mutation log | 250 | 761.99 | 328.09 | 4.31 | 322.51 | -| postgres facade over local encrypted | 250 | 987.93 | 253.05 | 19.74 | 0.00 | -| github plaintext contents api | 2 | 14157.49 | 0.14 | 6.70 | 1812.62 | +| local plaintext throttled visible snapshots | 250 | 97.55 | 2562.73 | 3.28 | 47.42 | +| local encrypted mutation log | 250 | 97.23 | 2571.34 | 0.87 | 45.26 | +| postgres facade over local encrypted | 250 | 148.46 | 1683.91 | 2.31 | 0.00 | + +이전 문서화된 run과 비교하면 local plaintext write는 173.14 writes/s에서 +2562.73 writes/s로 개선되어 14.80x입니다. local encrypted write는 328.09 +writes/s에서 2571.34 writes/s로 개선되어 7.84x입니다. -해석은 분명합니다. local execution은 실험/저빈도 workload에는 쓸 만합니다. 하지만 -mutation마다 GitHub Contents API를 직접 호출하는 방식은 hot path가 될 수 -없습니다. 현재 구현에서 live query는 manifest, mutation log, visible snapshot에서 -복원된 in-memory SQL engine에서 실행됩니다. WAL, index, batched Git commit은 현재 -보장사항이 아니라 다음 storage-engine 작업입니다. +해석은 분명합니다. local execution은 example, demo, 저빈도 workload에 쓸 수 +있는 수준입니다. single-statement mutation은 더 이상 매 write마다 전체 in-memory +database를 clone하지 않고, persistence 실패 시 committed manifest 상태로 +복구합니다. 하지만 mutation마다 GitHub Contents API를 직접 호출하는 방식은 hot +path가 될 수 없습니다. WAL, index, compaction, batched Git commit은 현재 완성된 +OLTP 보장이 아니라 다음 storage-engine 작업입니다. 자세한 내용은 [BENCHMARKS.md](BENCHMARKS.md)를 참고하세요. @@ -350,6 +402,9 @@ pnpm check pnpm test pnpm build pnpm benchmark +pnpm benchmark:evaluate +pnpm pack:dry-run +pnpm publish:dry-run pnpm start:facade pnpm example ``` diff --git a/package.json b/package.json index a7cce31..28e7972 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,38 @@ { "name": "@3xhaust/gitdb", "version": "0.1.0", - "description": "GitHub-native encrypted database with a PostgreSQL-compatible facade for existing ORMs.", + "description": "GitHub-backed local database runtime with auditable storage, transactions, and ORM APIs.", "type": "module", + "main": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", + "homepage": "https://3x-haust.github.io/gitdb/", + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, "bin": { - "gitdb": "./dist/src/cli/main.js" + "gitdb": "dist/src/cli/main.js" }, "files": [ - "dist", + "dist/src", "README.md", "docs" ], "scripts": { "build": "tsc -p tsconfig.json", "benchmark": "pnpm build && node scripts/benchmark.mjs", + "benchmark:compare": "GITDB_BENCH_OUTPUT=.gitdb/bench-current.json pnpm benchmark:evaluate > /dev/null && node scripts/benchmark-compare.mjs --current .gitdb/bench-current.json --baseline-path docs/BENCHMARKS.md --output site/benchmark.json --markdown site/benchmark.md", + "benchmark:evaluate": "pnpm build && node scripts/benchmark.mjs --json", "benchmark:github": "pnpm build && node scripts/benchmark.mjs --github", "check": "biome check . && tsc --noEmit", "example": "pnpm example:express-prisma", "example:express-prisma": "pnpm build && pnpm exec prisma generate --schema examples/express-prisma/schema.prisma && node examples/express-prisma/server.mjs", "format": "biome check --write .", + "pack:dry-run": "pnpm build && COREPACK_ENABLE_STRICT=0 npm pack --dry-run --json", + "publish:dry-run": "pnpm build && COREPACK_ENABLE_STRICT=0 npm publish --dry-run --access public", + "site:preview": "python3 -m http.server 4173 --directory site", "test": "vitest run", "test:e2e": "vitest run tests/e2e.test.ts", "start": "node dist/src/http/main.js", @@ -58,8 +72,12 @@ "node": ">=20.19.0" }, "license": "MIT", + "publishConfig": { + "access": "public", + "provenance": true + }, "repository": { "type": "git", - "url": "https://github.com/3x-haust/gitdb.git" + "url": "git+https://github.com/3x-haust/gitdb.git" } } diff --git a/scripts/benchmark-compare.mjs b/scripts/benchmark-compare.mjs new file mode 100644 index 0000000..122b2eb --- /dev/null +++ b/scripts/benchmark-compare.mjs @@ -0,0 +1,105 @@ +import { execFile } from "node:child_process" +import { mkdir, readFile, writeFile } from "node:fs/promises" +import { dirname } from "node:path" +import { promisify } from "node:util" +import { + buildBenchmarkEvidence, + formatComparisonMarkdown, + parseBenchmarkOutput, +} from "./benchmark-report.mjs" + +const execFileAsync = promisify(execFile) +const options = parseArgs(process.argv.slice(2)) +const currentPath = requireValue(options.current, "--current") +const baselineSource = await loadBaselineSource(options) +const currentText = await readFile(currentPath, "utf8") +const baselineText = baselineSource.text +const evidence = buildBenchmarkEvidence({ + baseline: parseBenchmarkOutput(baselineText), + baselineLabel: options.baselineLabel ?? "previous documented run", + baselineSource: baselineSource.source, + current: parseBenchmarkOutput(currentText), + currentLabel: options.currentLabel ?? "current working tree", + currentSource: currentPath, +}) +const markdown = formatComparisonMarkdown(evidence) + +if (options.output !== undefined) { + await writeJson(options.output, evidence) +} + +if (options.markdown !== undefined) { + await writeText(options.markdown, markdown) +} + +process.stdout.write(markdown) + +function parseArgs(argv) { + const parsed = {} + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index] + switch (arg) { + case "--baseline": + case "--baseline-label": + case "--baseline-path": + case "--baseline-ref": + case "--current": + case "--current-label": + case "--markdown": + case "--output": + parsed[toCamelCase(arg.slice(2))] = readArgValue(argv, index, arg) + index += 1 + break + default: + throw new Error(`unknown argument: ${arg}`) + } + } + return parsed +} + +function readArgValue(argv, index, name) { + const value = argv[index + 1] + if (value === undefined || value.startsWith("--")) { + throw new Error(`${name} requires a value`) + } + return value +} + +function toCamelCase(value) { + return value.replaceAll(/-([a-z])/g, (_, letter) => letter.toUpperCase()) +} + +async function loadBaselineSource(parsed) { + if (parsed.baseline !== undefined) { + return { + source: parsed.baseline, + text: await readFile(parsed.baseline, "utf8"), + } + } + + const baselinePath = parsed.baselinePath ?? "docs/BENCHMARKS.md" + const baselineRef = parsed.baselineRef ?? process.env.GITDB_BENCH_BASE_REF ?? "HEAD~1" + const { stdout } = await execFileAsync("git", ["show", `${baselineRef}:${baselinePath}`], { + maxBuffer: 1024 * 1024, + }) + return { + source: `${baselineRef}:${baselinePath}`, + text: stdout, + } +} + +async function writeJson(path, value) { + await writeText(path, `${JSON.stringify(value, null, 2)}\n`) +} + +async function writeText(path, value) { + await mkdir(dirname(path), { recursive: true }) + await writeFile(path, value, "utf8") +} + +function requireValue(value, name) { + if (value === undefined || value.trim().length === 0) { + throw new Error(`${name} is required`) + } + return value +} diff --git a/scripts/benchmark-report.mjs b/scripts/benchmark-report.mjs new file mode 100644 index 0000000..992919b --- /dev/null +++ b/scripts/benchmark-report.mjs @@ -0,0 +1,219 @@ +export function parseBenchmarkOutput(text) { + const trimmed = text.trim() + if (trimmed.startsWith("[") || trimmed.startsWith("{")) { + return parseJsonResults(trimmed) + } + return parseMarkdownResults(text) +} + +export function buildBenchmarkEvidence({ + baseline, + baselineLabel, + baselineSource, + current, + currentLabel, + currentSource, +}) { + const baselineByKey = new Map() + for (const row of baseline) { + baselineByKey.set(scenarioKey(row.label), row) + } + + const comparisons = [] + for (const currentRow of current) { + const key = scenarioKey(currentRow.label) + const baselineRow = baselineByKey.get(key) + if (baselineRow !== undefined) { + comparisons.push(compareRows(key, baselineRow, currentRow)) + } + } + + if (comparisons.length === 0) { + throw new Error("no comparable benchmark rows found") + } + + return { + baseline: { + label: baselineLabel, + source: baselineSource, + }, + comparisons, + current: { + label: currentLabel, + source: currentSource, + }, + generatedAt: new Date().toISOString(), + headline: headline(comparisons), + } +} + +export function formatComparisonMarkdown(evidence) { + const lines = [ + "| Scenario | Previous writes/s | Current writes/s | Change | Write ms change | Join ms change |", + "| --- | ---: | ---: | ---: | ---: | ---: |", + ] + for (const item of evidence.comparisons) { + lines.push( + `| ${item.scenario} | ${fixed(item.baseline.writesPerSecond)} | ${fixed(item.current.writesPerSecond)} | ${formatPct(item.writesPerSecondChangePct)} | ${formatPct(item.writeMsChangePct)} | ${formatPct(item.joinMsChangePct)} |`, + ) + } + return `${lines.join("\n")}\n` +} + +function parseJsonResults(text) { + const parsed = JSON.parse(text) + const rows = Array.isArray(parsed) ? parsed : parsed.results + if (!Array.isArray(rows)) { + throw new Error("benchmark JSON must be an array or contain a results array") + } + return rows.map((row) => normalizeResult(row)) +} + +function parseMarkdownResults(text) { + const results = [] + for (const line of text.split("\n")) { + if (!line.trim().startsWith("|")) { + continue + } + const cells = line + .split("|") + .slice(1, -1) + .map((cell) => cell.replaceAll("`", "").trim()) + if (cells.length < 6 || cells[0] === "Scenario" || cells[0].startsWith("---")) { + continue + } + const row = normalizeMarkdownRow(cells) + if (row !== undefined) { + results.push(row) + } + } + if (results.length === 0) { + throw new Error("no benchmark rows found") + } + return results +} + +function normalizeMarkdownRow(cells) { + const rows = parseFiniteNumber(cells[1]) + const writeMs = parseFiniteNumber(cells[2]) + const writesPerSecond = parseFiniteNumber(cells[3]) + const joinMs = parseFiniteNumber(cells[4]) + const reopenMs = parseFiniteNumber(cells[5]) + if ( + rows === undefined || + writeMs === undefined || + writesPerSecond === undefined || + joinMs === undefined || + reopenMs === undefined + ) { + return undefined + } + return { + joinMs, + label: cells[0], + reopenMs, + rows, + writeMs, + writesPerSecond, + } +} + +function normalizeResult(row) { + return { + joinMs: finiteNumber(row.joinMs, "joinMs"), + label: String(row.label), + reopenMs: finiteNumber(row.reopenMs, "reopenMs"), + rows: finiteNumber(row.rows, "rows"), + writeMs: finiteNumber(row.writeMs, "writeMs"), + writesPerSecond: finiteNumber(row.writesPerSecond, "writesPerSecond"), + } +} + +function compareRows(key, baselineRow, currentRow) { + return { + baseline: baselineRow, + current: currentRow, + joinMsChangePct: latencyChangePct(baselineRow.joinMs, currentRow.joinMs), + key, + reopenMsChangePct: latencyChangePct(baselineRow.reopenMs, currentRow.reopenMs), + rows: currentRow.rows, + scenario: currentRow.label, + writeMsChangePct: latencyChangePct(baselineRow.writeMs, currentRow.writeMs), + writeSpeedup: ratio(currentRow.writesPerSecond, baselineRow.writesPerSecond), + writesPerSecondChangePct: throughputChangePct( + baselineRow.writesPerSecond, + currentRow.writesPerSecond, + ), + } +} + +function scenarioKey(label) { + const normalized = label.toLowerCase() + if (normalized.startsWith("local plaintext")) { + return "local plaintext" + } + if (normalized.startsWith("local encrypted")) { + return "local encrypted" + } + if (normalized.startsWith("postgres facade")) { + return "postgres facade" + } + if (normalized.startsWith("github plaintext")) { + return "github plaintext" + } + return normalized +} + +function headline(comparisons) { + const localPlaintext = comparisons.find((row) => row.key === "local plaintext") + if (localPlaintext === undefined) { + return "Benchmark comparison generated" + } + return `Local plaintext writes are ${formatRatio(localPlaintext.writeSpeedup)} of the previous documented run` +} + +function latencyChangePct(before, after) { + if (before <= 0) { + return null + } + return ((before - after) / before) * 100 +} + +function throughputChangePct(before, after) { + if (before <= 0) { + return null + } + return ((after - before) / before) * 100 +} + +function ratio(after, before) { + if (before <= 0) { + return null + } + return after / before +} + +function formatPct(value) { + return value === null ? "n/a" : `${value >= 0 ? "+" : ""}${fixed(value)}%` +} + +function formatRatio(value) { + return value === null ? "n/a" : `${fixed(value)}x` +} + +function fixed(value) { + return value.toFixed(2) +} + +function parseFiniteNumber(value) { + const parsed = Number.parseFloat(value.replaceAll(",", "")) + return Number.isFinite(parsed) ? parsed : undefined +} + +function finiteNumber(value, field) { + const parsed = typeof value === "number" ? value : Number.parseFloat(String(value)) + if (!Number.isFinite(parsed)) { + throw new Error(`${field} must be a finite number`) + } + return parsed +} diff --git a/scripts/benchmark.mjs b/scripts/benchmark.mjs index c3c958f..0f7f6c7 100644 --- a/scripts/benchmark.mjs +++ b/scripts/benchmark.mjs @@ -1,6 +1,6 @@ -import { mkdtemp, rm } from "node:fs/promises" +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises" import { tmpdir } from "node:os" -import { join } from "node:path" +import { dirname, join } from "node:path" import { performance } from "node:perf_hooks" import { Client } from "pg" import { createAesGcmCipher } from "../dist/src/crypto/aes-gcm.js" @@ -17,8 +17,9 @@ const results = [] results.push( await benchEngine({ - label: "local plaintext visible snapshots", + label: "local plaintext throttled visible snapshots", rows, + snapshotPolicy: { mode: "interval", mutations: 100 }, store: (root) => new LocalPlaintextStore({ root }), }), ) @@ -39,12 +40,21 @@ if (args.has("--github")) { results.push(await benchGitHubPlaintext(githubRows)) } -process.stdout.write(formatMarkdown(results)) +if (process.env.GITDB_BENCH_OUTPUT !== undefined) { + await writeJson(process.env.GITDB_BENCH_OUTPUT, results) +} + +process.stdout.write( + args.has("--json") ? `${JSON.stringify(results, null, 2)}\n` : formatMarkdown(results), +) async function benchEngine(options) { const root = await mkdtemp(join(tmpdir(), "gitdb-bench-")) try { - const engine = await GitDbEngine.open({ store: options.store(root) }) + const engine = await GitDbEngine.open({ + snapshotPolicy: options.snapshotPolicy, + store: options.store(root), + }) await createTables(engine) const writeMs = await time(async () => { await insertRows(engine, options.rows) @@ -53,7 +63,10 @@ async function benchEngine(options) { await assertJoin(engine, options.rows) }) const reopenMs = await time(async () => { - const reopened = await GitDbEngine.open({ store: options.store(root) }) + const reopened = await GitDbEngine.open({ + snapshotPolicy: options.snapshotPolicy, + store: options.store(root), + }) await assertJoin(reopened, options.rows) }) return result(options.label, options.rows, writeMs, joinMs, reopenMs) @@ -175,6 +188,11 @@ function formatMarkdown(items) { return `${lines.join("\n")}\n` } +async function writeJson(path, value) { + await mkdir(dirname(path), { recursive: true }) + await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8") +} + function fixed(value) { return value.toFixed(2) } diff --git a/site/app.js b/site/app.js new file mode 100644 index 0000000..fed5ba8 --- /dev/null +++ b/site/app.js @@ -0,0 +1,151 @@ +const numberFormatter = new Intl.NumberFormat("en-US", { + maximumFractionDigits: 2, + minimumFractionDigits: 0, +}) + +const percentFormatter = new Intl.NumberFormat("en-US", { + maximumFractionDigits: 1, + minimumFractionDigits: 1, + signDisplay: "always", +}) + +const fallbackData = { + comparisons: [], + headline: "Run pnpm benchmark:compare to refresh benchmark evidence", +} + +render(await loadBenchmark()) + +async function loadBenchmark() { + try { + const response = await fetch("./benchmark.json", { cache: "no-store" }) + if (!response.ok) { + return fallbackData + } + return await response.json() + } catch { + return fallbackData + } +} + +function render(data) { + const local = data.comparisons.find((item) => item.key === "local plaintext") + setText("hero-speedup", formatRatio(local?.writeSpeedup)) + setText("hero-current-wps", formatNumber(local?.current.writesPerSecond)) + setText("hero-baseline-wps", formatNumber(local?.baseline.writesPerSecond)) + renderRows(data.comparisons) + renderBars(data.comparisons) +} + +function renderRows(comparisons) { + const target = document.getElementById("benchmark-rows") + if (target === null) { + return + } + target.replaceChildren() + if (comparisons.length === 0) { + const row = document.createElement("tr") + row.append(tableCell("Benchmark evidence pending", "left")) + row.append(tableCell("n/a")) + row.append(tableCell("n/a")) + row.append(tableCell("n/a")) + target.append(row) + return + } + for (const item of comparisons) { + const row = document.createElement("tr") + row.append(tableCell(item.scenario, "left")) + row.append(tableCell(formatNumber(item.baseline.writesPerSecond))) + row.append(tableCell(formatNumber(item.current.writesPerSecond))) + row.append(tableCell(formatPercent(item.writesPerSecondChangePct))) + target.append(row) + } +} + +function renderBars(comparisons) { + const target = document.getElementById("chart-bars") + if (target === null) { + return + } + target.replaceChildren() + const max = maxThroughput(comparisons) + if (max === 0) { + target.append(emptyState()) + return + } + for (const item of comparisons) { + target.append(barRow(item, max)) + } +} + +function barRow(item, max) { + const row = document.createElement("div") + row.className = "bar-row" + + const label = document.createElement("div") + label.className = "bar-label" + label.append(span(item.scenario)) + label.append(span(formatPercent(item.writesPerSecondChangePct))) + + const bars = document.createElement("div") + bars.className = "bars" + bars.append(bar("previous", item.baseline.writesPerSecond, max)) + bars.append(bar("current", item.current.writesPerSecond, max)) + + row.append(label) + row.append(bars) + return row +} + +function bar(kind, value, max) { + const element = document.createElement("div") + element.className = `bar ${kind}` + element.style.width = `${Math.max(4, (value / max) * 100)}%` + return element +} + +function tableCell(value, align = "right") { + const cell = document.createElement("td") + cell.textContent = value + if (align === "left") { + cell.style.textAlign = "left" + } + return cell +} + +function span(value) { + const element = document.createElement("span") + element.textContent = value + return element +} + +function emptyState() { + const element = document.createElement("p") + element.textContent = "Benchmark evidence pending." + return element +} + +function maxThroughput(comparisons) { + return comparisons.reduce((max, item) => { + return Math.max(max, item.baseline.writesPerSecond, item.current.writesPerSecond) + }, 0) +} + +function setText(id, value) { + const element = document.getElementById(id) + if (element !== null) { + element.textContent = value + } +} + +function formatNumber(value) { + return typeof value === "number" ? numberFormatter.format(value) : "n/a" +} + +function formatPercent(value) { + return typeof value === "number" ? `${percentFormatter.format(value)}%` : "n/a" +} + +function formatRatio(value) { + return typeof value === "number" ? `${numberFormatter.format(value)}x` : "n/a" +} diff --git a/site/assets/runtime-map.svg b/site/assets/runtime-map.svg new file mode 100644 index 0000000..ff803f6 --- /dev/null +++ b/site/assets/runtime-map.svg @@ -0,0 +1,56 @@ + + GitDB runtime architecture map + Application code talks to a local GitDB runtime, which writes transaction logs and visible snapshots before syncing durable state to a GitHub repository. + + + GitDB Runtime + local hot path with auditable GitHub storage + + + + App code + ORM repository + Postgres facade + + + + + + + + Local engine + serialized transactions + query execution + snapshot checkpoints + + + + + + + + WAL + visible snapshot + mutation log, manifest, table snapshots + rollback-aware persistence boundary + + + + + + + + GitHub + remote + durable + auditable + + + + + Why faster + + + + less snapshot rewrite work + + diff --git a/site/benchmark.json b/site/benchmark.json new file mode 100644 index 0000000..cc68a96 --- /dev/null +++ b/site/benchmark.json @@ -0,0 +1,92 @@ +{ + "baseline": { + "label": "previous documented run", + "source": "HEAD~1:docs/BENCHMARKS.md" + }, + "comparisons": [ + { + "baseline": { + "joinMs": 32.97, + "label": "local plaintext visible snapshots", + "reopenMs": 140.05, + "rows": 250, + "writeMs": 1443.9, + "writesPerSecond": 173.14 + }, + "current": { + "joinMs": 3.2837920000000054, + "label": "local plaintext throttled visible snapshots", + "reopenMs": 47.42029099999999, + "rows": 250, + "writeMs": 97.552042, + "writesPerSecond": 2562.7346683322116 + }, + "joinMsChangePct": 90.04006066120715, + "key": "local plaintext", + "reopenMsChangePct": 66.14045626561943, + "rows": 250, + "scenario": "local plaintext throttled visible snapshots", + "writeMsChangePct": 93.24385054366647, + "writeSpeedup": 14.801517086359084, + "writesPerSecondChangePct": 1380.1517086359086 + }, + { + "baseline": { + "joinMs": 4.31, + "label": "local encrypted mutation log", + "reopenMs": 322.51, + "rows": 250, + "writeMs": 761.99, + "writesPerSecond": 328.09 + }, + "current": { + "joinMs": 0.865791999999999, + "label": "local encrypted mutation log", + "reopenMs": 45.26150000000001, + "rows": 250, + "writeMs": 97.225708, + "writesPerSecond": 2571.3363794686893 + }, + "joinMsChangePct": 79.91201856148494, + "key": "local encrypted", + "reopenMsChangePct": 85.96586152367368, + "rows": 250, + "scenario": "local encrypted mutation log", + "writeMsChangePct": 87.2405532881009, + "writeSpeedup": 7.837289705473161, + "writesPerSecondChangePct": 683.7289705473161 + }, + { + "baseline": { + "joinMs": 19.74, + "label": "postgres facade over local encrypted", + "reopenMs": 0, + "rows": 250, + "writeMs": 987.93, + "writesPerSecond": 253.05 + }, + "current": { + "joinMs": 2.3100420000000668, + "label": "postgres facade over local encrypted", + "reopenMs": 0, + "rows": 250, + "writeMs": 148.463792, + "writesPerSecond": 1683.9122632675312 + }, + "joinMsChangePct": 88.29765957446774, + "key": "postgres facade", + "reopenMsChangePct": null, + "rows": 250, + "scenario": "postgres facade over local encrypted", + "writeMsChangePct": 84.9722356847145, + "writeSpeedup": 6.65446458513152, + "writesPerSecondChangePct": 565.446458513152 + } + ], + "current": { + "label": "current working tree", + "source": ".gitdb/bench-current.json" + }, + "generatedAt": "2026-06-13T16:15:58.844Z", + "headline": "Local plaintext writes are 14.80x of the previous documented run" +} diff --git a/site/benchmark.md b/site/benchmark.md new file mode 100644 index 0000000..de8f37c --- /dev/null +++ b/site/benchmark.md @@ -0,0 +1,5 @@ +| Scenario | Previous writes/s | Current writes/s | Change | Write ms change | Join ms change | +| --- | ---: | ---: | ---: | ---: | ---: | +| local plaintext throttled visible snapshots | 173.14 | 2562.73 | +1380.15% | +93.24% | +90.04% | +| local encrypted mutation log | 328.09 | 2571.34 | +683.73% | +87.24% | +79.91% | +| postgres facade over local encrypted | 253.05 | 1683.91 | +565.45% | +84.97% | +88.30% | diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..ea9a586 --- /dev/null +++ b/site/index.html @@ -0,0 +1,199 @@ + + + + + + GitDB - GitHub-backed database runtime + + + + +
+ + G + GitDB + + +
+ +
+
+
+

GitHub Repo + DB Runtime

+

GitDB

+

+ A local SQL engine and TypeORM-style repository API over durable, + auditable GitHub storage. GitHub is the storage and audit trail; the + hot path stays local. +

+ +
+
+
Local write speed
+
loading
+
+
+
Current writes/s
+
loading
+
+
+
Previous writes/s
+
loading
+
+
+
+
+ GitDB runtime architecture map +
+ Local execution with manifest-gated persistence and visible snapshots. +
+
+
+ +
+
+

What it is

+

A database runtime that treats a GitHub repo as durable storage

+

+ GitDB is not a database file uploaded to GitHub. It keeps query + execution local, writes mutation logs and manifests, and uses the + repository as a durable, inspectable audit trail. +

+
+
+
+

Use GitDB when

+
    +
  • project data should live in a dedicated repository
  • +
  • agents, demos, content tools, or config tools need audit history
  • +
  • new apps want a repository API without a separate ORM provider
  • +
  • existing clients need a local PostgreSQL-compatible endpoint
  • +
+
+
+

Do not use GitDB when

+
    +
  • the workload needs high-throughput OLTP today
  • +
  • many writers need low-latency distributed transactions
  • +
  • full PostgreSQL compatibility is a hard requirement
  • +
  • GitHub Contents API writes would sit on the hot path
  • +
+
+
+

Repository shape

+
gitdb/v1/
+  manifest.json
+  people/schema.json
+  people/data.json
+  teams/schema.json
+  teams/data.json
+  log/00000000000000000001.json
+
+
+
+ +
+
+

Measured example

+

Current runtime versus previous documented run

+

+ The benchmark creates `teams` and `people`, inserts rows, runs a join, + and reopens storage where applicable. +

+
+
+
+
+ previous + current +
+
+
+
+ + + + + + + + + + +
ScenarioPrevious writes/sCurrent writes/sChange
+
+
+
+ +
+
+

Storage engine first

+

Not a SQL facade over a file

+
+
+
+

Transaction queue

+

Local mutations are serialized before persistence so concurrent writes land in order.

+
+
+

Manifest gates

+

Commits advance through manifest state, visible snapshots, and rollback-aware logs.

+
+
+

Remote audit trail

+

GitHub stores durable state and reviewable history instead of serving every SELECT.

+
+
+
+ +
+
+

First-party ORM

+

Repository API for new apps, PostgreSQL facade for existing clients

+
+
const Person = defineEntity({
+  tableName: "people",
+  primaryKey: "id",
+  columns: { id: "STRING", name: "STRING", team_id: "STRING" },
+})
+
+const dataSource = await createGitDbDataSource({
+  entities: [Person],
+  store,
+  synchronize: true,
+})
+
+await dataSource.getRepository(Person).save({
+  id: "p1",
+  name: "Ada",
+  team_id: "runtime",
+})
+
+
+ + + + + + diff --git a/site/styles.css b/site/styles.css new file mode 100644 index 0000000..d4148ef --- /dev/null +++ b/site/styles.css @@ -0,0 +1,532 @@ +:root { + color: #171716; + background: #f5f1e8; + font-family: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-synthesis: none; + letter-spacing: 0; + text-rendering: optimizeLegibility; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + background: #f5f1e8; + color: #171716; +} + +a { + color: inherit; + text-decoration: none; +} + +.topbar { + position: sticky; + top: 0; + z-index: 10; + display: flex; + align-items: center; + justify-content: space-between; + min-height: 68px; + padding: 14px 28px; + border-bottom: 1px solid #d8d0c0; + background: rgba(245, 241, 232, 0.94); + backdrop-filter: blur(14px); +} + +.brand, +.nav, +.actions, +.metric-strip, +.benchmark-layout, +.runtime-grid, +footer { + display: flex; + align-items: center; +} + +.brand { + gap: 10px; + font-size: 18px; + font-weight: 760; +} + +.brand-mark { + display: grid; + width: 34px; + height: 34px; + place-items: center; + border: 1px solid #171716; + border-radius: 8px; + background: #cf4d34; + color: #fff9ee; +} + +.nav { + gap: 22px; + color: #4a463f; + font-size: 14px; +} + +footer a:hover, +.nav a:hover { + color: #b13d2b; +} + +main { + overflow: hidden; +} + +.hero { + display: grid; + grid-template-columns: minmax(0, 0.95fr) minmax(420px, 1.05fr); + gap: 42px; + max-width: 1220px; + min-height: calc(100vh - 68px); + margin: 0 auto; + padding: 64px 28px 40px; +} + +.hero-copy { + align-self: center; +} + +.eyebrow { + margin: 0 0 14px; + color: #2d6b5f; + font-size: 13px; + font-weight: 780; + text-transform: uppercase; +} + +h1, +h2, +h3, +p { + letter-spacing: 0; +} + +h1 { + margin: 0; + font-size: 76px; + line-height: 0.94; +} + +h2 { + max-width: 780px; + margin: 0; + font-size: 34px; + line-height: 1.12; +} + +h3 { + margin: 0 0 10px; + font-size: 18px; +} + +p { + color: #4d4840; + line-height: 1.65; +} + +.lede { + max-width: 650px; + margin: 22px 0 0; + font-size: 19px; +} + +.actions { + flex-wrap: wrap; + gap: 12px; + margin-top: 30px; +} + +.button { + min-height: 46px; + padding: 12px 18px; + border: 1px solid #171716; + border-radius: 8px; + font-weight: 760; +} + +.button.primary { + background: #171716; + color: #fff9ee; +} + +.button.secondary { + background: #fdf8ed; +} + +.metric-strip { + align-items: stretch; + gap: 0; + max-width: 680px; + margin: 34px 0 0; + padding: 0; + border: 1px solid #d5c8b4; + border-radius: 8px; + background: #fdf8ed; +} + +.metric-strip div { + flex: 1; + min-width: 0; + padding: 18px; + border-right: 1px solid #d5c8b4; +} + +.metric-strip div:last-child { + border-right: 0; +} + +dt { + color: #6f675b; + font-size: 12px; + font-weight: 700; + text-transform: uppercase; +} + +dd { + margin: 8px 0 0; + font-size: 24px; + font-weight: 820; +} + +.hero-visual { + align-self: center; + margin: 0; + border: 1px solid #1f1f1d; + border-radius: 8px; + background: #fdf8ed; + box-shadow: 14px 14px 0 #2d6b5f; +} + +.hero-visual img { + display: block; + width: 100%; + height: auto; + min-height: 420px; + object-fit: cover; +} + +figcaption { + padding: 14px 18px 18px; + border-top: 1px solid #d5c8b4; + color: #5a5348; + font-size: 14px; +} + +.explain, +.benchmark-band, +.runtime, +.orm { + padding: 64px 28px; +} + +.explain { + border-top: 1px solid #d8d0c0; + background: #fdf8ed; +} + +.explain-layout { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 18px; + max-width: 1220px; + margin: 0 auto; +} + +.definition, +.repo-layout { + border: 1px solid #cbbda6; + border-radius: 8px; + background: #f5f1e8; +} + +.definition { + padding: 24px; +} + +.definition ul { + display: grid; + gap: 12px; + margin: 16px 0 0; + padding-left: 20px; + color: #4d4840; + line-height: 1.55; +} + +.repo-layout { + grid-column: 1 / -1; + overflow: hidden; +} + +.repo-layout h3 { + padding: 22px 24px 0; +} + +.repo-layout pre { + overflow-x: auto; + margin: 0; + padding: 18px 24px 24px; + color: #2f2b25; + font-size: 15px; + line-height: 1.65; +} + +.benchmark-band { + border-top: 1px solid #d8d0c0; + border-bottom: 1px solid #d8d0c0; + background: #ebe3d3; +} + +.section-heading { + max-width: 1220px; + margin: 0 auto 28px; +} + +.section-heading p:last-child { + max-width: 740px; + margin-bottom: 0; +} + +.benchmark-layout { + align-items: stretch; + gap: 22px; + max-width: 1220px; + margin: 0 auto; +} + +.chart-panel, +.table-wrap, +.runtime-grid article, +.orm pre { + border: 1px solid #cbbda6; + border-radius: 8px; + background: #fdf8ed; +} + +.chart-panel { + flex: 0 0 360px; + min-height: 320px; + padding: 22px; +} + +.chart-axis { + display: flex; + justify-content: space-between; + color: #6f675b; + font-size: 12px; + font-weight: 700; + text-transform: uppercase; +} + +.chart-bars { + display: grid; + gap: 18px; + margin-top: 24px; +} + +.bar-row { + display: grid; + gap: 8px; +} + +.bar-label { + display: flex; + justify-content: space-between; + gap: 12px; + font-size: 13px; + font-weight: 760; +} + +.bars { + display: grid; + gap: 6px; +} + +.bar { + min-width: 4px; + height: 16px; + border-radius: 4px; +} + +.bar.previous { + background: #8d8171; +} + +.bar.current { + background: #cf4d34; +} + +.table-wrap { + flex: 1; + overflow-x: auto; +} + +table { + width: 100%; + min-width: 680px; + border-collapse: collapse; +} + +th, +td { + padding: 17px 18px; + border-bottom: 1px solid #d5c8b4; + text-align: right; + white-space: nowrap; +} + +th:first-child, +td:first-child { + max-width: 340px; + text-align: left; + white-space: normal; +} + +th { + color: #6f675b; + font-size: 12px; + text-transform: uppercase; +} + +tbody tr:last-child td { + border-bottom: 0; +} + +.runtime-grid { + align-items: stretch; + gap: 18px; + max-width: 1220px; + margin: 0 auto; +} + +.runtime-grid article { + flex: 1; + padding: 24px; +} + +.orm { + max-width: 1220px; + margin: 0 auto; +} + +.orm pre { + overflow-x: auto; + margin: 0; + padding: 24px; + background: #171716; + color: #fff9ee; + font-size: 14px; + line-height: 1.6; +} + +footer { + justify-content: center; + gap: 22px; + min-height: 88px; + border-top: 1px solid #d8d0c0; + color: #575045; + font-size: 14px; +} + +footer span { + color: #171716; + font-weight: 800; +} + +@media (max-width: 900px) { + .topbar { + align-items: flex-start; + flex-direction: column; + gap: 14px; + } + + .nav { + flex-wrap: wrap; + gap: 14px; + } + + .hero { + grid-template-columns: 1fr; + min-height: auto; + padding-top: 42px; + } + + .explain-layout { + grid-template-columns: 1fr; + } + + h1 { + font-size: 48px; + } + + h2 { + font-size: 28px; + } + + .lede { + font-size: 17px; + } + + .metric-strip, + .benchmark-layout, + .runtime-grid { + flex-direction: column; + } + + .metric-strip div { + border-right: 0; + border-bottom: 1px solid #d5c8b4; + } + + .metric-strip div:last-child { + border-bottom: 0; + } + + .hero-visual { + box-shadow: 8px 8px 0 #2d6b5f; + } + + .hero-visual img { + min-height: 300px; + } + + .chart-panel { + flex-basis: auto; + } +} + +@media (max-width: 560px) { + .topbar, + .hero, + .explain, + .benchmark-band, + .runtime, + .orm { + padding-right: 18px; + padding-left: 18px; + } + + .button { + width: 100%; + text-align: center; + } + + .actions { + width: 100%; + } + + h1 { + font-size: 42px; + } + + dd { + font-size: 21px; + } +} diff --git a/src/errors.ts b/src/errors.ts index 804a806..44dd827 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -32,3 +32,7 @@ export class UnsupportedSqlError extends Error { export class ProtocolError extends Error { readonly name = "ProtocolError" } + +export class GitDbOrmError extends Error { + readonly name = "GitDbOrmError" +} diff --git a/src/github/github-plaintext-store.ts b/src/github/github-plaintext-store.ts index 29da3f4..1663b45 100644 --- a/src/github/github-plaintext-store.ts +++ b/src/github/github-plaintext-store.ts @@ -84,6 +84,7 @@ export class GitHubPlaintextStore implements GitDbStore { } async readVisibleSnapshot(): Promise { + const checkpoint = await this.#readSnapshotCheckpoint() const tableNames = await this.#readTableNames() const tables = [] for (const tableName of tableNames) { @@ -99,7 +100,10 @@ export class GitHubPlaintextStore implements GitDbStore { }) } } - return tables.length === 0 ? null : { tables } + if (tables.length === 0) { + return null + } + return checkpoint === undefined ? { tables } : { sequence: checkpoint, tables } } async writeVisibleSnapshot(snapshot: VisibleDatabaseSnapshot): Promise { @@ -118,6 +122,13 @@ export class GitHubPlaintextStore implements GitDbStore { plaintext: table.rows, }) } + if (snapshot.sequence !== undefined) { + await this.#writeFile({ + message: "gitdb sync plaintext snapshot checkpoint", + path: `${this.#config.prefix}/snapshot.json`, + plaintext: { sequence: snapshot.sequence }, + }) + } } async #readTableNames(): Promise { @@ -166,6 +177,15 @@ export class GitHubPlaintextStore implements GitDbStore { } } + async #readSnapshotCheckpoint(): Promise { + const payload = await this.#readNullable(`${this.#config.prefix}/snapshot.json`) + if (payload === null) { + return undefined + } + const parsed = JSON.parse(payload) as { readonly sequence?: unknown } + return typeof parsed.sequence === "number" ? parsed.sequence : undefined + } + async #writeFile(input: WriteFileInput): Promise { let repositoryBootstrapped = false for (let attempt = 0; attempt < 5; attempt += 1) { diff --git a/src/index.ts b/src/index.ts index 2be50b5..aef9eb0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,19 @@ export { generateKey } from "./cli/keygen.js" export { createAesGcmCipher } from "./crypto/aes-gcm.js" export { GitHubEncryptedStore } from "./github/github-encrypted-store.js" +export { + createGitDbDataSource, + defineEntity, + type EntityDefinition, + type FindOptions, + type GitDbColumnType, + GitDbDataSource, + type GitDbDataSourceOptions, + GitDbRepository, +} from "./orm/index.js" export { createGitDbServer } from "./protocol/postgres-server.js" -export { GitDbEngine } from "./sql/engine.js" +export { GitDbEngine, type GitDbTransaction } from "./sql/engine.js" export { LocalEncryptedStore } from "./storage/local-encrypted-store.js" +export { LocalPlaintextStore } from "./storage/local-plaintext-store.js" export type { GitDbStore } from "./storage/store.js" export type { GitDbManifest, PersistedMutation, SqlResult, SqlRow } from "./types.js" diff --git a/src/orm/index.ts b/src/orm/index.ts new file mode 100644 index 0000000..86018fc --- /dev/null +++ b/src/orm/index.ts @@ -0,0 +1,188 @@ +import { z } from "zod" +import { GitDbOrmError } from "../errors.js" +import { GitDbEngine, type GitDbTransaction } from "../sql/engine.js" +import { sqlLiteral } from "../sql/rows.js" +import type { GitDbStore } from "../storage/store.js" +import type { JsonPrimitive, SqlRow } from "../types.js" + +const GitDbColumnTypes = ["STRING", "INT", "FLOAT", "BOOL"] as const + +export type GitDbColumnType = (typeof GitDbColumnTypes)[number] + +export type EntityDefinition = { + readonly columns: { readonly [Key in Extract]: GitDbColumnType } + readonly primaryKey: Extract + readonly tableName: string +} + +export type FindOptions = { + readonly where?: Partial, JsonPrimitive>> +} + +export type GitDbDataSourceOptions = { + readonly entities: readonly EntityDefinition[] + readonly store: GitDbStore + readonly synchronize?: boolean +} + +const EntityDefinitionSchema = z + .object({ + columns: z.record(z.string().min(1), z.enum(GitDbColumnTypes)), + primaryKey: z.string().min(1), + tableName: z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), + }) + .refine((definition) => definition.primaryKey in definition.columns, { + message: "primaryKey must exist in columns", + path: ["primaryKey"], + }) + +export function defineEntity( + definition: EntityDefinition, +): EntityDefinition { + EntityDefinitionSchema.parse(definition) + return definition +} + +export async function createGitDbDataSource( + options: GitDbDataSourceOptions, +): Promise { + const engine = await GitDbEngine.open({ store: options.store }) + const dataSource = new GitDbDataSource(engine) + if (options.synchronize === true) { + for (const entity of options.entities) { + await dataSource.synchronize(entity) + } + } + return dataSource +} + +export class GitDbDataSource { + readonly #engine: GitDbEngine + + constructor(engine: GitDbEngine) { + this.#engine = engine + } + + async query(sql: string): Promise { + return (await this.#engine.execute(sql)).rows + } + + getRepository(entity: EntityDefinition): GitDbRepository { + return new GitDbRepository(this.#engine, entity) + } + + async synchronize(entity: EntityDefinition): Promise { + await this.#engine.execute(createTableSql(entity)) + } + + async transaction(work: (transaction: GitDbTransaction) => Promise): Promise { + return await this.#engine.transaction(work) + } +} + +export class GitDbRepository { + readonly #engine: GitDbEngine + readonly #entity: EntityDefinition + + constructor(engine: GitDbEngine, entity: EntityDefinition) { + this.#engine = engine + this.#entity = entity + } + + async save(row: Row): Promise { + await this.#engine.transaction(async (transaction) => { + await transaction.execute(deleteSql(this.#entity, primaryKeyWhere(this.#entity, row))) + await transaction.execute(insertSql(this.#entity, row)) + }) + } + + async find(options: FindOptions = {}): Promise { + const result = await this.#engine.execute(selectSql(this.#entity, options.where ?? {})) + return result.rows.map((row) => row as Row) + } + + async findOne(options: FindOptions): Promise { + const rows = await this.find(options) + return rows[0] ?? null + } + + async delete(where: FindOptions["where"]): Promise { + if (where === undefined || Object.keys(where).length === 0) { + throw new GitDbOrmError("delete requires a non-empty where clause") + } + const result = await this.#engine.execute(deleteSql(this.#entity, where)) + return result.rowCount + } +} + +function createTableSql(entity: EntityDefinition): string { + const columns = Object.entries(entity.columns) + .map(([name, type]) => `${identifier(name)} ${type}`) + .join(", ") + return `CREATE TABLE IF NOT EXISTS ${identifier(entity.tableName)} (${columns})` +} + +function insertSql(entity: EntityDefinition, row: Row): string { + const columns = Object.keys(entity.columns) + const names = columns.map(identifier).join(", ") + const values = columns.map((column) => sqlLiteral(valueForKey(row, column))).join(", ") + return `INSERT INTO ${identifier(entity.tableName)} (${names}) VALUES (${values})` +} + +function selectSql( + entity: EntityDefinition, + where: FindOptions["where"], +): string { + const clause = whereClause(where) + return `SELECT * FROM ${identifier(entity.tableName)}${clause}` +} + +function deleteSql( + entity: EntityDefinition, + where: FindOptions["where"], +): string { + const clause = whereClause(where) + if (clause.length === 0) { + throw new GitDbOrmError("delete requires a non-empty where clause") + } + return `DELETE FROM ${identifier(entity.tableName)}${clause}` +} + +function whereClause(where: FindOptions["where"]): string { + if (where === undefined || Object.keys(where).length === 0) { + return "" + } + const parts = Object.entries(where).map( + ([column, value]) => `${identifier(column)} = ${sqlLiteral(value)}`, + ) + return ` WHERE ${parts.join(" AND ")}` +} + +function identifier(value: string): string { + if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) { + throw new GitDbOrmError(`invalid SQL identifier: ${value}`) + } + return value +} + +function valueForKey(row: Row, key: string): JsonPrimitive { + const value: unknown = Reflect.get(row, key) + if ( + value === null || + typeof value === "string" || + typeof value === "number" || + typeof value === "boolean" + ) { + return value + } + throw new GitDbOrmError(`entity value for ${key} must be a JSON primitive`) +} + +function primaryKeyWhere( + entity: EntityDefinition, + row: Row, +): Partial, JsonPrimitive>> { + const where: Partial, JsonPrimitive>> = {} + where[entity.primaryKey] = valueForKey(row, entity.primaryKey) + return where +} diff --git a/src/sql/database.ts b/src/sql/database.ts new file mode 100644 index 0000000..b7adbbc --- /dev/null +++ b/src/sql/database.ts @@ -0,0 +1,78 @@ +import alasql from "alasql" +import { z } from "zod" +import { SqlExecutionError } from "../errors.js" +import type { VisibleDatabaseSnapshot } from "../types.js" + +export type AlaSqlDatabase = InstanceType + +const AlaSqlColumnSchema = z.object({ + columnid: z.string().min(1), +}) + +const AlaSqlTableSchema = z.object({ + columns: z.array(AlaSqlColumnSchema), + data: z.array(z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()]))), +}) + +const AlaSqlTablesSchema = z.record(z.string(), AlaSqlTableSchema) + +let databaseCounter = 0 + +export function createDatabase(): AlaSqlDatabase { + databaseCounter += 1 + return new alasql.Database(`gitdb_${databaseCounter}`) +} + +export function databaseFromSnapshot(snapshot: VisibleDatabaseSnapshot): AlaSqlDatabase { + const db = createDatabase() + restoreSnapshot(db, snapshot) + return db +} + +export function snapshotFromDatabase( + db: AlaSqlDatabase, + sequence: number, +): VisibleDatabaseSnapshot { + const parsed = AlaSqlTablesSchema.parse(db.tables) + return { + sequence, + tables: Object.entries(parsed).map(([name, table]) => ({ + columns: table.columns.map((column) => column.columnid), + name, + rows: table.data, + })), + } +} + +export function restoreSnapshot(db: AlaSqlDatabase, snapshot: VisibleDatabaseSnapshot): void { + for (const table of snapshot.tables) { + const columns = table.columns.map((column) => `${column} STRING`).join(", ") + execOn(db, `CREATE TABLE IF NOT EXISTS ${table.name} (${columns})`, `restore ${table.name}`) + for (const row of table.rows) { + const values = table.columns.map((column) => sqlLiteral(row[column] ?? null)).join(", ") + execOn(db, `INSERT INTO ${table.name} VALUES (${values})`, `restore ${table.name}`) + } + } +} + +export function execOn(db: AlaSqlDatabase, normalizedSql: string, originalSql: string): unknown { + try { + return db.exec(normalizedSql) + } catch (error) { + const detail = error instanceof Error ? error.message : String(error) + throw new SqlExecutionError(originalSql, detail) + } +} + +function sqlLiteral(value: string | number | boolean | null): string { + if (value === null) { + return "NULL" + } + if (typeof value === "number") { + return value.toString() + } + if (typeof value === "boolean") { + return value ? "TRUE" : "FALSE" + } + return `'${value.replaceAll("'", "''")}'` +} diff --git a/src/sql/engine.ts b/src/sql/engine.ts index b516ec5..9388905 100644 --- a/src/sql/engine.ts +++ b/src/sql/engine.ts @@ -1,41 +1,44 @@ import alasql from "alasql" -import { z } from "zod" import { SqlExecutionError } from "../errors.js" import type { GitDbStore } from "../storage/store.js" -import type { - GitDbManifest, - PersistedMutation, - SqlResult, - VisibleDatabaseSnapshot, -} from "../types.js" +import type { GitDbManifest, PersistedMutation, SqlResult } from "../types.js" import { maybeCatalogResult } from "./catalog.js" +import { + type AlaSqlDatabase, + createDatabase, + databaseFromSnapshot, + execOn, + restoreSnapshot, + snapshotFromDatabase, +} from "./database.js" import { commandTag, isMutation, isTransactionControl, normalizePostgresSql } from "./normalize.js" import { toRows } from "./rows.js" +import { EngineTransaction, type GitDbTransaction } from "./transaction.js" + +export type { GitDbTransaction } from "./transaction.js" type GitDbEngineOptions = { readonly store: GitDbStore + readonly snapshotPolicy?: SnapshotPolicy } -const AlaSqlColumnSchema = z.object({ - columnid: z.string().min(1), -}) - -const AlaSqlTableSchema = z.object({ - columns: z.array(AlaSqlColumnSchema), - data: z.array(z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()]))), -}) - -const AlaSqlTablesSchema = z.record(z.string(), AlaSqlTableSchema) +export type SnapshotPolicy = + | { readonly mode: "everyMutation" } + | { readonly mode: "interval"; readonly mutations: number } export class GitDbEngine { - readonly #db: InstanceType + #db: AlaSqlDatabase readonly #store: GitDbStore + readonly #snapshotPolicy: SnapshotPolicy #manifest: GitDbManifest + #mutationQueue = Promise.resolve() + #lastVisibleSnapshotError: unknown = undefined - private constructor(store: GitDbStore, manifest: GitDbManifest) { + private constructor(store: GitDbStore, manifest: GitDbManifest, snapshotPolicy: SnapshotPolicy) { this.#store = store this.#manifest = manifest - this.#db = new alasql.Database("gitdb") + this.#snapshotPolicy = snapshotPolicy + this.#db = createDatabase() alasql.options.postgres = true } @@ -50,7 +53,11 @@ export class GitDbEngine { updatedAt: now, logSegments: [], } satisfies GitDbManifest) - const engine = new GitDbEngine(options.store, manifest) + const engine = new GitDbEngine( + options.store, + manifest, + options.snapshotPolicy ?? { mode: "everyMutation" }, + ) const restoredSnapshot = await engine.#restoreVisibleSnapshot() if (!restoredSnapshot) { await engine.#replay() @@ -70,15 +77,55 @@ export class GitDbEngine { return { command: commandTag(sql, 0), rowCount: 0, rows: [] } } const normalized = normalizePostgresSql(sql) - const result = this.#exec(normalized, sql) - const rows = toRows(result) - const rowCount = rows.length > 0 ? rows.length : typeof result === "number" ? result : 0 - const response = { command: commandTag(sql, rowCount), rowCount, rows } satisfies SqlResult if (isMutation(sql)) { - await this.#persist(sql) - await this.#persistVisibleSnapshot() + return await this.#executeSingleMutation(normalized, sql) } - return response + const result = execOn(this.#db, normalized, sql) + const rows = toRows(result) + const rowCount = rows.length > 0 ? rows.length : typeof result === "number" ? result : 0 + return { command: commandTag(sql, rowCount), rowCount, rows } + } + + async transaction(work: (transaction: GitDbTransaction) => Promise): Promise { + return await this.#enqueueMutation(async () => { + const baseSnapshot = snapshotFromDatabase(this.#db, this.#manifest.sequence) + const transactionDb = databaseFromSnapshot(baseSnapshot) + const transaction = new EngineTransaction(transactionDb) + const value = await work(transaction) + const mutations = transaction.mutations() + if (mutations.length === 0) { + return value + } + await this.#persistMany(mutations) + this.#db = transactionDb + if (this.#shouldPersistVisibleSnapshot()) { + await this.#persistVisibleSnapshotBestEffort() + } + return value + }) + } + + async #executeSingleMutation(normalizedSql: string, originalSql: string): Promise { + return await this.#enqueueMutation(async () => { + const committedManifest = this.#manifest + try { + const result = execOn(this.#db, normalizedSql, originalSql) + const rows = toRows(result) + const rowCount = rows.length > 0 ? rows.length : typeof result === "number" ? result : 0 + await this.#persistMany([originalSql]) + if (this.#shouldPersistVisibleSnapshot()) { + await this.#persistVisibleSnapshotBestEffort() + } + return { command: commandTag(originalSql, rowCount), rowCount, rows } + } catch (error: unknown) { + await this.#restoreCommittedState(committedManifest) + throw error + } + }) + } + + getLastVisibleSnapshotError(): unknown | undefined { + return this.#lastVisibleSnapshotError } async #restoreVisibleSnapshot(): Promise { @@ -89,14 +136,13 @@ export class GitDbEngine { if (snapshot === null) { return false } - for (const table of snapshot.tables) { - const columns = table.columns.map((column) => `${column} STRING`).join(", ") - this.#exec(`CREATE TABLE IF NOT EXISTS ${table.name} (${columns})`, `restore ${table.name}`) - for (const row of table.rows) { - const values = table.columns.map((column) => sqlLiteral(row[column] ?? null)).join(", ") - this.#exec(`INSERT INTO ${table.name} VALUES (${values})`, `restore ${table.name}`) - } + if (snapshot.sequence === undefined && this.#manifest.sequence > 0) { + return false + } + if (snapshot.sequence !== undefined && snapshot.sequence !== this.#manifest.sequence) { + return false } + restoreSnapshot(this.#db, snapshot) return true } @@ -104,60 +150,79 @@ export class GitDbEngine { if (this.#store.writeVisibleSnapshot === undefined) { return } - await this.#store.writeVisibleSnapshot(this.#visibleSnapshot()) + await this.#store.writeVisibleSnapshot(snapshotFromDatabase(this.#db, this.#manifest.sequence)) } - #visibleSnapshot(): VisibleDatabaseSnapshot { - const parsed = AlaSqlTablesSchema.parse(this.#db.tables) - return { - tables: Object.entries(parsed).map(([name, table]) => ({ - columns: table.columns.map((column) => column.columnid), - name, - rows: table.data, - })), + async #persistVisibleSnapshotBestEffort(): Promise { + try { + await this.#persistVisibleSnapshot() + this.#lastVisibleSnapshotError = undefined + } catch (error: unknown) { + this.#lastVisibleSnapshotError = error } } async #replay(): Promise { const mutations = await this.#store.readMutations(this.#manifest.logSegments) for (const mutation of mutations) { - this.#exec(normalizePostgresSql(mutation.sql), mutation.sql) + execOn(this.#db, normalizePostgresSql(mutation.sql), mutation.sql) } } - async #persist(sql: string): Promise { - const nextSequence = this.#manifest.sequence + 1 - const at = new Date().toISOString() - const mutation = { at, sequence: nextSequence, sql } satisfies PersistedMutation - const segment = await this.#store.appendMutation(mutation) - this.#manifest = { + async #persistMany(sqlStatements: readonly string[]): Promise { + let sequence = this.#manifest.sequence + const logSegments = [...this.#manifest.logSegments] + let updatedAt = this.#manifest.updatedAt + for (const sql of sqlStatements) { + sequence += 1 + updatedAt = new Date().toISOString() + const mutation = { at: updatedAt, sequence, sql } satisfies PersistedMutation + const segment = await this.#store.appendMutation(mutation) + logSegments.push(segment) + } + const nextManifest = { ...this.#manifest, - sequence: nextSequence, - updatedAt: at, - logSegments: [...this.#manifest.logSegments, segment], + logSegments, + sequence, + updatedAt, } - await this.#store.writeManifest(this.#manifest) + await this.#store.writeManifest(nextManifest) + this.#manifest = nextManifest } - #exec(normalizedSql: string, originalSql: string): unknown { - try { - return this.#db.exec(normalizedSql) - } catch (error) { - const detail = error instanceof Error ? error.message : String(error) - throw new SqlExecutionError(originalSql, detail) + async #restoreCommittedState(manifest: GitDbManifest): Promise { + this.#manifest = manifest + this.#db = createDatabase() + const restoredSnapshot = await this.#restoreVisibleSnapshot() + if (!restoredSnapshot) { + await this.#replay() } } -} -function sqlLiteral(value: string | number | boolean | null): string { - if (value === null) { - return "NULL" - } - if (typeof value === "number") { - return value.toString() + async #enqueueMutation(operation: () => Promise): Promise { + const queued = this.#mutationQueue.then(operation, operation) + this.#mutationQueue = queued.then( + () => undefined, + () => undefined, + ) + return await queued } - if (typeof value === "boolean") { - return value ? "TRUE" : "FALSE" + + #shouldPersistVisibleSnapshot(): boolean { + switch (this.#snapshotPolicy.mode) { + case "everyMutation": + return true + case "interval": + return this.#manifest.sequence % this.#snapshotPolicy.mutations === 0 + default: + return assertNever(this.#snapshotPolicy) + } } - return `'${value.replaceAll("'", "''")}'` +} + +function assertNever(value: never): never { + throw new SqlExecutionError( + "snapshot policy", + `unexpected snapshot policy ${JSON.stringify(value)}`, + ) } diff --git a/src/sql/transaction.ts b/src/sql/transaction.ts new file mode 100644 index 0000000..e9e4bc8 --- /dev/null +++ b/src/sql/transaction.ts @@ -0,0 +1,41 @@ +import type { SqlResult } from "../types.js" +import { maybeCatalogResult } from "./catalog.js" +import type { AlaSqlDatabase } from "./database.js" +import { execOn } from "./database.js" +import { commandTag, isMutation, isTransactionControl, normalizePostgresSql } from "./normalize.js" +import { toRows } from "./rows.js" + +export interface GitDbTransaction { + execute(sql: string): Promise +} + +export class EngineTransaction implements GitDbTransaction { + readonly #db: AlaSqlDatabase + readonly #mutations: string[] = [] + + constructor(db: AlaSqlDatabase) { + this.#db = db + } + + async execute(sql: string): Promise { + const catalog = maybeCatalogResult(sql) + if (catalog !== null) { + return catalog + } + if (isTransactionControl(sql)) { + return { command: commandTag(sql, 0), rowCount: 0, rows: [] } + } + const normalized = normalizePostgresSql(sql) + const result = execOn(this.#db, normalized, sql) + const rows = toRows(result) + const rowCount = rows.length > 0 ? rows.length : typeof result === "number" ? result : 0 + if (isMutation(sql)) { + this.#mutations.push(sql) + } + return { command: commandTag(sql, rowCount), rowCount, rows } + } + + mutations(): readonly string[] { + return this.#mutations + } +} diff --git a/src/storage/local-plaintext-store.ts b/src/storage/local-plaintext-store.ts index c8d5433..bee96ff 100644 --- a/src/storage/local-plaintext-store.ts +++ b/src/storage/local-plaintext-store.ts @@ -56,6 +56,7 @@ export class LocalPlaintextStore implements GitDbStore { } async readVisibleSnapshot(): Promise { + const checkpoint = await this.#readSnapshotCheckpoint() const tableNames = await this.#readDirectoryNames() const tables = [] for (const tableName of tableNames) { @@ -69,7 +70,10 @@ export class LocalPlaintextStore implements GitDbStore { }) } } - return tables.length === 0 ? null : { tables } + if (tables.length === 0) { + return null + } + return checkpoint === undefined ? { tables } : { sequence: checkpoint, tables } } async writeVisibleSnapshot(snapshot: VisibleDatabaseSnapshot): Promise { @@ -80,6 +84,9 @@ export class LocalPlaintextStore implements GitDbStore { }) await this.#writeJson(join(this.#root, table.name, "data.json"), table.rows) } + if (snapshot.sequence !== undefined) { + await this.#writeJson(join(this.#root, "snapshot.json"), { sequence: snapshot.sequence }) + } } async #writeJson(path: string, value: unknown): Promise { @@ -102,6 +109,15 @@ export class LocalPlaintextStore implements GitDbStore { } } + async #readSnapshotCheckpoint(): Promise { + const payload = await this.#readNullable(join(this.#root, "snapshot.json")) + if (payload === null) { + return undefined + } + const parsed = JSON.parse(payload) as { readonly sequence?: unknown } + return typeof parsed.sequence === "number" ? parsed.sequence : undefined + } + async #readNullable(path: string): Promise { try { return await readFile(path, "utf8") diff --git a/src/storage/plaintext-codec.ts b/src/storage/plaintext-codec.ts index 51b56ac..31ede09 100644 --- a/src/storage/plaintext-codec.ts +++ b/src/storage/plaintext-codec.ts @@ -37,6 +37,7 @@ const VisibleTableSnapshotSchema = VisibleTableSchemaSchema.extend({ }) const VisibleDatabaseSnapshotSchema = z.object({ + sequence: z.number().int().nonnegative().optional(), tables: z.array(VisibleTableSnapshotSchema), }) @@ -65,7 +66,10 @@ export function parseVisibleTableRows(payload: string): VisibleTableSnapshot["ro } export function parseVisibleDatabaseSnapshot(payload: string): VisibleDatabaseSnapshot { - return VisibleDatabaseSnapshotSchema.parse(JSON.parse(payload)) + const parsed = VisibleDatabaseSnapshotSchema.parse(JSON.parse(payload)) + return parsed.sequence === undefined + ? { tables: parsed.tables } + : { sequence: parsed.sequence, tables: parsed.tables } } export function stringifyPlaintext(value: unknown): string { diff --git a/src/types.ts b/src/types.ts index c555b6d..6938632 100644 --- a/src/types.ts +++ b/src/types.ts @@ -22,6 +22,7 @@ export type VisibleTableSchema = { } export type VisibleDatabaseSnapshot = { + readonly sequence?: number readonly tables: readonly VisibleTableSnapshot[] } diff --git a/tests/benchmark-script.test.ts b/tests/benchmark-script.test.ts new file mode 100644 index 0000000..7067fed --- /dev/null +++ b/tests/benchmark-script.test.ts @@ -0,0 +1,43 @@ +import { readFile } from "node:fs/promises" +import { describe, expect, it } from "vitest" +import packageJson from "../package.json" with { type: "json" } + +describe("benchmark evaluator", () => { + it("defines a benchmark evaluator script for JSON evidence output", () => { + // Given: package metadata is the user-facing command surface. + const scripts = packageJson.scripts + + // When: the evaluator command is inspected. + const command = scripts["benchmark:evaluate"] + + // Then: it builds first and asks the benchmark runner for JSON output. + expect(command).toBe("pnpm build && node scripts/benchmark.mjs --json") + }) + + it("supports JSON output and evidence file targets in the benchmark runner", async () => { + // Given: the benchmark runner is the evaluator used by package scripts. + const script = await readFile("scripts/benchmark.mjs", "utf8") + + // When: the runner implementation is inspected. + const hasJsonFlag = script.includes("--json") + const hasOutputTarget = script.includes("GITDB_BENCH_OUTPUT") + + // Then: evaluator runs can produce machine-readable evidence. + expect(hasJsonFlag).toBe(true) + expect(hasOutputTarget).toBe(true) + }) + + it("defines a benchmark comparison command for the website evidence file", () => { + // Given: the public site should be generated from reproducible benchmark evidence. + const scripts = packageJson.scripts + + // When: package scripts are inspected. + const command = scripts["benchmark:compare"] + + // Then: the comparison command writes both machine and Markdown summaries. + expect(command).toContain("GITDB_BENCH_OUTPUT=.gitdb/bench-current.json") + expect(command).toContain("scripts/benchmark-compare.mjs") + expect(command).toContain("--output site/benchmark.json") + expect(command).toContain("--markdown site/benchmark.md") + }) +}) diff --git a/tests/github-plaintext-store.test.ts b/tests/github-plaintext-store.test.ts new file mode 100644 index 0000000..dc21e6e --- /dev/null +++ b/tests/github-plaintext-store.test.ts @@ -0,0 +1,130 @@ +import { beforeEach, describe, expect, it, vi } from "vitest" +import { GitHubPlaintextStore } from "../src/github/github-plaintext-store.js" +import type { GitHubConfig } from "../src/github/types.js" + +const octokit = vi.hoisted(() => ({ + createOrUpdateFileContents: vi.fn(), + getContent: vi.fn(), +})) + +vi.mock("@octokit/rest", () => ({ + Octokit: vi.fn(function MockOctokit() { + return { + repos: { + createOrUpdateFileContents: octokit.createOrUpdateFileContents, + getContent: octokit.getContent, + }, + } + }), +})) + +const config = { + branch: "main", + owner: "3x-haust", + prefix: "gitdb/v1", + repo: "gitdb-example-db", + token: "redacted-token", +} satisfies GitHubConfig + +describe("GitHubPlaintextStore", () => { + beforeEach(() => { + octokit.createOrUpdateFileContents.mockReset() + octokit.getContent.mockReset() + }) + + it("reads visible snapshot checkpoint metadata", async () => { + // Given: GitHub stores a visible table snapshot with checkpoint metadata. + octokit.getContent.mockImplementation(async ({ path }: { readonly path: string }) => { + const content = githubContentFor(path) + if (content === null) { + throw new FakeGitHubStatusError(404) + } + return { data: content } + }) + const store = new GitHubPlaintextStore(config) + + // When: the visible snapshot is read. + const snapshot = await store.readVisibleSnapshot() + + // Then: the checkpoint sequence is preserved for manifest freshness checks. + expect(snapshot).toEqual({ + sequence: 7, + tables: [ + { + columns: ["id", "name"], + name: "people", + rows: [{ id: "p1", name: "Lin" }], + }, + ], + }) + }) + + it("writes visible snapshot checkpoint metadata", async () => { + // Given: GitHub has no existing visible snapshot files. + octokit.getContent.mockRejectedValue(new FakeGitHubStatusError(404)) + octokit.createOrUpdateFileContents.mockResolvedValue({ data: {} }) + const store = new GitHubPlaintextStore(config) + + // When: a checkpointed visible snapshot is written. + await store.writeVisibleSnapshot({ + sequence: 9, + tables: [ + { + columns: ["id", "name"], + name: "people", + rows: [{ id: "p1", name: "Lin" }], + }, + ], + }) + + // Then: GitHub receives snapshot metadata after table dashboard files. + const writes = octokit.createOrUpdateFileContents.mock.calls.map(([request]) => request) + expect(writes.map((request) => request.path)).toEqual([ + "gitdb/v1/people/schema.json", + "gitdb/v1/people/data.json", + "gitdb/v1/snapshot.json", + ]) + expect(decodeContent(writes[2].content)).toEqual({ sequence: 9 }) + }) +}) + +class FakeGitHubStatusError extends Error { + constructor(readonly status: number) { + super(`GitHub status ${status}`) + } +} + +function githubContentFor(path: string): unknown { + switch (path) { + case "gitdb/v1": + return [ + { name: "log", type: "dir" }, + { name: "people", type: "dir" }, + ] + case "gitdb/v1/snapshot.json": + return encodedFile({ sequence: 7 }, "snapshot-sha") + case "gitdb/v1/people/schema.json": + return encodedFile({ columns: ["id", "name"], name: "people" }, "schema-sha") + case "gitdb/v1/people/data.json": + return encodedFile([{ id: "p1", name: "Lin" }], "data-sha") + default: + return null + } +} + +function encodedFile( + value: unknown, + sha: string, +): { readonly content: string; readonly sha: string } { + return { + content: Buffer.from(JSON.stringify(value), "utf8").toString("base64"), + sha, + } +} + +function decodeContent(content: unknown): unknown { + if (typeof content !== "string") { + throw new TypeError("expected GitHub content to be a base64 string") + } + return JSON.parse(Buffer.from(content, "base64").toString("utf8")) as unknown +} diff --git a/tests/orm.test.ts b/tests/orm.test.ts new file mode 100644 index 0000000..9f7a070 --- /dev/null +++ b/tests/orm.test.ts @@ -0,0 +1,62 @@ +import { mkdtemp } from "node:fs/promises" +import { tmpdir } from "node:os" +import { join } from "node:path" +import { describe, expect, it } from "vitest" +import { createGitDbDataSource, defineEntity, type EntityDefinition } from "../src/orm/index.js" +import { LocalPlaintextStore } from "../src/storage/local-plaintext-store.js" + +type Person = { + readonly id: string + readonly name: string + readonly team_id: string +} + +const PersonEntity = defineEntity({ + columns: { + id: "STRING", + name: "STRING", + team_id: "STRING", + }, + primaryKey: "id", + tableName: "people", +}) + +describe("GitDB ORM", () => { + it("rejects unsupported JSON column metadata", () => { + // Given: entity metadata that claims a JSON column. + const entity = { + columns: { + id: "STRING", + metadata: "JSON", + }, + primaryKey: "id", + tableName: "people", + } as unknown as EntityDefinition<{ readonly id: string; readonly metadata: string }> + + // When/Then: the ORM refuses a type it cannot persist faithfully yet. + expect(() => defineEntity(entity)).toThrow() + }) + + it("saves, finds, and deletes entities through a TypeORM-style repository", async () => { + // Given: a GitDB data source synchronized from entity metadata. + const root = await mkdtemp(join(tmpdir(), "gitdb-orm-")) + const dataSource = await createGitDbDataSource({ + entities: [PersonEntity], + store: new LocalPlaintextStore({ root }), + synchronize: true, + }) + const people = dataSource.getRepository(PersonEntity) + + // When: callers use repository methods instead of raw SQL. + await people.save({ id: "p1", name: "Lin", team_id: "storage" }) + await people.save({ id: "p2", name: "Ada", team_id: "runtime" }) + const found = await people.find({ where: { team_id: "storage" } }) + const one = await people.findOne({ where: { id: "p2" } }) + await people.delete({ id: "p1" }) + + // Then: the repository exposes typed CRUD ergonomics over the local runtime. + await expect(people.find()).resolves.toEqual([{ id: "p2", name: "Ada", team_id: "runtime" }]) + expect(found).toEqual([{ id: "p1", name: "Lin", team_id: "storage" }]) + expect(one).toEqual({ id: "p2", name: "Ada", team_id: "runtime" }) + }) +}) diff --git a/tests/package-metadata.test.ts b/tests/package-metadata.test.ts new file mode 100644 index 0000000..0a84815 --- /dev/null +++ b/tests/package-metadata.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest" +import packageJson from "../package.json" with { type: "json" } + +describe("npm publish metadata", () => { + it("declares package root exports, types, and public publish config", () => { + // Given: npm consumers install the built package by its root entrypoint. + const metadata = packageJson + + // When: publish metadata is inspected before packing. + const rootExport = metadata.exports["."] + + // Then: Node and TypeScript consumers get an explicit package surface. + expect(metadata.main).toBe("./dist/src/index.js") + expect(metadata.types).toBe("./dist/src/index.d.ts") + expect(metadata.homepage).toBe("https://3x-haust.github.io/gitdb/") + expect(metadata.bin).toEqual({ gitdb: "dist/src/cli/main.js" }) + expect(metadata.files).toEqual(["dist/src", "README.md", "docs"]) + expect(metadata.publishConfig).toEqual({ access: "public", provenance: true }) + expect(metadata.repository.url).toBe("git+https://github.com/3x-haust/gitdb.git") + expect(rootExport).toEqual({ + import: "./dist/src/index.js", + types: "./dist/src/index.d.ts", + }) + }) + + it("declares dry-run publish scripts that do not publish to npm", () => { + // Given: release validation must be credential-safe. + const scripts = packageJson.scripts + + // When: publish scripts are inspected. + const pack = scripts["pack:dry-run"] + const publish = scripts["publish:dry-run"] + + // Then: both commands validate package contents without publishing. + expect(pack).toBe("pnpm build && COREPACK_ENABLE_STRICT=0 npm pack --dry-run --json") + expect(publish).toBe( + "pnpm build && COREPACK_ENABLE_STRICT=0 npm publish --dry-run --access public", + ) + }) +}) diff --git a/tests/plaintext-store.test.ts b/tests/plaintext-store.test.ts index 93a3e2d..e8b9356 100644 --- a/tests/plaintext-store.test.ts +++ b/tests/plaintext-store.test.ts @@ -1,9 +1,10 @@ -import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises" +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises" import { tmpdir } from "node:os" import { join } from "node:path" import { afterEach, describe, expect, it } from "vitest" import { GitDbEngine } from "../src/sql/engine.js" import { LocalPlaintextStore } from "../src/storage/local-plaintext-store.js" +import type { VisibleDatabaseSnapshot } from "../src/types.js" describe("plaintext store", () => { const roots: string[] = [] @@ -62,4 +63,104 @@ describe("plaintext store", () => { expect(visibleData).toContain('"Lin"') expect(result.rows).toEqual([{ name: "Ada" }]) }) + + it("replays logs when visible snapshot checkpoint is stale", async () => { + // Given: a plaintext store with a current visible snapshot and later unsnapshotted log entries. + const root = await mkdtemp(join(tmpdir(), "gitdb-stale-snapshot-")) + roots.push(root) + const first = await GitDbEngine.open({ + snapshotPolicy: { mode: "interval", mutations: 100 }, + store: new LocalPlaintextStore({ root }), + }) + await first.execute("CREATE TABLE events (id STRING, label STRING)") + await first.execute("INSERT INTO events VALUES ('e1', 'snapshotted')") + await first.execute("INSERT INTO events VALUES ('e2', 'log-only')") + + // When: the engine reopens after the manifest moved beyond the visible snapshot checkpoint. + const second = await GitDbEngine.open({ store: new LocalPlaintextStore({ root }) }) + const result = await second.execute("SELECT label FROM events ORDER BY id") + + // Then: it ignores the stale visible files and replays the mutation log. + expect(result.rows).toEqual([{ label: "snapshotted" }, { label: "log-only" }]) + }) + + it("replays logs when visible snapshot has no checkpoint", async () => { + // Given: a manifest-backed store with old dashboard files but no snapshot checkpoint. + const root = await mkdtemp(join(tmpdir(), "gitdb-uncheckpointed-snapshot-")) + roots.push(root) + const engine = await GitDbEngine.open({ + snapshotPolicy: { mode: "interval", mutations: 100 }, + store: new LocalPlaintextStore({ root }), + }) + await engine.execute("CREATE TABLE events (id STRING, label STRING)") + await engine.execute("INSERT INTO events VALUES ('e1', 'dashboard-only')") + await engine.execute("INSERT INTO events VALUES ('e2', 'log-only')") + await mkdir(join(root, "gitdb/v1/events"), { recursive: true }) + await writeFile( + join(root, "gitdb/v1/events/schema.json"), + '{ "name": "events", "columns": ["id", "label"] }', + "utf8", + ) + await writeFile( + join(root, "gitdb/v1/events/data.json"), + '[{ "id": "e1", "label": "dashboard-only" }]', + "utf8", + ) + + // When: the engine reopens after seeing uncheckpointed visible files. + const reopened = await GitDbEngine.open({ store: new LocalPlaintextStore({ root }) }) + const result = await reopened.execute("SELECT label FROM events ORDER BY id") + + // Then: manifest/log replay wins over the uncheckpointed dashboard snapshot. + expect(result.rows).toEqual([{ label: "dashboard-only" }, { label: "log-only" }]) + }) + + it("writes visible snapshot checkpoint after table files", async () => { + // Given: a table path that will fail because it is already a file. + const root = await mkdtemp(join(tmpdir(), "gitdb-snapshot-order-")) + roots.push(root) + const store = new LocalPlaintextStore({ root }) + await mkdir(join(root, "gitdb/v1"), { recursive: true }) + await writeFile(join(root, "gitdb/v1/events"), "not a directory", "utf8") + + // When: the visible snapshot write fails before table contents are complete. + await expect( + store.writeVisibleSnapshot({ + sequence: 1, + tables: [{ columns: ["id"], name: "events", rows: [{ id: "e1" }] }], + }), + ).rejects.toThrow() + + // Then: the advanced checkpoint is not left behind. + await expect(readFile(join(root, "gitdb/v1/snapshot.json"), "utf8")).rejects.toThrow() + }) + + it("throttles visible snapshot writes for local plaintext speed", async () => { + // Given: a plaintext store that counts visible snapshot writes. + const root = await mkdtemp(join(tmpdir(), "gitdb-snapshot-policy-")) + roots.push(root) + const store = new CountingPlaintextStore({ root }) + const engine = await GitDbEngine.open({ + snapshotPolicy: { mode: "interval", mutations: 100 }, + store, + }) + + // When: many local mutations run against the same table. + await engine.execute("CREATE TABLE events (id STRING, label STRING)") + for (let index = 0; index < 250; index += 1) { + await engine.execute(`INSERT INTO events VALUES ('e${index}', 'event ${index}')`) + } + + // Then: visible dashboard snapshots are checkpointed instead of rewritten per row. + expect(store.snapshotWrites).toBeLessThanOrEqual(3) + }) }) + +class CountingPlaintextStore extends LocalPlaintextStore { + snapshotWrites = 0 + + override async writeVisibleSnapshot(snapshot: VisibleDatabaseSnapshot): Promise { + this.snapshotWrites += 1 + await super.writeVisibleSnapshot(snapshot) + } +} diff --git a/tests/site.test.ts b/tests/site.test.ts new file mode 100644 index 0000000..bf82937 --- /dev/null +++ b/tests/site.test.ts @@ -0,0 +1,43 @@ +import { readFile } from "node:fs/promises" +import { describe, expect, it } from "vitest" + +type BenchmarkEvidence = { + readonly comparisons: readonly unknown[] + readonly headline: string +} + +describe("website", () => { + it("publishes benchmark evidence consumed by the static site", async () => { + const parsed: unknown = JSON.parse(await readFile("site/benchmark.json", "utf8")) + + expect(isBenchmarkEvidence(parsed)).toBe(true) + if (!isBenchmarkEvidence(parsed)) { + throw new Error("invalid benchmark evidence") + } + expect(parsed.comparisons.length).toBeGreaterThan(0) + expect(parsed.headline).toContain("Local plaintext") + }) + + it("deploys the site directory through GitHub Pages", async () => { + const workflow = await readFile(".github/workflows/pages.yml", "utf8") + const index = await readFile("site/index.html", "utf8") + const app = await readFile("site/app.js", "utf8") + + expect(workflow).toContain("path: site") + expect(app).toContain("./benchmark.json") + expect(index).toContain("A database runtime that treats a GitHub repo as durable storage") + expect(index).toContain("GitDB is not a database file uploaded to GitHub") + expect(index).toContain("./assets/runtime-map.svg") + }) +}) + +function isBenchmarkEvidence(value: unknown): value is BenchmarkEvidence { + return ( + typeof value === "object" && + value !== null && + "comparisons" in value && + Array.isArray(value.comparisons) && + "headline" in value && + typeof value.headline === "string" + ) +} diff --git a/tests/sql-engine.test.ts b/tests/sql-engine.test.ts index 6019da3..e5dd040 100644 --- a/tests/sql-engine.test.ts +++ b/tests/sql-engine.test.ts @@ -1,10 +1,18 @@ -import { mkdtemp, readFile } from "node:fs/promises" +import { mkdtemp, readdir, readFile } from "node:fs/promises" import { tmpdir } from "node:os" import { join } from "node:path" import { describe, expect, it } from "vitest" import { createAesGcmCipher } from "../src/crypto/aes-gcm.js" import { GitDbEngine } from "../src/sql/engine.js" import { LocalEncryptedStore } from "../src/storage/local-encrypted-store.js" +import { LocalPlaintextStore } from "../src/storage/local-plaintext-store.js" +import type { GitDbStore } from "../src/storage/store.js" +import type { + GitDbManifest, + PersistedMutation, + SegmentId, + VisibleDatabaseSnapshot, +} from "../src/types.js" describe("GitDbEngine", () => { it("executes join and aggregate queries while persisting encrypted GitDB segments", async () => { @@ -55,4 +63,195 @@ describe("GitDbEngine", () => { // Then: state is rebuilt from GitDB files, not process memory. expect(result.rows).toEqual([{ label: "release" }]) }) + + it("commits transactions atomically and rolls back failed statements without log drift", async () => { + // Given: a table with one committed row and a durable encrypted mutation log. + const root = await mkdtemp(join(tmpdir(), "gitdb-transaction-")) + const key = Buffer.alloc(32, 11).toString("base64url") + const store = new LocalEncryptedStore({ root, cipher: createAesGcmCipher(key) }) + const engine = await GitDbEngine.open({ store }) + await engine.execute("CREATE TABLE accounts (id STRING, balance INT)") + await engine.execute("INSERT INTO accounts VALUES ('a1', 10)") + + // When: one transaction succeeds and a later transaction fails midway. + await engine.transaction(async (transaction) => { + await transaction.execute("INSERT INTO accounts VALUES ('a2', 20)") + await transaction.execute("INSERT INTO accounts VALUES ('a3', 30)") + }) + const logAfterCommit = await readdir(join(root, "gitdb", "v1", "log")) + await expect( + engine.transaction(async (transaction) => { + await transaction.execute("INSERT INTO accounts VALUES ('a4', 40)") + await transaction.execute("THIS IS NOT SQL") + }), + ).rejects.toThrow("SQL failed") + + // Then: the failed transaction is not visible and did not append partial WAL segments. + const logAfterRollback = await readdir(join(root, "gitdb", "v1", "log")) + const rows = await engine.execute("SELECT id, balance FROM accounts ORDER BY id") + expect(rows.rows).toEqual([ + { balance: 10, id: "a1" }, + { balance: 20, id: "a2" }, + { balance: 30, id: "a3" }, + ]) + expect(logAfterRollback).toHaveLength(logAfterCommit.length) + }) + + it("does not publish orphan segments after a manifest write failure", async () => { + // Given: a plaintext store that fails the next manifest write after appending a segment. + const root = await mkdtemp(join(tmpdir(), "gitdb-manifest-failure-")) + const store = new FailingManifestStore(new LocalPlaintextStore({ root })) + const engine = await GitDbEngine.open({ + snapshotPolicy: { mode: "interval", mutations: 100 }, + store, + }) + await engine.execute("CREATE TABLE accounts (id STRING, balance INT)") + await engine.execute("INSERT INTO accounts VALUES ('a1', 10)") + + // When: persistence fails after a transaction append, then a later write succeeds. + store.failNextManifestWrite() + await expect( + engine.transaction(async (transaction) => { + await transaction.execute("INSERT INTO accounts VALUES ('a2', 20)") + }), + ).rejects.toThrow("manifest failed") + await engine.execute("INSERT INTO accounts VALUES ('a3', 30)") + const reopened = await GitDbEngine.open({ store: new LocalPlaintextStore({ root }) }) + const rows = await reopened.execute("SELECT id, balance FROM accounts ORDER BY id") + + // Then: the failed orphan segment is not pulled into a later manifest. + expect(rows.rows).toEqual([ + { balance: 10, id: "a1" }, + { balance: 30, id: "a3" }, + ]) + }) + + it("restores committed state when a single-statement manifest write fails", async () => { + // Given: a plaintext store that fails manifest persistence after appending a segment. + const root = await mkdtemp(join(tmpdir(), "gitdb-fast-manifest-failure-")) + const store = new FailingManifestStore(new LocalPlaintextStore({ root })) + const engine = await GitDbEngine.open({ + snapshotPolicy: { mode: "interval", mutations: 100 }, + store, + }) + await engine.execute("CREATE TABLE accounts (id STRING, balance INT)") + await engine.execute("INSERT INTO accounts VALUES ('a1', 10)") + + // When: the single-statement fast path fails after mutating memory. + store.failNextManifestWrite() + await expect(engine.execute("INSERT INTO accounts VALUES ('a2', 20)")).rejects.toThrow( + "manifest failed", + ) + const liveRows = await engine.execute("SELECT id, balance FROM accounts ORDER BY id") + const reopened = await GitDbEngine.open({ store: new LocalPlaintextStore({ root }) }) + const reopenedRows = await reopened.execute("SELECT id, balance FROM accounts ORDER BY id") + + // Then: live memory and reopened storage both reflect only committed manifest state. + expect(liveRows.rows).toEqual([{ balance: 10, id: "a1" }]) + expect(reopenedRows.rows).toEqual([{ balance: 10, id: "a1" }]) + }) + + it("keeps committed mutations successful when visible snapshot refresh fails", async () => { + // Given: a plaintext store whose derived visible snapshot can fail independently. + const root = await mkdtemp(join(tmpdir(), "gitdb-snapshot-failure-")) + const store = new FailingSnapshotStore(new LocalPlaintextStore({ root })) + const engine = await GitDbEngine.open({ store }) + await engine.execute("CREATE TABLE accounts (id STRING, balance INT)") + + // When: the manifest/log commit succeeds but the post-commit checkpoint write fails. + store.failNextSnapshotWrite() + await expect(engine.execute("INSERT INTO accounts VALUES ('a1', 10)")).resolves.toMatchObject({ + rowCount: 1, + }) + const liveRows = await engine.execute("SELECT id, balance FROM accounts") + const reopened = await GitDbEngine.open({ store: new LocalPlaintextStore({ root }) }) + const replayedRows = await reopened.execute("SELECT id, balance FROM accounts") + + // Then: the committed row remains visible and recoverable from the mutation log. + expect(engine.getLastVisibleSnapshotError()).toBeInstanceOf(Error) + expect(liveRows.rows).toEqual([{ balance: 10, id: "a1" }]) + expect(replayedRows.rows).toEqual([{ balance: 10, id: "a1" }]) + }) }) + +class FailingManifestStore implements GitDbStore { + readonly #delegate: GitDbStore + #failNextManifestWrite = false + + constructor(delegate: GitDbStore) { + this.#delegate = delegate + } + + failNextManifestWrite(): void { + this.#failNextManifestWrite = true + } + + async readManifest(): Promise { + return await this.#delegate.readManifest() + } + + async writeManifest(manifest: GitDbManifest): Promise { + if (this.#failNextManifestWrite) { + this.#failNextManifestWrite = false + throw new Error("manifest failed") + } + await this.#delegate.writeManifest(manifest) + } + + async appendMutation(mutation: PersistedMutation): Promise { + return await this.#delegate.appendMutation(mutation) + } + + async readMutations(segments: readonly SegmentId[]): Promise { + return await this.#delegate.readMutations(segments) + } + + async readVisibleSnapshot(): Promise { + return (await this.#delegate.readVisibleSnapshot?.()) ?? null + } + + async writeVisibleSnapshot(snapshot: VisibleDatabaseSnapshot): Promise { + await this.#delegate.writeVisibleSnapshot?.(snapshot) + } +} + +class FailingSnapshotStore implements GitDbStore { + readonly #delegate: GitDbStore + #failNextSnapshotWrite = false + + constructor(delegate: GitDbStore) { + this.#delegate = delegate + } + + failNextSnapshotWrite(): void { + this.#failNextSnapshotWrite = true + } + + async readManifest(): Promise { + return await this.#delegate.readManifest() + } + + async writeManifest(manifest: GitDbManifest): Promise { + await this.#delegate.writeManifest(manifest) + } + + async appendMutation(mutation: PersistedMutation): Promise { + return await this.#delegate.appendMutation(mutation) + } + + async readMutations(segments: readonly SegmentId[]): Promise { + return await this.#delegate.readMutations(segments) + } + + async readVisibleSnapshot(): Promise { + return (await this.#delegate.readVisibleSnapshot?.()) ?? null + } + + async writeVisibleSnapshot(snapshot: VisibleDatabaseSnapshot): Promise { + if (this.#failNextSnapshotWrite) { + this.#failNextSnapshotWrite = false + throw new Error("snapshot failed") + } + await this.#delegate.writeVisibleSnapshot?.(snapshot) + } +} From 106745da2b09c08a95888279758936148630df24 Mon Sep 17 00:00:00 2001 From: 3x-haust Date: Sun, 14 Jun 2026 10:44:29 +0900 Subject: [PATCH 02/31] refactor: rebuild gitdb as first-party runtime --- .gitignore | 2 - Dockerfile | 7 +- README.md | 352 ++--- biome.json | 5 +- docs/ARCHITECTURE.md | 153 ++- docs/BENCHMARKS.md | 111 +- docs/README.ko.md | 403 ++---- examples/express-prisma/.env.example | 10 - examples/express-prisma/schema.prisma | 8 - examples/express-prisma/server.mjs | 94 -- examples/local-runtime/index.mjs | 56 + main.ts | 2 +- package.json | 39 +- pnpm-lock.yaml | 1793 +------------------------ scripts/benchmark-report.mjs | 17 +- scripts/benchmark.mjs | 99 +- site/app.js | 77 +- site/assets/runtime-map.svg | 2 +- site/benchmark.json | 95 +- site/benchmark.md | 5 +- site/favicon.svg | 7 + site/index.html | 86 +- site/styles.css | 2 +- src/cli/main.ts | 27 +- src/config/env.ts | 2 - src/github/errors.ts | 2 +- src/http/app.module.ts | 7 - src/http/gitdb.controller.ts | 25 - src/http/gitdb.runtime.ts | 34 - src/http/main.ts | 27 - src/index.ts | 2 +- src/orm/index.ts | 32 +- src/protocol/field.ts | 26 - src/protocol/parameters.ts | 11 - src/protocol/postgres-server.ts | 179 --- src/sql/catalog.ts | 29 - src/sql/engine.ts | 13 +- src/sql/normalize.ts | 2 +- src/sql/transaction.ts | 9 +- tests/benchmark-script.test.ts | 2 +- tests/e2e.test.ts | 85 +- tests/http-runtime.test.ts | 15 - tests/no-postgres-facade.test.ts | 129 ++ tests/package-metadata.test.ts | 4 +- tests/site.test.ts | 17 +- 45 files changed, 944 insertions(+), 3160 deletions(-) delete mode 100644 examples/express-prisma/.env.example delete mode 100644 examples/express-prisma/schema.prisma delete mode 100644 examples/express-prisma/server.mjs create mode 100644 examples/local-runtime/index.mjs create mode 100644 site/favicon.svg delete mode 100644 src/http/app.module.ts delete mode 100644 src/http/gitdb.controller.ts delete mode 100644 src/http/gitdb.runtime.ts delete mode 100644 src/http/main.ts delete mode 100644 src/protocol/field.ts delete mode 100644 src/protocol/parameters.ts delete mode 100644 src/protocol/postgres-server.ts delete mode 100644 src/sql/catalog.ts delete mode 100644 tests/http-runtime.test.ts create mode 100644 tests/no-postgres-facade.test.ts diff --git a/.gitignore b/.gitignore index 4a80b7a..2bd7980 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,6 @@ dist .gitdb .gitdb-example-public .env -examples/express-prisma/.env -examples/express-prisma/generated .DS_Store coverage *.tsbuildinfo diff --git a/Dockerfile b/Dockerfile index 053d33d..6248aec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,12 +13,7 @@ RUN pnpm build FROM node:20-alpine AS runtime WORKDIR /app ENV NODE_ENV=production -ENV GITDB_HOST=0.0.0.0 -ENV GITDB_PORT=7432 -ENV PORT=3000 COPY package.json pnpm-lock.yaml ./ RUN corepack enable && corepack prepare pnpm@10.33.0 --activate && pnpm install --prod --frozen-lockfile COPY --from=build /app/dist ./dist -EXPOSE 3000 -EXPOSE 7432 -CMD ["node", "dist/src/http/main.js"] +CMD ["node", "dist/src/cli/main.js", "check"] diff --git a/README.md b/README.md index cec129b..65f6de9 100644 --- a/README.md +++ b/README.md @@ -2,70 +2,51 @@ English | [한국어](docs/README.ko.md) | [Website](https://3x-haust.github.io/gitdb/) -GitDB turns a GitHub repository into a project-scoped database runtime. - -The database hot path is local: GitDB opens a runtime process, executes queries -in its SQL engine, serializes writes through a transaction executor, and persists -state as a manifest, mutation log, and visible snapshots. GitHub is the durable -and auditable storage layer, not the place GitDB visits for every `SELECT`. - -GitDB now exposes two application entry points: a first-party TypeORM-style -`DataSource`/repository API for new apps, and a PostgreSQL-compatible local TCP -facade for tools such as Prisma and `pg`. +GitDB is a GitHub-backed database runtime for project data. It keeps the hot path +local, persists mutations through a manifest-gated log, and uses a GitHub +repository as durable storage plus an audit trail. ```text -GitDB ORM / Prisma / pg - | - | postgresql://127.0.0.1:7432/main - v -GitDB local runtime - | - | transaction executor + SQL engine + manifest/log replay - v -GitHub repository +App code + -> GitDB DataSource / Repository + -> Local SQL engine + transaction queue + -> Manifest, mutation log, visible snapshots + -> GitHub repository for durable history ``` -GitDB is not SQLite-over-GitHub, and it does not upload `.db` files. The -PostgreSQL facade is only a compatibility layer; the storage-engine work is the -core of the project. +GitDB is not a single database file uploaded to a repo. The important parts are +the storage engine, transaction boundary, replayable log, snapshot model, and the +first-party API that lets apps use those pieces directly. ## Why GitDB -GitHub already gives small teams commits, pull requests, history, branching, -review, public visibility, private repos, and access control. GitDB uses those -primitives for project data. - -Use it when you want: +Use GitDB when you want: - A database repository per project, for example `my-app-db` -- A first-party repository API for typed CRUD without adopting a separate ORM -- PostgreSQL-compatible access for existing Prisma, `pg`, or raw SQL clients -- Public data that can be inspected and edited from GitHub's web UI -- Encrypted data in public or private repositories -- Auditable commits for agent memory, demos, content tools, config tools, and - low-frequency app data +- Local query execution without per-query network round-trips +- A TypeORM-style `DataSource` and repository API included in the package +- Public table snapshots that can be inspected from GitHub when plaintext mode is intentional +- Encrypted manifest and mutation logs for private data +- Auditable commits for agents, demos, content tools, config tools, and low-frequency app data -Do not use it when you need high-throughput OLTP, low-latency multi-writer -transactions, or full PostgreSQL compatibility today. +GitDB is still experimental. Do not use it for high-throughput OLTP, low-latency +distributed writers, or workloads that require mature secondary indexes today. -## Features +## Current Surface | Area | Current behavior | | --- | --- | -| ORM access | First-party `DataSource`/repository API plus PostgreSQL-style local endpoint | -| SQL | `CREATE TABLE`, `INSERT`, `DELETE`, `SELECT`, joins, grouping, ordering, aggregates, and common raw-query flows | -| Runtime guarantees | Single-process transaction queue, manifest-gated log replay, and checkpointed visible snapshots | -| GitHub storage | Dedicated repository per database, created on first write when permissions allow | -| Public plaintext mode | `table/schema.json` and `table/data.json` are visible and editable in GitHub | -| Encrypted mode | AES-256-GCM encrypted manifest and mutation log files | -| Local mode | No GitHub variables required; data stays under a local root directory | -| Example app | Express + Prisma API using GitDB through the PostgreSQL facade | -| Benchmarks | Local, facade, and GitHub Contents API benchmark commands included | +| App API | `createGitDbDataSource`, `defineEntity`, and typed repositories | +| SQL engine | `CREATE TABLE`, `INSERT`, `DELETE`, `SELECT`, joins, grouping, ordering, and aggregates | +| Storage | Local encrypted, local plaintext, GitHub encrypted, and GitHub plaintext stores | +| Durability | Manifest-gated mutation log replay with visible snapshot checkpoints | +| CLI | `gitdb keygen`, `gitdb query`, and `gitdb check` | +| Example | First-party local runtime example under `examples/local-runtime` | +| Package | npm-ready metadata, exports, bin, pack dry-run, and publish dry-run scripts | ## Repository Layout -In plaintext public mode, GitDB writes both internal state and human-facing table -snapshots: +Plaintext mode writes internal state plus human-readable snapshots: ```text gitdb/v1/ @@ -89,7 +70,7 @@ gitdb/v1/ } ``` -`data.json` contains only rows: +`data.json` contains rows: ```json [ @@ -98,11 +79,7 @@ gitdb/v1/ ] ``` -That means a public database repository can be browsed like a lightweight -Firebase-style data console. Editing `data.json` in GitHub and committing the -change updates the visible table snapshot that GitDB restores on the next open. - -In encrypted mode, GitDB writes opaque files: +Encrypted mode writes opaque files: ```text gitdb/v1/ @@ -116,11 +93,11 @@ gitdb/v1/ Install and build: ```bash -pnpm install -pnpm build +corepack pnpm install +corepack pnpm build ``` -Use the first-party ORM-style repository API: +Use the first-party repository API: ```ts import { LocalPlaintextStore, createGitDbDataSource, defineEntity } from "@3xhaust/gitdb" @@ -148,168 +125,91 @@ await people.save({ id: "p1", name: "Lin", team_id: "storage" }) const storagePeople = await people.find({ where: { team_id: "storage" } }) ``` -Run the local PostgreSQL facade with encrypted local storage: - -```bash -export GITDB_KEY="$(node dist/src/cli/main.js keygen)" -pnpm start:facade -``` - -Connect with `psql`, `pg`, Prisma, or another PostgreSQL client: +Run the bundled example: ```bash -psql postgresql://127.0.0.1:7432/main +corepack pnpm example ``` -Example SQL: +It builds the package, opens a local plaintext store, writes `teams` and +`people`, runs a join, reopens the store, and prints a JSON summary. -```sql -CREATE TABLE teams (id STRING, name STRING); -CREATE TABLE people (id STRING, name STRING, team_id STRING); +## CLI -INSERT INTO teams VALUES ('t1', 'Storage'); -INSERT INTO people VALUES ('p1', 'Lin', 't1'); +Generate an encryption key: -SELECT people.name, teams.name AS team -FROM people -JOIN teams ON people.team_id = teams.id; +```bash +node dist/src/cli/main.js keygen ``` -## Express + Prisma Example - -The example is a real API shape: Express handles HTTP routes, Prisma talks to -GitDB through the PostgreSQL facade, and GitDB stores the data locally or in the -GitHub database repository configured by the example `.env`. +Check the configured store: ```bash -cp examples/express-prisma/.env.example examples/express-prisma/.env -pnpm example +GITDB_ENCRYPTION=off GITDB_ROOT=.gitdb node dist/src/cli/main.js check ``` -In another terminal: +Execute one SQL statement: ```bash -curl http://127.0.0.1:3090/health -curl -X POST http://127.0.0.1:3090/seed -curl http://127.0.0.1:3090/people +GITDB_ENCRYPTION=off GITDB_ROOT=.gitdb \ + node dist/src/cli/main.js query "CREATE TABLE people (id STRING, name STRING)" ``` -By default the example uses plaintext mode: +## Environment Model + +Local plaintext mode: ```env GITDB_ENCRYPTION=off -GITDB_ROOT=.gitdb-example-public -GITDB_GITHUB_OWNER=3x-haust -GITDB_GITHUB_REPO=gitdb-example-db -GITDB_GITHUB_BRANCH=main -GITDB_GITHUB_PREFIX=gitdb/v1 -GITDB_GITHUB_TOKEN= -API_PORT=3090 +GITDB_ROOT=.gitdb ``` -Leave `GITDB_GITHUB_TOKEN` empty to test against local files. Add a GitHub token -to write to the dedicated public database repository. The token needs Contents -read/write access to that database repository. If the repository does not exist, -the token also needs permission to create repositories for the owner. - -## Environment Model - -The root `.env` is for the GitDB facade process: +Local encrypted mode: ```env GITDB_ENCRYPTION=on GITDB_KEY=generated-by-gitdb-keygen GITDB_ROOT=.gitdb -GITDB_HOST=0.0.0.0 -GITDB_PORT=7432 -``` - -Application examples keep their own `.env` files because application settings -and database-repository settings should not leak into the package root. - -### Encryption - -`GITDB_KEY` must be a base64url-encoded 32-byte key generated by GitDB: - -```bash -node dist/src/cli/main.js keygen ``` -Keep it outside Git. If the key changes, previously encrypted data cannot be -decrypted. - -Use `GITDB_ENCRYPTION=off` only for intentional public demos where table names, -columns, and rows should be visible in GitHub. +GitHub-backed modes additionally use: -### GitHub Storage - -Set these variables in the process that runs the facade: - -```bash -export GITDB_GITHUB_OWNER="3x-haust" -export GITDB_GITHUB_REPO="my-project-db" -export GITDB_GITHUB_BRANCH="main" -export GITDB_GITHUB_PREFIX="gitdb/v1" -export GITDB_GITHUB_TOKEN="github_pat_... or ghp_..." -gitdb serve -``` - -Recommended pattern: - -- Source repo: `my-project` -- Database repo: `my-project-db` -- Public demo data: `GITDB_ENCRYPTION=off` -- Real public or private data: `GITDB_ENCRYPTION=on` - -## Runtime And Trust Model - -`gitdb serve` and a hosted GitDB endpoint are the same kind of runtime: -PostgreSQL facade, SQL engine, and GitHub sync. The important question is where -that runtime runs. - -| Mode | Runtime location | Who can decrypt? | Best for | -| --- | --- | --- | --- | -| Self-hosted encrypted | Your app server, VPS, local machine, or private infra | Only the environment holding `GITDB_KEY` | Real app data, public encrypted repos, private repos | -| Hosted plaintext | GitDB hosted runtime such as `gitdb.3xhaust.dev` | Everyone can read the GitHub repo anyway | Public demos, public datasets, inspectable examples | -| Hosted encrypted | GitDB hosted runtime | The hosted runtime must receive/use the key | Managed convenience mode, not zero-knowledge | - -If you want encrypted data where only your service can decrypt it, run GitDB -yourself: - -```text -Your App -> your gitdb serve -> encrypted GitHub repo +```env +GITDB_GITHUB_OWNER=3x-haust +GITDB_GITHUB_REPO=my-project-db +GITDB_GITHUB_BRANCH=main +GITDB_GITHUB_PREFIX=gitdb/v1 +GITDB_GITHUB_TOKEN=github_token_with_contents_write_access ``` -Do not send `GITDB_KEY` to a hosted runtime unless you intentionally choose a -managed mode where that runtime is trusted to process plaintext query results. +Leave `GITDB_GITHUB_TOKEN` blank for local-only development. Use +`GITDB_ENCRYPTION=off` only for intentional public demos where table names, +columns, and rows should be visible. ## Architecture GitDB is split into four layers: -1. First-party ORM API +1. First-party API - Provides `DataSource`, `Repository`, `save`, `find`, `findOne`, `delete`, - and explicit transaction access. - - Keeps new apps close to GitDB's runtime instead of forcing a PostgreSQL - compatibility hop. - -2. PostgreSQL-compatible facade - - Opens a local TCP endpoint for existing clients. - - Lets Prisma, `pg`, and raw SQL clients use a normal PostgreSQL connection - string. - -3. SQL engine - - Owns schema, mutation execution, query execution, joins, grouping, and - result rows. - - Serializes local write transactions before persistence. - - Targets the PostgreSQL subset produced by common Node.js clients and ORM - raw-query flows. - -4. Storage providers - - Local encrypted store for development and tests. + raw `query`, and explicit transaction access. + - Keeps new apps directly on GitDB's runtime surface. + +2. SQL engine + - Owns schema, mutation execution, query execution, joins, grouping, ordering, + and result rows. + - Serializes local mutations before persistence. + +3. Storage providers + - Local encrypted store for development and private local data. - Local plaintext store for visible snapshots. - GitHub encrypted/plaintext stores for remote durability. +4. Audit and recovery model + - Manifest state records the committed sequence. + - Mutation logs are replayable on open. + - Visible snapshots accelerate plaintext reopen paths. + See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for more detail. ## Benchmarks @@ -317,64 +217,43 @@ See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for more detail. Run local benchmarks: ```bash -pnpm benchmark +corepack pnpm benchmark ``` -Compare the current runtime with the previous documented run and refresh the -website evidence: +Refresh website benchmark evidence: ```bash -GITDB_BENCH_ROWS=250 pnpm benchmark:compare +GITDB_BENCH_ROWS=250 corepack pnpm benchmark:site ``` -Run the GitHub write benchmark: +Compare the current runtime with the previous documented local run: ```bash -GITDB_BENCH_GITHUB_ROWS=2 pnpm benchmark:github +GITDB_BENCH_ROWS=250 corepack pnpm benchmark:compare ``` Latest measured run: | Scenario | Rows | Write ms | Writes/s | Join ms | Reopen ms | | --- | ---: | ---: | ---: | ---: | ---: | -| local plaintext throttled visible snapshots | 250 | 97.55 | 2562.73 | 3.28 | 47.42 | -| local encrypted mutation log | 250 | 97.23 | 2571.34 | 0.87 | 45.26 | -| postgres facade over local encrypted | 250 | 148.46 | 1683.91 | 2.31 | 0.00 | - -Compared with the previous documented run, local plaintext writes improved from -173.14 writes/s to 2562.73 writes/s, or 14.80x. Local encrypted writes improved -from 328.09 writes/s to 2571.34 writes/s, or 7.84x. +| local plaintext throttled visible snapshots | 250 | 146.75 | 1703.61 | 4.88 | 77.62 | +| local encrypted mutation log | 250 | 234.37 | 1066.70 | 1.18 | 78.31 | +| orm local plaintext | 250 | 5377.34 | 46.49 | 1.30 | 136.29 | -Interpretation: local execution is usable for examples, demos, and low-frequency -workloads. Single-statement mutations no longer clone the whole in-memory -database before every write; persistence failures restore committed manifest -state. Direct per-mutation GitHub Contents API writes are still too slow for the -hot path. WAL, indexes, compaction, and batched Git commits remain -storage-engine work, not current full-OLTP guarantees. +Interpretation: raw local execution is already usable for examples, demos, and +low-frequency project data. Repository `save()` is intentionally safer but much +slower today because every row is written through a small transaction. The next +performance work is storage-shaped: batch repository writes, add page-level +snapshots, add indexes, compact logs, and sync Git commits in batches. See [docs/BENCHMARKS.md](docs/BENCHMARKS.md). -## Performance Roadmap - -The next performance work is storage-shaped, not facade-shaped: - -- Local WAL first: return success after local durable write in `fast` mode -- Batched Git commits: replace repeated Contents API writes with Git Database - tree commits -- Snapshot throttling: refresh visible `data.json` on intervals or explicit - sync, not every mutation -- Chunked table pages: avoid rewriting a whole table for one row change -- Local primary and secondary indexes: keep joins and filters off the GitHub hot - path -- Manifest versions: skip unchanged table reads on cold start -- Strong mode: optionally block until the GitHub commit lands - ## Security Model Encrypted mode protects manifest and mutation log contents with AES-256-GCM. Keys are never stored in the repository. -Public GitHub repositories can still reveal metadata: +Public repositories can still reveal metadata: - Commit time - File count @@ -387,8 +266,8 @@ storage versions. ## Current Limitations - SQL support is intentionally limited to the subset GitDB currently executes. -- PostgreSQL catalog emulation is not complete. -- Multi-process writers are guarded by GitHub state, but this is not yet a +- Repository `save()` is not optimized for bulk inserts yet. +- Multi-process writers are guarded by remote state, but this is not yet a high-concurrency OLTP database. - GitHub Contents API mode is useful for demos and correctness testing, not production write throughput. @@ -399,45 +278,16 @@ Unsupported SQL should fail explicitly instead of pretending to work. ## Commands ```bash -pnpm check -pnpm test -pnpm build -pnpm benchmark -pnpm benchmark:evaluate -pnpm pack:dry-run -pnpm publish:dry-run -pnpm start:facade -pnpm example -``` - -## Deployment - -The deployable service is a NestJS HTTP control plane plus the -PostgreSQL-compatible facade in the same process: - -```bash -docker build -t gitdb . -docker run -p 3000:3000 -p 7432:7432 --env-file .env gitdb +corepack pnpm check +corepack pnpm test +corepack pnpm build +corepack pnpm benchmark +corepack pnpm benchmark:evaluate +corepack pnpm pack:dry-run +corepack pnpm publish:dry-run +corepack pnpm example ``` -`pnpm start` runs the HTTP control plane. `pnpm start:facade` runs only the TCP -facade for local ORM testing. - -The current public HTTP control plane is deployed at: - -```text -https://gitdb.3xhaust.dev/health -``` - -`gitdb.3xhaust.dev` should be treated as a hosted GitDB runtime/control-plane -instance. It is useful for public plaintext workflows, demos, setup flows, and -future managed modes. For encrypted data where only your service may decrypt the -database, run `gitdb serve` in your own environment and keep `GITDB_KEY` there. - -HTTP deployment does not automatically expose the TCP facade to external ORM -clients. For remote ORM access, run `gitdb serve` near the application or deploy -to an environment that exposes TCP port `7432`. - ## License MIT diff --git a/biome.json b/biome.json index 0359bcb..dc624d6 100644 --- a/biome.json +++ b/biome.json @@ -44,8 +44,9 @@ "!dist", "!coverage", "!.gitdb-example-public", - "!.omx", - "!examples/express-prisma/generated" + "!.claude", + "!.omc", + "!.omx" ] }, "overrides": [ diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 08ac519..a4bc200 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,83 +1,116 @@ -# Architecture +# GitDB Architecture -GitDB has five layers. +GitDB is a first-party database runtime that uses a GitHub repository as durable +storage and audit history. Applications use the GitDB API directly; query +execution and write ordering stay local. -## First-Party ORM API +## Layers -New applications can use GitDB directly through `createGitDbDataSource`, -`defineEntity`, and repository methods such as `save`, `find`, `findOne`, and -`delete`. This is intentionally closer to TypeORM's `DataSource` and repository -shape than to a PostgreSQL-only facade. +### First-Party API -The ORM API still executes through the same local runtime and transaction -executor as raw SQL. It is a convenience surface over the storage engine, not a -separate persistence path. +`createGitDbDataSource` opens a runtime over a selected store. `defineEntity` +declares table metadata, and `GitDbRepository` provides `save`, `find`, +`findOne`, and `delete`. -## PostgreSQL Facade +The API intentionally mirrors familiar `DataSource` and repository shapes while +keeping callers close to GitDB's own transaction and storage model. -`gitdb serve` starts a PostgreSQL-compatible TCP server. Existing ORMs keep their -normal PostgreSQL provider and point their connection string at GitDB. +### SQL Engine -The facade handles startup, simple queries, prepared parse/bind/execute flows, -row descriptions, data rows, and command completion messages. +`GitDbEngine` owns local execution. It handles schema mutations, row mutations, +queries, joins, grouping, ordering, aggregate results, transaction execution, and +manifest updates. -## SQL Engine +Single-statement mutations are serialized through an internal queue. Explicit +multi-statement work uses `engine.transaction()`, runs on an isolated database +copy, and persists only after the callback succeeds. -The current engine uses `alasql` for advanced SQL execution, including joins, -grouping, ordering, and aggregates. PostgreSQL-flavored SQL is normalized before -execution so common ORM-generated SQL can run. +### Storage Providers -Unsupported PostgreSQL features return explicit errors instead of falling back -silently. +GitDB stores committed state through the `GitDbStore` interface: -Mutations are serialized through a single-engine transaction queue. A -single-statement mutation uses the hot local database directly, then persists the -mutation segment and manifest. If persistence fails, the engine restores the last -committed manifest state before returning the error. Explicit multi-statement -transactions still run against an isolated in-memory database clone and swap the -live database only after all segments and the manifest persist successfully. The -manifest is updated in memory only after the durable manifest write succeeds, so -a later mutation cannot publish orphan segments from a failed transaction. +- `LocalEncryptedStore` +- `LocalPlaintextStore` +- `GitHubEncryptedStore` +- `GitHubPlaintextStore` -## Storage Engine +Stores persist the manifest and mutation log. Plaintext stores can also write +visible table snapshots for inspection and faster reopen paths. -GitDB persists schema and data changes as mutation segments plus a manifest: +### Recovery Boundary -```text -gitdb/v1/manifest.json or manifest.enc -gitdb/v1/log/.json or .enc +The manifest sequence is the committed boundary. On open, GitDB restores a +matching visible snapshot when one exists; otherwise it replays log segments in +manifest order. + +If persistence fails after a local mutation, the engine restores the previous +committed manifest state before returning the error. + +## Transaction Model + +GitDB currently provides a single-process write queue. It prevents in-process +write interleaving and makes commit order explicit. + +The current model does not claim mature distributed concurrency. Multi-process +writers still need stronger remote locking or compare-and-swap semantics before +GitDB can be treated as a high-concurrency OLTP system. + +## Snapshot Policy + +Visible snapshots can be written on every mutation or at an interval: + +```ts +await GitDbEngine.open({ + snapshotPolicy: { mode: "interval", mutations: 100 }, + store, +}) ``` -On startup, the engine restores a visible snapshot when its checkpoint sequence -matches the manifest. Otherwise it reads the manifest and replays the mutation -log into the hot query engine. This avoids GitHub round-trips during query -execution. +Interval snapshots reduce write amplification in plaintext mode. The mutation +log remains the source of truth between checkpoints. + +## App Runtime Example + +```ts +const Person = defineEntity({ + tableName: "people", + primaryKey: "id", + columns: { id: "STRING", name: "STRING", team_id: "STRING" }, +}) + +const dataSource = await createGitDbDataSource({ + entities: [Person], + store: new LocalPlaintextStore({ root: ".gitdb" }), + synchronize: true, +}) + +await dataSource.getRepository(Person).save({ + id: "p1", + name: "Lin", + team_id: "storage", +}) +``` -Plaintext visible snapshots are dashboard artifacts and cold-start checkpoints. -They can be written every mutation for maximum visibility or throttled by -mutation interval for faster local workloads. Snapshot table files are written -before `snapshot.json`; the checkpoint is the final marker that lets the engine -trust a visible snapshot. Once a manifest has committed mutations, visible files -without a checkpoint are ignored and the mutation log is replayed instead. +## Deployment Shape -## GitHub Provider +GitDB is packaged as a library and CLI. A production application should open a +store inside its own process, keep `GITDB_KEY` in that process environment when +encrypted mode is enabled, and choose whether remote GitHub sync is local-only, +background, or blocking for the workflow. -The GitHub provider uses the Repository Contents API to read and create/update -encrypted segment files. Existing file `sha` values are used for optimistic -concurrency when updating files. +The Dockerfile builds the package and defaults to `gitdb check`, which verifies +that the configured store can be opened. -GitHub is the durable sync layer. Query execution stays local for acceptable -latency. +## Performance Direction -Public deployments should write encrypted database objects to a branch that is -separate from the application branch. The current release uses `main` for source -code and `data` for encrypted `gitdb/v1` objects. This keeps public storage -opaque while avoiding auto-deploy loops from database writes. +The current hot path is local. The next work is storage-engine work: -## Deployment Boundary +- Batch repository writes into one transaction +- Add page-level visible snapshots instead of whole-table rewrites +- Add primary and secondary indexes +- Compact mutation logs +- Batch Git object writes instead of writing one Contents API object per mutation +- Add explicit `fast` and `strong` durability modes -`pnpm start` launches the HTTP control plane and the PostgreSQL-compatible TCP -facade in one process. HTTP deployments can verify readiness through `/` or -`/health`. ORM clients still need TCP access to the facade port. When the deploy -target only exposes HTTP ingress, run `gitdb serve` near the ORM process and use -GitHub as the shared encrypted durability layer. +This keeps GitDB differentiated as a repository-backed runtime instead of a +network adapter over files. diff --git a/docs/BENCHMARKS.md b/docs/BENCHMARKS.md index 0eca8b6..b950559 100644 --- a/docs/BENCHMARKS.md +++ b/docs/BENCHMARKS.md @@ -1,97 +1,80 @@ # GitDB Benchmarks -Benchmarks are intentionally split between local engine speed and GitHub sync +Benchmarks are split between raw local engine speed and first-party repository speed. GitDB should feel fast because queries run against the local SQL engine; GitHub is the durable sync layer, not the per-query hot path. ## Environment - Date: 2026-06-14 KST -- Machine: macOS 26.5.1 arm64, Node.js 24.11.0 -- Command: `GITDB_BENCH_ROWS=250 pnpm benchmark:compare` -- JSON evaluator: `GITDB_BENCH_OUTPUT=.gitdb/bench-current.json pnpm benchmark:evaluate` -- Baseline: `HEAD~1:docs/BENCHMARKS.md` -- Site evidence: `site/benchmark.json` +- Machine: macOS arm64 +- Command: `GITDB_BENCH_ROWS=250 corepack pnpm benchmark:compare` - Workload: create `teams` and `people`, insert rows, execute a join, reopen the store where applicable. -## Current Local Results +## Current Results | Scenario | Rows | Write ms | Writes/s | Join ms | Reopen ms | | --- | ---: | ---: | ---: | ---: | ---: | -| local plaintext throttled visible snapshots | 250 | 97.55 | 2562.73 | 3.28 | 47.42 | -| local encrypted mutation log | 250 | 97.23 | 2571.34 | 0.87 | 45.26 | -| postgres facade over local encrypted | 250 | 148.46 | 1683.91 | 2.31 | 0.00 | +| local plaintext throttled visible snapshots | 250 | 146.75 | 1703.61 | 4.88 | 77.62 | +| local encrypted mutation log | 250 | 234.37 | 1066.70 | 1.18 | 78.31 | +| orm local plaintext | 250 | 5377.34 | 46.49 | 1.30 | 136.29 | -## Previous-Version Comparison +## Previous Local Run -| Scenario | Previous writes/s | Current writes/s | Change | Write ms change | Join ms change | -| --- | ---: | ---: | ---: | ---: | ---: | -| local plaintext throttled visible snapshots | 173.14 | 2562.73 | +1380.15% | +93.24% | +90.04% | -| local encrypted mutation log | 328.09 | 2571.34 | +683.73% | +87.24% | +79.91% | -| postgres facade over local encrypted | 253.05 | 1683.91 | +565.45% | +84.97% | +88.30% | +The previous documented local engine results were: -## Historical GitHub Contents API +| Scenario | Previous writes/s | Current writes/s | Change | +| --- | ---: | ---: | ---: | +| local plaintext visible snapshots | 173.14 | 1703.61 | +883.95% | +| local encrypted mutation log | 328.09 | 1066.70 | +225.12% | -This was not rerun during the local performance pass. It remains the historical -remote-write reference from 2026-06-09 KST: +The comparison is intentionally limited to the local engine scenarios that still +exist in the first-party runtime. Retired network-server numbers are no longer +part of the public benchmark surface. -| Scenario | Rows | Write ms | Writes/s | Join ms | Reopen ms | -| --- | ---: | ---: | ---: | ---: | ---: | -| github plaintext contents api | 2 | 14157.49 | 0.14 | 6.70 | 1812.62 | +## ORM Overhead -## Interpretation +The raw engine inserts rows with direct SQL statements. `GitDbRepository.save()` +is safer but slower because it runs an upsert-style flow: -Local writes are now fast enough for examples, demos, and low-frequency project -data. The hot-path improvement is storage-engine shaped: a single-statement -mutation no longer clones and restores the whole in-memory database before every -write. It mutates the live local engine, persists through the manifest-gated log, -and restores committed state if persistence fails. Explicit multi-statement -transactions still use an isolated transaction database for rollback. +1. `DELETE FROM table WHERE pk = ?` +2. `INSERT INTO table (...) VALUES (...)` +3. Both statements inside a transaction -The first-party ORM API avoids the PostgreSQL wire hop for new apps, while the -facade remains useful for existing clients. Visible snapshots are checkpointed -instead of being rewritten on every local plaintext mutation. +For bulk inserts today, prefer `dataSource.query()` with direct SQL. The roadmap +adds `repository.insert()` and `repository.saveMany()` so repository code can +batch rows without paying one transaction per row. -GitHub Contents API writes are not acceptable as the hot write path. The current -GitHub plaintext mode writes multiple files per SQL mutation: log segment, -manifest, visible schema, and visible data. During benchmarking, GitHub also -returned transient 500/502 responses under repeated Contents API writes. GitDB -now retries those transient write failures, but the result confirms that direct -per-mutation GitHub writes are a demo path, not the final architecture. +## Historical GitHub Contents API -## Performance Plan +GitHub Contents API writes are not acceptable as the hot write path. GitDB keeps +query execution local and treats GitHub as remote durable storage and reviewable +history. Direct per-mutation remote writes are useful for demos and correctness +testing, not for high-throughput writes. -1. Add a local write buffer. - Return success after local WAL fsync in `fast` mode, then sync GitHub in the - background. +## Performance Plan -2. Batch GitHub commits. - Replace per-file Contents API writes with Git Database tree commits, so one - transaction can update log, manifest, indexes, and visible snapshots in a - single Git commit. +1. Add batch repository writes. + `saveMany()` should persist many entities through one transaction. -3. Snapshot less often. - Keep mutation logs per write, but refresh visible `table/data.json` on a - timer, row threshold, or explicit `gitdb sync` command. +2. Add page-level visible snapshots. + Plaintext mode should update changed pages instead of rewriting whole tables. -4. Write table deltas. - Store public rows as `table/rows/.json` or chunked pages, then - rebuild `data.json` as a dashboard artifact. This avoids rewriting the whole - table for every insert. +3. Add indexes. + Maintain primary and secondary indexes in the local runtime so joins and + filters avoid full scans. -5. Add local indexes. - Maintain primary and secondary indexes in local cache so joins and filters do - not scan every visible row. +4. Compact logs. + Merge old mutation segments into periodic checkpoints. -6. Add cold-start manifests. - Include table snapshot versions in `manifest.json` so startup can skip - directory listing and unchanged table reads. +5. Batch Git sync. + Replace repeated Contents API writes with Git object tree commits. -7. Separate durability modes. +6. Add durability modes. Keep `fast` as local-durable/background-GitHub and add `strong` for blocking - until the GitHub commit lands. + until the remote commit lands. -8. Add benchmark gates. - Track local 1k/10k row writes, joins, reopen time, GitHub 10-row sync, and - GitHub batch sync once tree commits land. +7. Add benchmark gates. + Track local 1k/10k row writes, repository bulk vs single-row writes, joins, + reopen time, and remote batch sync once tree commits land. diff --git a/docs/README.ko.md b/docs/README.ko.md index 2b1d819..14d78f9 100644 --- a/docs/README.ko.md +++ b/docs/README.ko.md @@ -2,74 +2,51 @@ [English](../README.md) | 한국어 | [Website](https://3x-haust.github.io/gitdb/) -GitDB는 GitHub repository 하나를 프로젝트 전용 데이터베이스 runtime으로 쓰게 -해주는 GitHub-native database입니다. - -query hot path는 로컬입니다. GitDB runtime이 SQL engine에서 query를 실행하고, -write는 transaction executor를 통해 직렬화한 뒤 manifest, mutation log, visible -snapshot으로 상태를 저장합니다. GitHub는 매 `SELECT`마다 접근하는 곳이 아니라 -durable/auditable storage layer입니다. - -애플리케이션은 GitDB의 first-party TypeORM 스타일 `DataSource`/repository API를 -직접 쓰거나, Prisma와 `pg` 같은 기존 PostgreSQL client용 local TCP facade에 -접속할 수 있습니다. +GitDB는 프로젝트 데이터를 위한 GitHub 기반 데이터베이스 런타임입니다. 쿼리와 +쓰기의 hot path는 로컬 엔진에서 처리하고, GitHub 저장소는 durable storage와 +audit trail로 사용합니다. ```text -GitDB ORM / Prisma / pg - | - | postgresql://127.0.0.1:7432/main - v -GitDB local runtime - | - | transaction executor + SQL engine + manifest/log replay - v -GitHub repository +Application code + -> GitDB DataSource / Repository + -> Local SQL engine + transaction queue + -> Manifest, mutation log, visible snapshots + -> GitHub repository for durable history ``` -GitDB는 SQLite를 GitHub에 올리는 도구가 아닙니다. `.db` 파일을 업로드하지 -않습니다. PostgreSQL facade는 호환 레이어이고, 핵심은 storage engine과 local -runtime입니다. +GitDB는 데이터베이스 파일 하나를 GitHub에 올리는 구조가 아닙니다. 핵심은 +storage engine, transaction boundary, replay 가능한 mutation log, snapshot +모델, 그리고 그 위에 직접 붙는 first-party API입니다. ## 왜 GitDB인가 -GitHub에는 이미 commit, pull request, history, branch, review, public/private -repo, access control이 있습니다. GitDB는 이 GitHub primitive를 프로젝트 데이터에 -적용합니다. - -GitDB가 잘 맞는 경우: +다음 상황에 적합합니다: -- 프로젝트마다 전용 데이터베이스 repo를 두고 싶을 때, 예: `my-app-db` -- 별도 ORM provider 없이 typed repository API를 쓰고 싶을 때 -- Prisma, `pg`, raw SQL client를 PostgreSQL 호환 endpoint로 연결하고 싶을 때 -- public demo 데이터를 GitHub 웹 UI에서 바로 보고 수정하고 싶을 때 -- public/private repo에 데이터를 암호화해서 저장하고 싶을 때 -- agent memory, demo, content tool, config tool, 저빈도 앱 데이터를 commit - history로 남기고 싶을 때 +- 프로젝트마다 별도 database repository를 두고 싶을 때 +- 매 쿼리마다 네트워크 왕복을 만들지 않고 로컬에서 실행하고 싶을 때 +- 패키지에 포함된 TypeORM 스타일 `DataSource`와 repository API가 필요할 때 +- 의도적인 public demo에서 table snapshot을 GitHub에서 바로 보고 싶을 때 +- private data를 encrypted manifest와 mutation log로 저장하고 싶을 때 +- agent memory, demo, content tool, config tool처럼 감사 가능한 기록이 필요한 저빈도 데이터 -GitDB가 맞지 않는 경우: +GitDB는 아직 실험 프로젝트입니다. 높은 처리량의 OLTP, 짧은 지연 시간의 다중 +writer, 성숙한 secondary index가 필요한 워크로드에는 아직 적합하지 않습니다. -- 고빈도 OLTP -- 낮은 지연시간의 다중 writer transaction -- 오늘 당장 완전한 PostgreSQL 호환성이 필요한 서비스 - -## 기능 +## 현재 표면 | 영역 | 현재 동작 | | --- | --- | -| ORM 접근 | First-party `DataSource`/repository API와 PostgreSQL 스타일 local endpoint | -| SQL | `CREATE TABLE`, `INSERT`, `DELETE`, `SELECT`, join, group, order, aggregate, 일반적인 raw query 흐름 | -| Runtime 보장 | single-process transaction queue, manifest-gated log replay, checkpointed visible snapshot | -| GitHub 저장소 | 데이터베이스마다 전용 repo 사용, 권한이 있으면 최초 write 시 repo 생성 | -| Public plaintext mode | `table/schema.json`, `table/data.json`을 GitHub에서 직접 확인/수정 | -| Encrypted mode | AES-256-GCM으로 암호화된 manifest/log 저장 | -| Local mode | GitHub 변수 없이 로컬 디렉터리에 저장 | -| Example app | Express + Prisma API가 PostgreSQL facade를 통해 GitDB 사용 | -| Benchmark | local, facade, GitHub Contents API 벤치마크 명령 포함 | +| App API | `createGitDbDataSource`, `defineEntity`, typed repository | +| SQL engine | `CREATE TABLE`, `INSERT`, `DELETE`, `SELECT`, join, grouping, ordering, aggregate | +| Storage | local encrypted, local plaintext, GitHub encrypted, GitHub plaintext | +| Durability | manifest-gated mutation log replay와 visible snapshot checkpoint | +| CLI | `gitdb keygen`, `gitdb query`, `gitdb check` | +| Example | `examples/local-runtime`의 first-party local runtime 예제 | +| Package | npm exports, bin, pack dry-run, publish dry-run 설정 | -## GitHub에 저장되는 구조 +## 저장소 구조 -plaintext public mode에서는 내부 mutation log와 사람이 보기 쉬운 table snapshot을 -같이 저장합니다. +Plaintext mode는 내부 상태와 사람이 읽을 수 있는 snapshot을 함께 씁니다: ```text gitdb/v1/ @@ -84,29 +61,7 @@ gitdb/v1/ 00000000000000000001.json ``` -`schema.json`에는 schema만 들어갑니다. - -```json -{ - "name": "people", - "columns": ["id", "name", "team_id"] -} -``` - -`data.json`에는 row만 들어갑니다. - -```json -[ - { "id": "p1", "name": "Lin", "team_id": "t1" }, - { "id": "p2", "name": "Ada", "team_id": "t2" } -] -``` - -그래서 public database repo는 가벼운 Firebase 콘솔처럼 확인할 수 있습니다. -GitHub 웹에서 `data.json`을 수정하고 commit하면, 다음 GitDB open 시 visible table -snapshot에서 데이터를 복원합니다. - -encrypted mode에서는 opaque file만 저장합니다. +Encrypted mode는 opaque 파일을 씁니다: ```text gitdb/v1/ @@ -117,14 +72,12 @@ gitdb/v1/ ## 빠른 시작 -설치와 빌드: - ```bash -pnpm install -pnpm build +corepack pnpm install +corepack pnpm build ``` -first-party ORM 스타일 repository API 사용: +First-party repository API: ```ts import { LocalPlaintextStore, createGitDbDataSource, defineEntity } from "@3xhaust/gitdb" @@ -152,291 +105,149 @@ await people.save({ id: "p1", name: "Lin", team_id: "storage" }) const storagePeople = await people.find({ where: { team_id: "storage" } }) ``` -encrypted local storage로 PostgreSQL facade 실행: +예제 실행: ```bash -export GITDB_KEY="$(node dist/src/cli/main.js keygen)" -pnpm start:facade +corepack pnpm example ``` -`psql`, `pg`, Prisma 등 PostgreSQL client로 접속: +예제는 local plaintext store를 열고, `teams`와 `people`을 쓰고, join을 실행한 뒤 +store를 다시 열어 JSON summary를 출력합니다. -```bash -psql postgresql://127.0.0.1:7432/main -``` - -예시 SQL: +## CLI -```sql -CREATE TABLE teams (id STRING, name STRING); -CREATE TABLE people (id STRING, name STRING, team_id STRING); +Encryption key 생성: -INSERT INTO teams VALUES ('t1', 'Storage'); -INSERT INTO people VALUES ('p1', 'Lin', 't1'); - -SELECT people.name, teams.name AS team -FROM people -JOIN teams ON people.team_id = teams.id; +```bash +node dist/src/cli/main.js keygen ``` -## Express + Prisma 예제 - -예제는 실제 API 서비스 형태입니다. Express가 HTTP route를 제공하고, Prisma는 -PostgreSQL facade를 통해 GitDB에 접속하며, GitDB는 example `.env`에 설정된 로컬 -디렉터리 또는 GitHub database repo에 저장합니다. +Store 확인: ```bash -cp examples/express-prisma/.env.example examples/express-prisma/.env -pnpm example +GITDB_ENCRYPTION=off GITDB_ROOT=.gitdb node dist/src/cli/main.js check ``` -다른 터미널에서: +SQL 한 문장 실행: ```bash -curl http://127.0.0.1:3090/health -curl -X POST http://127.0.0.1:3090/seed -curl http://127.0.0.1:3090/people +GITDB_ENCRYPTION=off GITDB_ROOT=.gitdb \ + node dist/src/cli/main.js query "CREATE TABLE people (id STRING, name STRING)" ``` -example은 기본적으로 plaintext mode입니다. +## 환경 변수 + +Local plaintext mode: ```env GITDB_ENCRYPTION=off -GITDB_ROOT=.gitdb-example-public -GITDB_GITHUB_OWNER=3x-haust -GITDB_GITHUB_REPO=gitdb-example-db -GITDB_GITHUB_BRANCH=main -GITDB_GITHUB_PREFIX=gitdb/v1 -GITDB_GITHUB_TOKEN= -API_PORT=3090 +GITDB_ROOT=.gitdb ``` -`GITDB_GITHUB_TOKEN`을 비워두면 로컬 파일로 테스트합니다. GitHub에 실제로 쓰고 -싶으면 dedicated database repo에 Contents read/write 권한이 있는 token을 넣으면 -됩니다. repo가 아직 없으면 owner 아래 repo를 생성할 권한도 필요합니다. - -## 환경 변수 모델 - -루트 `.env`는 GitDB facade process용입니다. +Local encrypted mode: ```env GITDB_ENCRYPTION=on GITDB_KEY=generated-by-gitdb-keygen GITDB_ROOT=.gitdb -GITDB_HOST=0.0.0.0 -GITDB_PORT=7432 -``` - -example은 각자 별도의 `.env`를 둡니다. app 설정과 database-repository 설정이 -package root에 섞이지 않게 하기 위해서입니다. - -### 암호화 - -`GITDB_KEY`는 GitDB가 생성한 base64url 32-byte key여야 합니다. - -```bash -node dist/src/cli/main.js keygen -``` - -이 값은 Git에 올리면 안 됩니다. key가 바뀌면 기존 encrypted data를 복호화할 수 -없습니다. - -`GITDB_ENCRYPTION=off`는 table name, column, row를 GitHub에서 공개적으로 보고 -싶은 demo에서만 사용하세요. - -### GitHub storage - -facade를 실행하는 process에 아래 변수를 넣습니다. - -```bash -export GITDB_GITHUB_OWNER="3x-haust" -export GITDB_GITHUB_REPO="my-project-db" -export GITDB_GITHUB_BRANCH="main" -export GITDB_GITHUB_PREFIX="gitdb/v1" -export GITDB_GITHUB_TOKEN="github_pat_... or ghp_..." -gitdb serve ``` -권장 구조: - -- source repo: `my-project` -- database repo: `my-project-db` -- 공개 demo data: `GITDB_ENCRYPTION=off` -- 실제 public/private data: `GITDB_ENCRYPTION=on` - -## Runtime과 신뢰 모델 - -`gitdb serve`와 hosted GitDB endpoint는 같은 종류의 runtime입니다. 둘 다 -PostgreSQL facade, SQL engine, GitHub sync를 담당합니다. 중요한 차이는 그 runtime이 -어디에서 실행되느냐입니다. - -| Mode | Runtime 위치 | 누가 복호화 가능한가 | 적합한 경우 | -| --- | --- | --- | --- | -| Self-hosted encrypted | 유저 앱 서버, VPS, 로컬, private infra | `GITDB_KEY`를 가진 유저 환경만 | 실제 앱 데이터, public encrypted repo, private repo | -| Hosted plaintext | `gitdb.3xhaust.dev` 같은 GitDB hosted runtime | 어차피 GitHub repo를 보는 모두 | public demo, public dataset, inspectable example | -| Hosted encrypted | GitDB hosted runtime | hosted runtime이 key를 받아야 함 | 편의성을 위한 managed mode, zero-knowledge 아님 | +GitHub 저장소를 쓸 때는 아래 값을 추가합니다: -암호화된 데이터를 “내 서비스만 복호화 가능”하게 만들고 싶다면 GitDB를 직접 -호스트해야 합니다. - -```text -Your App -> your gitdb serve -> encrypted GitHub repo +```env +GITDB_GITHUB_OWNER=3x-haust +GITDB_GITHUB_REPO=my-project-db +GITDB_GITHUB_BRANCH=main +GITDB_GITHUB_PREFIX=gitdb/v1 +GITDB_GITHUB_TOKEN=github_token_with_contents_write_access ``` -`GITDB_KEY`를 hosted runtime에 보내는 순간, 그 runtime은 query 실행을 위해 -plaintext를 처리할 수 있습니다. 이건 의도적으로 key를 맡기는 managed mode이지, -운영자도 볼 수 없는 구조가 아닙니다. +로컬 개발만 할 때는 `GITDB_GITHUB_TOKEN`을 비워둡니다. `GITDB_ENCRYPTION=off`는 +table 이름, column, row가 공개되어도 되는 demo에서만 사용하세요. ## 아키텍처 -GitDB는 네 층으로 나뉩니다. +GitDB는 네 계층으로 나뉩니다: -1. First-party ORM API - - `DataSource`, `Repository`, `save`, `find`, `findOne`, `delete`, - explicit transaction을 제공합니다. - - 새 앱은 PostgreSQL 호환 hop 없이 GitDB runtime에 더 가깝게 붙을 수 - 있습니다. +1. First-party API + - `DataSource`, `Repository`, `save`, `find`, `findOne`, `delete`, raw + `query`, transaction access를 제공합니다. + - 새 앱이 GitDB runtime 표면에 직접 붙도록 합니다. -2. PostgreSQL-compatible facade - - 기존 client용 로컬 TCP endpoint를 엽니다. - - Prisma, `pg`, raw SQL client가 평범한 PostgreSQL connection string으로 - 접속합니다. +2. SQL engine + - Schema, mutation, query, join, grouping, ordering, result row를 처리합니다. + - Local mutation을 persistence 전에 직렬화합니다. -3. SQL engine - - schema, mutation execution, query execution, join, grouping, result row를 - 처리합니다. - - local write transaction을 직렬화합니다. - - Node.js client와 ORM raw-query 흐름에서 자주 나오는 PostgreSQL subset을 - 우선 지원합니다. +3. Storage provider + - Local encrypted store + - Local plaintext store + - GitHub encrypted/plaintext store -4. Storage provider - - 개발/테스트용 local encrypted store - - visible snapshot용 local plaintext store - - remote durability용 GitHub encrypted/plaintext store +4. Audit and recovery model + - Manifest가 committed sequence를 기록합니다. + - Mutation log는 open 시 replay 가능합니다. + - Visible snapshot은 plaintext reopen path를 빠르게 합니다. 자세한 내용은 [ARCHITECTURE.md](ARCHITECTURE.md)를 참고하세요. ## 벤치마크 -로컬 벤치마크: +Local benchmark: ```bash -pnpm benchmark +corepack pnpm benchmark ``` -현재 runtime과 이전 문서화된 run을 비교하고 웹사이트 evidence를 갱신: +Website benchmark evidence 갱신: ```bash -GITDB_BENCH_ROWS=250 pnpm benchmark:compare +GITDB_BENCH_ROWS=250 corepack pnpm benchmark:site ``` -GitHub write 벤치마크: +이전 local run과 비교: ```bash -GITDB_BENCH_GITHUB_ROWS=2 pnpm benchmark:github +GITDB_BENCH_ROWS=250 corepack pnpm benchmark:compare ``` -최근 측정 결과: +최근 측정: | Scenario | Rows | Write ms | Writes/s | Join ms | Reopen ms | | --- | ---: | ---: | ---: | ---: | ---: | -| local plaintext throttled visible snapshots | 250 | 97.55 | 2562.73 | 3.28 | 47.42 | -| local encrypted mutation log | 250 | 97.23 | 2571.34 | 0.87 | 45.26 | -| postgres facade over local encrypted | 250 | 148.46 | 1683.91 | 2.31 | 0.00 | - -이전 문서화된 run과 비교하면 local plaintext write는 173.14 writes/s에서 -2562.73 writes/s로 개선되어 14.80x입니다. local encrypted write는 328.09 -writes/s에서 2571.34 writes/s로 개선되어 7.84x입니다. +| local plaintext throttled visible snapshots | 250 | 146.75 | 1703.61 | 4.88 | 77.62 | +| local encrypted mutation log | 250 | 234.37 | 1066.70 | 1.18 | 78.31 | +| orm local plaintext | 250 | 5377.34 | 46.49 | 1.30 | 136.29 | -해석은 분명합니다. local execution은 example, demo, 저빈도 workload에 쓸 수 -있는 수준입니다. single-statement mutation은 더 이상 매 write마다 전체 in-memory -database를 clone하지 않고, persistence 실패 시 committed manifest 상태로 -복구합니다. 하지만 mutation마다 GitHub Contents API를 직접 호출하는 방식은 hot -path가 될 수 없습니다. WAL, index, compaction, batched Git commit은 현재 완성된 -OLTP 보장이 아니라 다음 storage-engine 작업입니다. +해석: raw local execution은 demo와 저빈도 프로젝트 데이터에 충분히 빠릅니다. +Repository `save()`는 row마다 작은 transaction을 쓰기 때문에 안전하지만 아직 +느립니다. 다음 성능 작업은 batch repository write, page-level snapshot, index, +log compaction, batched Git sync입니다. 자세한 내용은 [BENCHMARKS.md](BENCHMARKS.md)를 참고하세요. -## 성능 개선 로드맵 - -다음 성능 개선은 facade보다 storage 구조가 핵심입니다. - -- Local WAL first: `fast` mode에서는 local durable write 후 성공 반환 -- Batched Git commits: 반복 Contents API write를 Git Database tree commit으로 교체 -- Snapshot throttling: 매 mutation마다 `data.json`을 다시 쓰지 않기 -- Chunked table pages: row 하나 때문에 table 전체를 다시 쓰지 않기 -- Local primary/secondary index: join/filter가 GitHub hot path를 타지 않게 하기 -- Manifest versions: cold start에서 unchanged table read 생략 -- Strong mode: 필요할 때만 GitHub commit 완료까지 block - -## 보안 모델 - -encrypted mode에서는 manifest와 mutation log를 AES-256-GCM으로 암호화합니다. -복호화 key는 repository에 저장하지 않습니다. - -public GitHub repository에서는 암호화해도 아래 metadata는 드러날 수 있습니다. - -- commit 시간 -- file 개수 -- 대략적인 file 크기 -- write 빈도 - -완화 방법은 batch, padding, compaction, 향후 opaque path storage입니다. - ## 현재 한계 -- SQL 지원 범위는 현재 GitDB engine이 실행하는 subset입니다. -- PostgreSQL catalog emulation은 아직 완전하지 않습니다. -- multi-process writer는 GitHub state로 감지하지만, 고동시성 OLTP database는 - 아닙니다. -- GitHub Contents API mode는 demo/correctness test용이지 production write - throughput용이 아닙니다. -- public plaintext mode는 말 그대로 공개 모드입니다. - -지원하지 않는 SQL은 조용히 틀린 결과를 내지 않고 명시적으로 실패해야 합니다. - -## 명령어 - -```bash -pnpm check -pnpm test -pnpm build -pnpm benchmark -pnpm benchmark:evaluate -pnpm pack:dry-run -pnpm publish:dry-run -pnpm start:facade -pnpm example -``` +- SQL 지원 범위는 현재 실행 가능한 subset으로 제한됩니다. +- Repository `save()`는 bulk insert에 최적화되어 있지 않습니다. +- Multi-process writer는 remote state로 보호하지만 아직 높은 동시성의 OLTP DB는 아닙니다. +- GitHub Contents API mode는 demo와 correctness test용이지 production write throughput용이 아닙니다. +- Public plaintext mode는 private mode가 아닙니다. -## 배포 +지원하지 않는 SQL은 조용히 성공한 척하지 않고 명시적으로 실패해야 합니다. -배포 서비스는 NestJS HTTP control plane과 PostgreSQL-compatible facade를 같은 -process에서 실행합니다. +## Commands ```bash -docker build -t gitdb . -docker run -p 3000:3000 -p 7432:7432 --env-file .env gitdb +corepack pnpm check +corepack pnpm test +corepack pnpm build +corepack pnpm benchmark +corepack pnpm benchmark:evaluate +corepack pnpm pack:dry-run +corepack pnpm publish:dry-run +corepack pnpm example ``` -`pnpm start`는 HTTP control plane을 실행합니다. `pnpm start:facade`는 local ORM -test용 TCP facade만 실행합니다. - -현재 public HTTP control plane: - -```text -https://gitdb.3xhaust.dev/health -``` - -`gitdb.3xhaust.dev`는 hosted GitDB runtime/control-plane instance로 보면 됩니다. -public plaintext workflow, demo, setup flow, 향후 managed mode에는 유용합니다. -하지만 encrypted data를 내 서비스만 복호화하게 만들고 싶다면 직접 `gitdb serve`를 -실행하고 `GITDB_KEY`를 그 환경에만 둬야 합니다. - -HTTP deployment가 외부 ORM client에 TCP facade를 자동으로 노출하는 것은 아닙니다. -remote ORM access가 필요하면 application 근처에서 `gitdb serve`를 실행하거나 TCP -port `7432`를 노출하는 배포 환경을 사용해야 합니다. - -## 라이선스 +## License MIT diff --git a/examples/express-prisma/.env.example b/examples/express-prisma/.env.example deleted file mode 100644 index 57f06ad..0000000 --- a/examples/express-prisma/.env.example +++ /dev/null @@ -1,10 +0,0 @@ -GITDB_ENCRYPTION=off -GITDB_ROOT=.gitdb-example-public -GITDB_HOST=127.0.0.1 -GITDB_PORT=0 -GITDB_GITHUB_OWNER=3x-haust -GITDB_GITHUB_REPO=gitdb-example-db -GITDB_GITHUB_BRANCH=main -GITDB_GITHUB_PREFIX=gitdb/v1 -GITDB_GITHUB_TOKEN= -API_PORT=3090 diff --git a/examples/express-prisma/schema.prisma b/examples/express-prisma/schema.prisma deleted file mode 100644 index fc34d5d..0000000 --- a/examples/express-prisma/schema.prisma +++ /dev/null @@ -1,8 +0,0 @@ -generator client { - provider = "prisma-client-js" - output = "./generated/client" -} - -datasource db { - provider = "postgresql" -} diff --git a/examples/express-prisma/server.mjs b/examples/express-prisma/server.mjs deleted file mode 100644 index 08c1e7a..0000000 --- a/examples/express-prisma/server.mjs +++ /dev/null @@ -1,94 +0,0 @@ -import { PrismaPg } from "@prisma/adapter-pg" -import { config as loadEnv } from "dotenv" -import express from "express" -import { createStoreFromEnv } from "../../dist/src/cli/store-factory.js" -import { createGitDbServer } from "../../dist/src/protocol/postgres-server.js" -import { GitDbEngine } from "../../dist/src/sql/engine.js" -import { PrismaClient } from "./generated/client/index.js" - -const envFile = {} -loadEnv({ path: new URL(".env", import.meta.url), processEnv: envFile }) - -const env = { - ...envFile, - ...process.env, - GITDB_ENCRYPTION: process.env.GITDB_ENCRYPTION ?? envFile.GITDB_ENCRYPTION ?? "off", - GITDB_HOST: process.env.GITDB_HOST ?? envFile.GITDB_HOST ?? "127.0.0.1", - GITDB_PORT: process.env.GITDB_PORT ?? envFile.GITDB_PORT ?? "0", - GITDB_ROOT: process.env.GITDB_ROOT ?? envFile.GITDB_ROOT ?? ".gitdb-example-public", -} - -const { mode, store } = createStoreFromEnv(env) -const engine = await GitDbEngine.open({ store }) -const server = await createGitDbServer({ - engine, - host: env.GITDB_HOST, - port: Number.parseInt(env.GITDB_PORT, 10), -}) -const databaseUrl = `postgresql://${server.host}:${server.port}/main` -const adapter = new PrismaPg({ connectionString: databaseUrl }) - -const prisma = new PrismaClient({ - adapter, -}) -const app = express() -const apiPort = Number.parseInt(process.env.API_PORT ?? envFile.API_PORT ?? "3090", 10) - -app.use(express.json()) - -await prisma.$executeRawUnsafe("CREATE TABLE IF NOT EXISTS teams (id STRING, name STRING)") -await prisma.$executeRawUnsafe( - "CREATE TABLE IF NOT EXISTS people (id STRING, name STRING, team_id STRING)", -) - -app.get("/health", (_request, response) => { - response.json({ databaseUrl, mode, status: "ready" }) -}) - -app.post("/seed", async (_request, response) => { - await prisma.$executeRawUnsafe("DELETE FROM people") - await prisma.$executeRawUnsafe("DELETE FROM teams") - await prisma.$executeRawUnsafe("INSERT INTO teams VALUES ('t1', 'Storage')") - await prisma.$executeRawUnsafe("INSERT INTO teams VALUES ('t2', 'Runtime')") - await prisma.$executeRawUnsafe("INSERT INTO people VALUES ('p1', 'Lin', 't1')") - await prisma.$executeRawUnsafe("INSERT INTO people VALUES ('p2', 'Ada', 't2')") - response.status(201).json({ inserted: 4 }) -}) - -app.get("/people", async (_request, response) => { - const rows = await prisma.$queryRawUnsafe(` - SELECT people.name AS person, teams.name AS team - FROM people - JOIN teams ON people.team_id = teams.id - ORDER BY people.name - `) - response.json({ rows }) -}) - -const httpServer = app.listen(apiPort, "127.0.0.1", () => { - process.stdout.write(`Express API: http://127.0.0.1:${apiPort}\n`) - process.stdout.write(`GitDB facade: ${databaseUrl}\n`) - process.stdout.write(`GitDB mode: ${mode}\n`) - process.stdout.write(`Try: curl -X POST http://127.0.0.1:${apiPort}/seed\n`) - process.stdout.write(`Try: curl http://127.0.0.1:${apiPort}/people\n`) -}) - -async function shutdown() { - await prisma.$disconnect() - await server.close() - await new Promise((resolve, reject) => { - httpServer.close((error) => { - if (error) { - reject(error) - return - } - resolve() - }) - }) -} - -process.on("SIGINT", () => { - void shutdown().finally(() => { - process.exit(0) - }) -}) diff --git a/examples/local-runtime/index.mjs b/examples/local-runtime/index.mjs new file mode 100644 index 0000000..159da33 --- /dev/null +++ b/examples/local-runtime/index.mjs @@ -0,0 +1,56 @@ +import { mkdtemp, rm } from "node:fs/promises" +import { tmpdir } from "node:os" +import { join } from "node:path" +import { createGitDbDataSource, defineEntity, LocalPlaintextStore } from "../../dist/src/index.js" + +const Team = defineEntity({ + columns: { + id: "STRING", + name: "STRING", + }, + primaryKey: "id", + tableName: "teams", +}) + +const Person = defineEntity({ + columns: { + id: "STRING", + name: "STRING", + team_id: "STRING", + }, + primaryKey: "id", + tableName: "people", +}) + +const root = await mkdtemp(join(tmpdir(), "gitdb-local-runtime-")) + +try { + const entities = [Team, Person] + const dataSource = await createGitDbDataSource({ + entities, + store: new LocalPlaintextStore({ root }), + synchronize: true, + }) + + await dataSource.getRepository(Team).save({ id: "t1", name: "Storage" }) + await dataSource.getRepository(Person).save({ id: "p1", name: "Lin", team_id: "t1" }) + + const joined = await dataSource.query( + "SELECT people.name AS person, teams.name AS team FROM people JOIN teams ON people.team_id = teams.id", + ) + const reopened = await createGitDbDataSource({ + entities, + store: new LocalPlaintextStore({ root }), + synchronize: false, + }) + + const summary = { + joined, + mode: "local-plaintext", + reopenedPeople: await reopened.getRepository(Person).find(), + status: "ok", + } + process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`) +} finally { + await rm(root, { force: true, recursive: true }) +} diff --git a/main.ts b/main.ts index c9faf9c..d17c71c 100644 --- a/main.ts +++ b/main.ts @@ -1 +1 @@ -import "./src/http/main.js" +import "./src/cli/main.js" diff --git a/package.json b/package.json index 28e7972..a31f02f 100644 --- a/package.json +++ b/package.json @@ -21,50 +21,33 @@ "docs" ], "scripts": { - "build": "tsc -p tsconfig.json", - "benchmark": "pnpm build && node scripts/benchmark.mjs", - "benchmark:compare": "GITDB_BENCH_OUTPUT=.gitdb/bench-current.json pnpm benchmark:evaluate > /dev/null && node scripts/benchmark-compare.mjs --current .gitdb/bench-current.json --baseline-path docs/BENCHMARKS.md --output site/benchmark.json --markdown site/benchmark.md", - "benchmark:evaluate": "pnpm build && node scripts/benchmark.mjs --json", - "benchmark:github": "pnpm build && node scripts/benchmark.mjs --github", + "build": "rm -rf dist && tsc -p tsconfig.json", + "benchmark": "corepack pnpm build && node scripts/benchmark.mjs", + "benchmark:compare": "GITDB_BENCH_OUTPUT=.gitdb/bench-current.json corepack pnpm benchmark:evaluate > /dev/null && node scripts/benchmark-compare.mjs --current .gitdb/bench-current.json --baseline-path docs/BENCHMARKS.md --output site/benchmark.json --markdown site/benchmark.md", + "benchmark:evaluate": "corepack pnpm build && node scripts/benchmark.mjs --json", + "benchmark:site": "GITDB_SITE_OUTPUT=site/benchmark.json corepack pnpm benchmark:evaluate > /dev/null", + "benchmark:github": "corepack pnpm build && node scripts/benchmark.mjs --github", "check": "biome check . && tsc --noEmit", - "example": "pnpm example:express-prisma", - "example:express-prisma": "pnpm build && pnpm exec prisma generate --schema examples/express-prisma/schema.prisma && node examples/express-prisma/server.mjs", + "example": "corepack pnpm example:local-runtime", + "example:local-runtime": "corepack pnpm build && node examples/local-runtime/index.mjs", "format": "biome check --write .", - "pack:dry-run": "pnpm build && COREPACK_ENABLE_STRICT=0 npm pack --dry-run --json", - "publish:dry-run": "pnpm build && COREPACK_ENABLE_STRICT=0 npm publish --dry-run --access public", + "pack:dry-run": "corepack pnpm build && COREPACK_ENABLE_STRICT=0 npm pack --dry-run --json", + "publish:dry-run": "corepack pnpm build && COREPACK_ENABLE_STRICT=0 npm publish --dry-run --access public", "site:preview": "python3 -m http.server 4173 --directory site", "test": "vitest run", - "test:e2e": "vitest run tests/e2e.test.ts", - "start": "node dist/src/http/main.js", - "start:facade": "node dist/src/cli/main.js serve", - "start:prod": "node dist/src/http/main.js" + "test:e2e": "vitest run tests/e2e.test.ts" }, "dependencies": { - "@nestjs/common": "^11.1.25", - "@nestjs/core": "^11.1.25", - "@nestjs/platform-express": "^11.1.25", "@octokit/rest": "22.0.1", "alasql": "4.17.3", "commander": "15.0.0", - "pg-server": "0.1.8", - "pgsql-ast-parser": "12.0.2", "pino": "10.3.1", - "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.2", "zod": "4.4.3" }, "devDependencies": { "@biomejs/biome": "2.4.16", - "@prisma/adapter-pg": "^7.8.0", - "@prisma/client": "^7.8.0", - "@prisma/client-runtime-utils": "^7.8.0", - "@types/express": "^5.0.6", "@types/node": "25.9.2", - "@types/pg": "8.20.0", "dotenv": "^17.4.2", - "express": "^5.2.1", - "pg": "8.21.0", - "prisma": "^7.8.0", "typescript": "6.0.3", "vitest": "4.1.8" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0a632e7..bfa8b70 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,15 +8,6 @@ importers: .: dependencies: - '@nestjs/common': - specifier: ^11.1.25 - version: 11.1.25(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': - specifier: ^11.1.25 - version: 11.1.25(@nestjs/common@11.1.25(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.25)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/platform-express': - specifier: ^11.1.25 - version: 11.1.25(@nestjs/common@11.1.25(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.25) '@octokit/rest': specifier: 22.0.1 version: 22.0.1 @@ -26,21 +17,9 @@ importers: commander: specifier: 15.0.0 version: 15.0.0 - pg-server: - specifier: 0.1.8 - version: 0.1.8 - pgsql-ast-parser: - specifier: 12.0.2 - version: 12.0.2 pino: specifier: 10.3.1 version: 10.3.1 - reflect-metadata: - specifier: ^0.2.2 - version: 0.2.2 - rxjs: - specifier: ^7.8.2 - version: 7.8.2 zod: specifier: 4.4.3 version: 4.4.3 @@ -48,36 +27,12 @@ importers: '@biomejs/biome': specifier: 2.4.16 version: 2.4.16 - '@prisma/adapter-pg': - specifier: ^7.8.0 - version: 7.8.0 - '@prisma/client': - specifier: ^7.8.0 - version: 7.8.0(prisma@7.8.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(typescript@6.0.3) - '@prisma/client-runtime-utils': - specifier: ^7.8.0 - version: 7.8.0 - '@types/express': - specifier: ^5.0.6 - version: 5.0.6 '@types/node': specifier: 25.9.2 version: 25.9.2 - '@types/pg': - specifier: 8.20.0 - version: 8.20.0 dotenv: specifier: ^17.4.2 version: 17.4.2 - express: - specifier: ^5.2.1 - version: 5.2.1 - pg: - specifier: 8.21.0 - version: 8.21.0 - prisma: - specifier: ^7.8.0 - version: 7.8.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) typescript: specifier: 6.0.3 version: 6.0.3 @@ -215,23 +170,6 @@ packages: cpu: [x64] os: [win32] - '@borewit/text-codec@0.2.2': - resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==} - - '@electric-sql/pglite-socket@0.1.1': - resolution: {integrity: sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw==} - hasBin: true - peerDependencies: - '@electric-sql/pglite': 0.4.1 - - '@electric-sql/pglite-tools@0.3.1': - resolution: {integrity: sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA==} - peerDependencies: - '@electric-sql/pglite': 0.4.1 - - '@electric-sql/pglite@0.4.1': - resolution: {integrity: sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==} - '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -241,12 +179,6 @@ packages: '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@hono/node-server@1.19.11': - resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} - engines: {node: '>=18.14.1'} - peerDependencies: - hono: ^4 - '@isaacs/ttlcache@1.4.1': resolution: {integrity: sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==} engines: {node: '>=12'} @@ -278,56 +210,12 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@kurkle/color@0.3.4': - resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} - - '@lukeed/csprng@1.1.0': - resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} - engines: {node: '>=8'} - '@napi-rs/wasm-runtime@1.1.4': resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@nestjs/common@11.1.25': - resolution: {integrity: sha512-eObNI89zpPEHQWr9BxD5yQe6hcDjzT6sj/jng1tWBL4GXEgZzrVYOnFA4pzk1ZvDNHa3z7GD2Yy0Z06Ihv9y6g==} - peerDependencies: - class-transformer: '>=0.4.1' - class-validator: '>=0.13.2' - reflect-metadata: ^0.1.12 || ^0.2.0 - rxjs: ^7.1.0 - peerDependenciesMeta: - class-transformer: - optional: true - class-validator: - optional: true - - '@nestjs/core@11.1.25': - resolution: {integrity: sha512-tkpoJlSB1CPGWHzEw3AyiUZhazngzhzKw14DbF9Rb5Ny50GP0zV6diTFkahob+HqzDDVjK/2DTt41OMFa4lK5w==} - engines: {node: '>= 20'} - peerDependencies: - '@nestjs/common': ^11.0.0 - '@nestjs/microservices': ^11.0.0 - '@nestjs/platform-express': ^11.0.0 - '@nestjs/websockets': ^11.0.0 - reflect-metadata: ^0.1.12 || ^0.2.0 - rxjs: ^7.1.0 - peerDependenciesMeta: - '@nestjs/microservices': - optional: true - '@nestjs/platform-express': - optional: true - '@nestjs/websockets': - optional: true - - '@nestjs/platform-express@11.1.25': - resolution: {integrity: sha512-a4f4WIGG8oCF4lwh7SlVOZXif0EwC4fsM/JZY5/X7OkHd8rp7Dxnx+sB2lwKAtnP6A29os/IPdH2Rll+wVJ6Qg==} - peerDependencies: - '@nestjs/common': ^11.0.0 - '@nestjs/core': ^11.0.0 - '@octokit/auth-token@6.0.0': resolution: {integrity: sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==} engines: {node: '>= 20'} @@ -386,143 +274,6 @@ packages: '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} - '@prisma/adapter-pg@7.8.0': - resolution: {integrity: sha512-ygb3UkerK3v8MDpXVgCISdRNDozpxh6+JVJgiIGbSr5KBgz10LLf5ejUskPGoXlsIjxsOu6nuy1JVQr2EKGSlg==} - - '@prisma/client-runtime-utils@7.8.0': - resolution: {integrity: sha512-5NQZztQ0oY/ADFkmd9gPuweH5A1/CCY8YQPorLLO0Mu6a87mY5gsnDkzmFmIHs9NFaLnZojzgddFVN4RpKYrdw==} - - '@prisma/client@7.8.0': - resolution: {integrity: sha512-HFp3Dawv/3sU3JtlPha90IB+48lS7zHiH4LKZPjmcE8YH5P9DOXGPvo8dqOtO7MqLDd1p2hOWMcFlRT1DMblHw==} - engines: {node: ^20.19 || ^22.12 || >=24.0} - peerDependencies: - prisma: '*' - typescript: '>=5.4.0' - peerDependenciesMeta: - prisma: - optional: true - typescript: - optional: true - - '@prisma/config@7.8.0': - resolution: {integrity: sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw==} - - '@prisma/debug@7.2.0': - resolution: {integrity: sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==} - - '@prisma/debug@7.8.0': - resolution: {integrity: sha512-p+QZReysDUqXC+mk17q9a+Y/qzh4c2KYliDK30buYUyfrGeTGSyfmc0AIrJRhZJrLHhRiJa9Au/J72h3C+szvA==} - - '@prisma/dev@0.24.3': - resolution: {integrity: sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==} - - '@prisma/driver-adapter-utils@7.8.0': - resolution: {integrity: sha512-/Q13o0ZT0rjc1Xk0Q9KhZYwuq2EW/vSbWUBKfgEKkaCuB/Sg6bqnjmTZqC5cD4d6y1vfFAEwBRzfzoSMIVJ55A==} - - '@prisma/engines-version@7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a': - resolution: {integrity: sha512-fJPQxCkLgA5EayWaW8eArgCvjJ+N+Kz3VyeNKMEeYiQC4alNkxRKFVAGxv/ZUzuJISKqdw+zGeDbS6mn6RCPOA==} - - '@prisma/engines@7.8.0': - resolution: {integrity: sha512-jx3rCnNNrt5uzbkKlegtQ2GZHxSlihMCzutgT/BP6UIDF1r9tDI39hV/0T/cHZgzJ3ELbuQPXlVZy+Y1n0pcgw==} - - '@prisma/fetch-engine@7.8.0': - resolution: {integrity: sha512-gwB0Euiz/DDRyxFRpLXYlK3RfaZUj1c5dAYMuhZYfApg7arknJlcb9bIsOHDppJmbqYaVA+yBIiFMDBfprsNPQ==} - - '@prisma/get-platform@7.2.0': - resolution: {integrity: sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==} - - '@prisma/get-platform@7.8.0': - resolution: {integrity: sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==} - - '@prisma/query-plan-executor@7.2.0': - resolution: {integrity: sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==} - - '@prisma/streams-local@0.1.2': - resolution: {integrity: sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg==} - engines: {bun: '>=1.3.6', node: '>=22.0.0'} - - '@prisma/studio-core@0.27.3': - resolution: {integrity: sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw==} - engines: {node: ^20.19 || ^22.12 || >=24.0, pnpm: '8'} - peerDependencies: - '@types/react': ^18.0.0 || ^19.0.0 - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - - '@radix-ui/primitive@1.1.3': - resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} - - '@radix-ui/react-compose-refs@1.1.2': - resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-primitive@2.1.3': - resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-slot@1.2.3': - resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-toggle@1.1.10': - resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-use-controllable-state@1.2.2': - resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-effect-event@0.0.2': - resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-layout-effect@1.1.1': - resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@react-native/assets-registry@0.85.3': resolution: {integrity: sha512-u9ZiYP23vA2IFtdFQFmetzSmk6SM0xgKIoiOsr1hXNHjHaLhOm+/Ph1ud57wX6+Dbwdzx8coJgnzSKL3W21PCg==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} @@ -683,40 +434,18 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@tokenizer/inflate@0.4.1': - resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} - engines: {node: '>=18'} - - '@tokenizer/token@0.3.0': - resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} - '@tybys/wasm-util@0.10.2': resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} - '@types/body-parser@1.19.6': - resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} - '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - '@types/connect@3.4.38': - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - '@types/express-serve-static-core@5.1.1': - resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} - - '@types/express@5.0.6': - resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} - - '@types/http-errors@2.0.5': - resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} - '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -729,24 +458,9 @@ packages: '@types/node@25.9.2': resolution: {integrity: sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==} - '@types/pg@8.20.0': - resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} - - '@types/qs@6.15.1': - resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} - - '@types/range-parser@1.2.7': - resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/react@19.2.17': resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} - '@types/send@1.2.1': - resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} - - '@types/serve-static@2.2.0': - resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} - '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -799,9 +513,6 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} - ajv@8.20.0: - resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} - alasql@4.17.3: resolution: {integrity: sha512-EOBD6zfZJ1u6Y76672MapJOyahzjDIQGE7nspt4yKhBWZB2k0cIHHakx6iJaolKkjB4vMPIo09ysw52aQfZ9rw==} engines: {node: '>=15'} @@ -822,9 +533,6 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} - append-field@1.0.0: - resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} - asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} @@ -836,10 +544,6 @@ packages: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} - aws-ssl-profiles@1.1.2: - resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} - engines: {node: '>= 6.0.0'} - babel-plugin-syntax-hermes-parser@0.33.3: resolution: {integrity: sha512-/Z9xYdaJ1lC0pT9do6TqCqhOSLfZ5Ot8D5za1p+feEfWYupCOfGbhhEXN9r2ZgJtDNUNRw/Z+T2CvAGKBqtqWA==} @@ -857,13 +561,6 @@ packages: before-after-hook@4.0.0: resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} - better-result@2.9.2: - resolution: {integrity: sha512-WIFoBPCdnTOdk9inkE1ZRvCZ4P0CpSkAiLlchC65N7n9DcjZ3NhqkBOlafzpOVnO8ixyi37kicmSJ3ENhPZl7Q==} - - body-parser@2.2.2: - resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} - engines: {node: '>=18'} - braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -879,30 +576,6 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - busboy@1.6.0: - resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} - engines: {node: '>=10.16.0'} - - bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - - c12@3.3.4: - resolution: {integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==} - peerDependencies: - magicast: '*' - peerDependenciesMeta: - magicast: - optional: true - - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - camelcase@6.3.0: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} @@ -918,14 +591,6 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chart.js@4.5.1: - resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} - engines: {pnpm: '>=8'} - - chokidar@5.0.0: - resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} - engines: {node: '>= 20.19.0'} - chrome-launcher@0.15.2: resolution: {integrity: sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==} engines: {node: '>=12.13.0'} @@ -966,25 +631,10 @@ packages: commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - concat-stream@2.0.0: - resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} - engines: {'0': node >= 6.0} - - confbox@0.2.4: - resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} - connect@3.7.0: resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} engines: {node: '>= 0.10.0'} - content-disposition@1.1.0: - resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} - engines: {node: '>=18'} - - content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - content-type@2.0.0: resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} engines: {node: '>=18'} @@ -992,18 +642,6 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-signature@1.2.2: - resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} - engines: {node: '>=6.6.0'} - - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - - cors@2.8.6: - resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} - engines: {node: '>= 0.10'} - cross-fetch@4.1.0: resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==} @@ -1031,24 +669,10 @@ packages: supports-color: optional: true - deepmerge-ts@7.1.5: - resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} - engines: {node: '>=16.0.0'} - - defu@6.1.7: - resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} - - denque@2.1.0: - resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} - engines: {node: '>=0.10'} - depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} - destr@2.0.5: - resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} - destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -1057,33 +681,19 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - discontinuous-range@1.0.0: - resolution: {integrity: sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==} - dotenv@17.4.2: resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} engines: {node: '>=12'} - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - effect@3.20.0: - resolution: {integrity: sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==} - electron-to-chromium@1.5.368: resolution: {integrity: sha512-7RckJJK4uESJF9PxvfMWd3TGqIiieUTG4HxnKaKuIpGbcr+r2ZEB3g2gAhCP3Fqm42vJSzLfgab9eva/C4/XVw==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - empathic@2.0.0: - resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} - engines: {node: '>=14'} - encodeurl@1.0.2: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} @@ -1092,28 +702,12 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} - env-paths@3.0.0: - resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - error-stack-parser@2.1.4: resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - es-module-lexer@2.1.0: resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} - es-object-atoms@1.1.2: - resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} - engines: {node: '>= 0.4'} - escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -1143,26 +737,6 @@ packages: exponential-backoff@3.1.3: resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} - express@5.2.1: - resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} - engines: {node: '>= 18'} - - exsolve@1.0.8: - resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} - - fast-check@3.23.2: - resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} - engines: {node: '>=8.0.0'} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-safe-stringify@2.1.1: - resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - - fast-uri@3.1.2: - resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} - fb-dotslash@0.5.8: resolution: {integrity: sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA==} engines: {node: '>=20'} @@ -1180,10 +754,6 @@ packages: picomatch: optional: true - file-type@21.3.4: - resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} - engines: {node: '>=20'} - fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -1192,40 +762,18 @@ packages: resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} engines: {node: '>= 0.8'} - finalhandler@2.1.1: - resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} - engines: {node: '>= 18.0.0'} - flow-enums-runtime@0.0.6: resolution: {integrity: sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==} - foreground-child@3.3.1: - resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} - engines: {node: '>=14'} - - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} - fresh@2.0.0: - resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} - engines: {node: '>= 0.8'} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - generate-function@2.3.1: - resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} - gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -1234,46 +782,13 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - - get-port-please@3.2.0: - resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==} - - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - - giget@3.2.0: - resolution: {integrity: sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==} - hasBin: true - - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - grammex@3.1.12: - resolution: {integrity: sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==} - - graphmatch@1.1.1: - resolution: {integrity: sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==} - has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - hasown@2.0.4: - resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} - engines: {node: '>= 0.4'} - hermes-compiler@250829098.0.10: resolution: {integrity: sha512-TcRlZ0/TlyfJqquRFAWoyElVNnkdYRi/sEp4/Qy8/GYxjg8j2cS9D4MjuaQ+qimkmLN7AmO+44IznRf06mAr0w==} @@ -1289,28 +804,14 @@ packages: hermes-parser@0.35.0: resolution: {integrity: sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA==} - hono@4.12.24: - resolution: {integrity: sha512-I36D1s+HgQc55KbhEr4iybfxv/9o1zdpw+XEM6dJa91LqQD0HCoSGdxpRJCZE+aavs87j4V3Ls2OJzq8C/U4iw==} - engines: {node: '>=16.9.0'} - http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} - http-status-codes@2.3.0: - resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==} - https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} - iconv-lite@0.7.2: - resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} - engines: {node: '>=0.10.0'} - - ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - image-size@1.2.1: resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==} engines: {node: '>=16.x'} @@ -1322,10 +823,6 @@ packages: invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} @@ -1339,12 +836,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-promise@4.0.0: - resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - - is-property@1.0.2: - resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} - is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} @@ -1352,10 +843,6 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - iterare@1.2.1: - resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} - engines: {node: '>=6'} - jest-get-type@29.6.3: resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1387,9 +874,6 @@ packages: engines: {node: '>=6'} hasBin: true - json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - json-with-bigint@3.5.8: resolution: {integrity: sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==} @@ -1479,16 +963,9 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} - load-esm@1.0.3: - resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==} - engines: {node: '>=13.2.0'} - lodash.throttle@4.1.1: resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} - long@5.3.2: - resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} - loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -1496,10 +973,6 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lru.min@1.1.4: - resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} - engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1509,25 +982,9 @@ packages: marky@1.3.0: resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - - media-typer@0.3.0: - resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} - engines: {node: '>= 0.6'} - - media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} - engines: {node: '>= 0.8'} - memoize-one@5.2.1: resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} - merge-descriptors@2.0.0: - resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} - engines: {node: '>=18'} - merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -1593,18 +1050,10 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - mime-db@1.54.0: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - mime-types@3.0.2: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} @@ -1619,36 +1068,17 @@ packages: engines: {node: '>=10'} hasBin: true - moo@0.5.3: - resolution: {integrity: sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA==} - ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - multer@2.1.1: - resolution: {integrity: sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==} - engines: {node: '>= 10.16.0'} - - mysql2@3.15.3: - resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==} - engines: {node: '>= 8.0'} - - named-placeholders@1.1.6: - resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} - engines: {node: '>=8.0.0'} - nanoid@3.3.12: resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nearley@2.20.1: - resolution: {integrity: sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==} - hasBin: true - negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} @@ -1676,21 +1106,10 @@ packages: resolution: {integrity: sha512-eJXMpz4aQHXF/YBB9ddqZDIS+ooO91hObo9FoW/xBkr54/zCwYYCDqT/O54vNo8kOkWs5Ou/y28NgdrV0edQNA==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - - object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} - obug@2.1.2: resolution: {integrity: sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==} engines: {node: '>=12.20.0'} - ohash@2.0.11: - resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} - on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} @@ -1703,9 +1122,6 @@ packages: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - open@7.4.2: resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} engines: {node: '>=8'} @@ -1718,55 +1134,9 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - path-to-regexp@8.4.2: - resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - perfect-debounce@2.1.0: - resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} - - pg-cloudflare@1.4.0: - resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} - - pg-connection-string@2.13.0: - resolution: {integrity: sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==} - - pg-int8@1.0.1: - resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} - engines: {node: '>=4.0.0'} - - pg-pool@3.14.0: - resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} - peerDependencies: - pg: '>=8.0' - - pg-protocol@1.14.0: - resolution: {integrity: sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==} - - pg-server@0.1.8: - resolution: {integrity: sha512-iSgMkGSJsRlXrOaM/W1bPEbf0BKbprKsyTidKrGSDkQfZdBiA6aAhaWB80ectwt/JZBeiwIkkIFCKLTEYSnE1Q==} - - pg-types@2.2.0: - resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} - engines: {node: '>=4'} - - pg@8.21.0: - resolution: {integrity: sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==} - engines: {node: '>= 16.0.0'} - peerDependencies: - pg-native: '>=3.0.1' - peerDependenciesMeta: - pg-native: - optional: true - - pgpass@1.0.5: - resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} - - pgsql-ast-parser@12.0.2: - resolution: {integrity: sha512-1WWa96Sw6h4uv9GLw98EzH/+xoBTC8j2TwV/AMW3E+Ir/fHOu/jLLbj6kPiz3y2bGISTKNYvKWwHoqvQ5FLuAw==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1788,106 +1158,33 @@ packages: resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} hasBin: true - pkg-types@2.3.1: - resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} - postcss@8.5.15: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} - postgres-array@2.0.0: - resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} - engines: {node: '>=4'} - - postgres-array@3.0.4: - resolution: {integrity: sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==} - engines: {node: '>=12'} - - postgres-bytea@1.0.1: - resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} - engines: {node: '>=0.10.0'} - - postgres-date@1.0.7: - resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} - engines: {node: '>=0.10.0'} - - postgres-interval@1.2.0: - resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} - engines: {node: '>=0.10.0'} - - postgres@3.4.7: - resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} - engines: {node: '>=12'} - pretty-format@29.7.0: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - prisma@7.8.0: - resolution: {integrity: sha512-yfN4yrw7HV9kEJhoy1+jgah0jafEIQsf7uWouSsM8MvJtlubsk+kM7AIBWZ8+GJl74Yj3c+nbYqBkMOxtsZ3Lw==} - engines: {node: ^20.19 || ^22.12 || >=24.0} - hasBin: true - peerDependencies: - better-sqlite3: '>=9.0.0' - typescript: '>=5.4.0' - peerDependenciesMeta: - better-sqlite3: - optional: true - typescript: - optional: true - process-warning@5.0.0: resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} promise@8.3.0: resolution: {integrity: sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==} - proper-lockfile@4.1.2: - resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + queue@6.0.2: + resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - - pure-rand@6.1.0: - resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} - - qs@6.15.2: - resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} - engines: {node: '>=0.6'} - - queue@6.0.2: - resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} - - quick-format-unescaped@4.0.4: - resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} - - railroad-diagrams@1.0.0: - resolution: {integrity: sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==} - - randexp@0.4.6: - resolution: {integrity: sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==} - engines: {node: '>=0.12'} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - raw-body@3.0.2: - resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} - engines: {node: '>= 0.10'} - - rc9@3.0.1: - resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==} - react-devtools-core@6.1.5: resolution: {integrity: sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==} - react-dom@19.2.7: - resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} - peerDependencies: - react: ^19.2.7 - react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -1922,14 +1219,6 @@ packages: resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} engines: {node: '>=0.10.0'} - readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - - readdirp@5.0.0: - resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} - engines: {node: '>= 20.19.0'} - real-require@0.2.0: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} @@ -1937,53 +1226,22 @@ packages: real-require@1.0.0: resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==} - reflect-metadata@0.2.2: - resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} - regenerator-runtime@0.13.11: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} - remeda@2.33.4: - resolution: {integrity: sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==} - require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - - ret@0.1.15: - resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==} - engines: {node: '>=0.12'} - - retry@0.12.0: - resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} - engines: {node: '>= 4'} - rolldown@1.0.3: resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - router@2.2.0: - resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} - engines: {node: '>= 18'} - - rxjs@7.8.2: - resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} - - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - safe-stable-stringify@2.5.0: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -2000,13 +1258,6 @@ packages: resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} engines: {node: '>= 0.8.0'} - send@1.2.1: - resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} - engines: {node: '>= 18'} - - seq-queue@0.0.5: - resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} - serialize-error@2.1.0: resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==} engines: {node: '>=0.10.0'} @@ -2015,10 +1266,6 @@ packages: resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} engines: {node: '>= 0.8.0'} - serve-static@2.2.1: - resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} - engines: {node: '>= 18'} - setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -2034,32 +1281,9 @@ packages: resolution: {integrity: sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==} engines: {node: '>= 0.4'} - side-channel-list@1.0.1: - resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} - engines: {node: '>= 0.4'} - - side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} - - side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} - - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} - siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - sonic-boom@4.2.1: resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} @@ -2082,10 +1306,6 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} - sqlstring@2.3.3: - resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} - engines: {node: '>= 0.6'} - stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -2104,31 +1324,17 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - std-env@4.1.0: resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} - streamsearch@1.1.0: - resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} - engines: {node: '>=10.0.0'} - string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} - string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strtok3@10.3.5: - resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} - engines: {node: '>=18'} - supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -2175,10 +1381,6 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} - token-types@6.1.2: - resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} - engines: {node: '>=14.16'} - tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -2189,30 +1391,11 @@ packages: resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} engines: {node: '>=8'} - type-is@1.6.18: - resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} - engines: {node: '>= 0.6'} - - type-is@2.1.0: - resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} - engines: {node: '>= 18'} - - typedarray@0.0.6: - resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typescript@6.0.3: resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} hasBin: true - uid@2.0.2: - resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} - engines: {node: '>=8'} - - uint8array-extras@1.5.0: - resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} - engines: {node: '>=18'} - undici-types@7.24.6: resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} @@ -2232,25 +1415,10 @@ packages: utf8@3.0.0: resolution: {integrity: sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==} - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} - valibot@1.2.0: - resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} - peerDependencies: - typescript: '>=5' - peerDependenciesMeta: - typescript: - optional: true - - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - vite@8.0.16: resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2364,9 +1532,6 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@7.5.11: resolution: {integrity: sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==} engines: {node: '>=8.3.0'} @@ -2379,10 +1544,6 @@ packages: utf-8-validate: optional: true - xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} - y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -2411,9 +1572,6 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} - zeptomatch@2.1.0: - resolution: {integrity: sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==} - zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} @@ -2573,18 +1731,6 @@ snapshots: '@biomejs/cli-win32-x64@2.4.16': optional: true - '@borewit/text-codec@0.2.2': {} - - '@electric-sql/pglite-socket@0.1.1(@electric-sql/pglite@0.4.1)': - dependencies: - '@electric-sql/pglite': 0.4.1 - - '@electric-sql/pglite-tools@0.3.1(@electric-sql/pglite@0.4.1)': - dependencies: - '@electric-sql/pglite': 0.4.1 - - '@electric-sql/pglite@0.4.1': {} - '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -2601,10 +1747,6 @@ snapshots: tslib: 2.8.1 optional: true - '@hono/node-server@1.19.11(hono@4.12.24)': - dependencies: - hono: 4.12.24 - '@isaacs/ttlcache@1.4.1': optional: true @@ -2652,10 +1794,6 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.5 optional: true - '@kurkle/color@0.3.4': {} - - '@lukeed/csprng@1.1.0': {} - '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 @@ -2663,43 +1801,6 @@ snapshots: '@tybys/wasm-util': 0.10.2 optional: true - '@nestjs/common@11.1.25(reflect-metadata@0.2.2)(rxjs@7.8.2)': - dependencies: - file-type: 21.3.4 - iterare: 1.2.1 - load-esm: 1.0.3 - reflect-metadata: 0.2.2 - rxjs: 7.8.2 - tslib: 2.8.1 - uid: 2.0.2 - transitivePeerDependencies: - - supports-color - - '@nestjs/core@11.1.25(@nestjs/common@11.1.25(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.25)(reflect-metadata@0.2.2)(rxjs@7.8.2)': - dependencies: - '@nestjs/common': 11.1.25(reflect-metadata@0.2.2)(rxjs@7.8.2) - fast-safe-stringify: 2.1.1 - iterare: 1.2.1 - path-to-regexp: 8.4.2 - reflect-metadata: 0.2.2 - rxjs: 7.8.2 - tslib: 2.8.1 - uid: 2.0.2 - optionalDependencies: - '@nestjs/platform-express': 11.1.25(@nestjs/common@11.1.25(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.25) - - '@nestjs/platform-express@11.1.25(@nestjs/common@11.1.25(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.25)': - dependencies: - '@nestjs/common': 11.1.25(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.25(@nestjs/common@11.1.25(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.25)(reflect-metadata@0.2.2)(rxjs@7.8.2) - cors: 2.8.6 - express: 5.2.1 - multer: 2.1.1 - path-to-regexp: 8.4.2 - tslib: 2.8.1 - transitivePeerDependencies: - - supports-color - '@octokit/auth-token@6.0.0': {} '@octokit/core@7.0.6': @@ -2767,159 +1868,6 @@ snapshots: '@pinojs/redact@0.4.0': {} - '@prisma/adapter-pg@7.8.0': - dependencies: - '@prisma/driver-adapter-utils': 7.8.0 - '@types/pg': 8.20.0 - pg: 8.21.0 - postgres-array: 3.0.4 - transitivePeerDependencies: - - pg-native - - '@prisma/client-runtime-utils@7.8.0': {} - - '@prisma/client@7.8.0(prisma@7.8.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(typescript@6.0.3)': - dependencies: - '@prisma/client-runtime-utils': 7.8.0 - optionalDependencies: - prisma: 7.8.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) - typescript: 6.0.3 - - '@prisma/config@7.8.0': - dependencies: - c12: 3.3.4 - deepmerge-ts: 7.1.5 - effect: 3.20.0 - empathic: 2.0.0 - transitivePeerDependencies: - - magicast - - '@prisma/debug@7.2.0': {} - - '@prisma/debug@7.8.0': {} - - '@prisma/dev@0.24.3(typescript@6.0.3)': - dependencies: - '@electric-sql/pglite': 0.4.1 - '@electric-sql/pglite-socket': 0.1.1(@electric-sql/pglite@0.4.1) - '@electric-sql/pglite-tools': 0.3.1(@electric-sql/pglite@0.4.1) - '@hono/node-server': 1.19.11(hono@4.12.24) - '@prisma/get-platform': 7.2.0 - '@prisma/query-plan-executor': 7.2.0 - '@prisma/streams-local': 0.1.2 - foreground-child: 3.3.1 - get-port-please: 3.2.0 - hono: 4.12.24 - http-status-codes: 2.3.0 - pathe: 2.0.3 - proper-lockfile: 4.1.2 - remeda: 2.33.4 - std-env: 3.10.0 - valibot: 1.2.0(typescript@6.0.3) - zeptomatch: 2.1.0 - transitivePeerDependencies: - - typescript - - '@prisma/driver-adapter-utils@7.8.0': - dependencies: - '@prisma/debug': 7.8.0 - - '@prisma/engines-version@7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a': {} - - '@prisma/engines@7.8.0': - dependencies: - '@prisma/debug': 7.8.0 - '@prisma/engines-version': 7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a - '@prisma/fetch-engine': 7.8.0 - '@prisma/get-platform': 7.8.0 - - '@prisma/fetch-engine@7.8.0': - dependencies: - '@prisma/debug': 7.8.0 - '@prisma/engines-version': 7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a - '@prisma/get-platform': 7.8.0 - - '@prisma/get-platform@7.2.0': - dependencies: - '@prisma/debug': 7.2.0 - - '@prisma/get-platform@7.8.0': - dependencies: - '@prisma/debug': 7.8.0 - - '@prisma/query-plan-executor@7.2.0': {} - - '@prisma/streams-local@0.1.2': - dependencies: - ajv: 8.20.0 - better-result: 2.9.2 - env-paths: 3.0.0 - proper-lockfile: 4.1.2 - - '@prisma/studio-core@0.27.3(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/react-toggle': 1.1.10(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@types/react': 19.2.17 - chart.js: 4.5.1 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - transitivePeerDependencies: - - '@types/react-dom' - - '@radix-ui/primitive@1.1.3': {} - - '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.17)(react@19.2.7)': - dependencies: - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-primitive@2.1.3(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-slot@1.2.3(@types/react@19.2.17)(react@19.2.7)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-toggle@1.1.10(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-primitive': 2.1.3(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.17)(react@19.2.7)': - dependencies: - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.17)(react@19.2.7)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.17)(react@19.2.7)': - dependencies: - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - '@react-native/assets-registry@0.85.3': optional: true @@ -3056,53 +2004,20 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@tokenizer/inflate@0.4.1': - dependencies: - debug: 4.4.3 - token-types: 6.1.2 - transitivePeerDependencies: - - supports-color - - '@tokenizer/token@0.3.0': {} - '@tybys/wasm-util@0.10.2': dependencies: tslib: 2.8.1 optional: true - '@types/body-parser@1.19.6': - dependencies: - '@types/connect': 3.4.38 - '@types/node': 25.9.2 - '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 - '@types/connect@3.4.38': - dependencies: - '@types/node': 25.9.2 - '@types/deep-eql@4.0.2': {} '@types/estree@1.0.9': {} - '@types/express-serve-static-core@5.1.1': - dependencies: - '@types/node': 25.9.2 - '@types/qs': 6.15.1 - '@types/range-parser': 1.2.7 - '@types/send': 1.2.1 - - '@types/express@5.0.6': - dependencies: - '@types/body-parser': 1.19.6 - '@types/express-serve-static-core': 5.1.1 - '@types/serve-static': 2.2.0 - - '@types/http-errors@2.0.5': {} - '@types/istanbul-lib-coverage@2.0.6': optional: true @@ -3120,28 +2035,10 @@ snapshots: dependencies: undici-types: 7.24.6 - '@types/pg@8.20.0': - dependencies: - '@types/node': 25.9.2 - pg-protocol: 1.14.0 - pg-types: 2.2.0 - - '@types/qs@6.15.1': {} - - '@types/range-parser@1.2.7': {} - '@types/react@19.2.17': dependencies: csstype: 3.2.3 - - '@types/send@1.2.1': - dependencies: - '@types/node': 25.9.2 - - '@types/serve-static@2.2.0': - dependencies: - '@types/http-errors': 2.0.5 - '@types/node': 25.9.2 + optional: true '@types/yargs-parser@21.0.3': optional: true @@ -3201,6 +2098,7 @@ snapshots: dependencies: mime-types: 3.0.2 negotiator: 1.0.0 + optional: true acorn@8.16.0: optional: true @@ -3208,13 +2106,6 @@ snapshots: agent-base@7.1.4: optional: true - ajv@8.20.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.1.2 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - alasql@4.17.3(react-native@0.85.3(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.7)): dependencies: cross-fetch: 4.1.0 @@ -3238,8 +2129,6 @@ snapshots: ansi-styles@5.2.0: optional: true - append-field@1.0.0: {} - asap@2.0.6: optional: true @@ -3247,8 +2136,6 @@ snapshots: atomic-sleep@1.0.0: {} - aws-ssl-profiles@1.1.2: {} - babel-plugin-syntax-hermes-parser@0.33.3: dependencies: hermes-parser: 0.33.3 @@ -3265,22 +2152,6 @@ snapshots: before-after-hook@4.0.0: {} - better-result@2.9.2: {} - - body-parser@2.2.2: - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 4.4.3 - http-errors: 2.0.1 - iconv-lite: 0.7.2 - on-finished: 2.4.1 - qs: 6.15.2 - raw-body: 3.0.2 - type-is: 2.1.0 - transitivePeerDependencies: - - supports-color - braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -3300,38 +2171,8 @@ snapshots: node-int64: 0.4.0 optional: true - buffer-from@1.1.2: {} - - busboy@1.6.0: - dependencies: - streamsearch: 1.1.0 - - bytes@3.1.2: {} - - c12@3.3.4: - dependencies: - chokidar: 5.0.0 - confbox: 0.2.4 - defu: 6.1.7 - dotenv: 17.4.2 - exsolve: 1.0.8 - giget: 3.2.0 - jiti: 2.7.0 - ohash: 2.0.11 - pathe: 2.0.3 - perfect-debounce: 2.1.0 - pkg-types: 2.3.1 - rc9: 3.0.1 - - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 + buffer-from@1.1.2: + optional: true camelcase@6.3.0: optional: true @@ -3347,14 +2188,6 @@ snapshots: supports-color: 7.2.0 optional: true - chart.js@4.5.1: - dependencies: - '@kurkle/color': 0.3.4 - - chokidar@5.0.0: - dependencies: - readdirp: 5.0.0 - chrome-launcher@0.15.2: dependencies: '@types/node': 25.9.2 @@ -3406,16 +2239,8 @@ snapshots: commander@15.0.0: {} - commander@2.20.3: {} - - concat-stream@2.0.0: - dependencies: - buffer-from: 1.1.2 - inherits: 2.0.4 - readable-stream: 3.6.2 - typedarray: 0.0.6 - - confbox@0.2.4: {} + commander@2.20.3: + optional: true connect@3.7.0: dependencies: @@ -3427,23 +2252,10 @@ snapshots: - supports-color optional: true - content-disposition@1.1.0: {} - - content-type@1.0.5: {} - content-type@2.0.0: {} convert-source-map@2.0.0: {} - cookie-signature@1.2.2: {} - - cookie@0.7.2: {} - - cors@2.8.6: - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 - cross-fetch@4.1.0: dependencies: node-fetch: 2.7.0 @@ -3455,8 +2267,10 @@ snapshots: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 + optional: true - csstype@3.2.3: {} + csstype@3.2.3: + optional: true debug@2.6.9: dependencies: @@ -3466,71 +2280,43 @@ snapshots: debug@4.4.3: dependencies: ms: 2.1.3 + optional: true - deepmerge-ts@7.1.5: {} - - defu@6.1.7: {} - - denque@2.1.0: {} - - depd@2.0.0: {} - - destr@2.0.5: {} + depd@2.0.0: + optional: true destroy@1.2.0: optional: true detect-libc@2.1.2: {} - discontinuous-range@1.0.0: {} - dotenv@17.4.2: {} - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - - ee-first@1.1.1: {} - - effect@3.20.0: - dependencies: - '@standard-schema/spec': 1.1.0 - fast-check: 3.23.2 + ee-first@1.1.1: + optional: true electron-to-chromium@1.5.368: optional: true emoji-regex@8.0.0: {} - empathic@2.0.0: {} - encodeurl@1.0.2: optional: true - encodeurl@2.0.0: {} - - env-paths@3.0.0: {} + encodeurl@2.0.0: + optional: true error-stack-parser@2.1.4: dependencies: stackframe: 1.3.4 optional: true - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - es-module-lexer@2.1.0: {} - es-object-atoms@1.1.2: - dependencies: - es-errors: 1.3.0 - escalade@3.2.0: {} - escape-html@1.0.3: {} + escape-html@1.0.3: + optional: true escape-string-regexp@4.0.0: optional: true @@ -3539,7 +2325,8 @@ snapshots: dependencies: '@types/estree': 1.0.9 - etag@1.8.1: {} + etag@1.8.1: + optional: true event-target-shim@5.0.1: optional: true @@ -3549,51 +2336,6 @@ snapshots: exponential-backoff@3.1.3: optional: true - express@5.2.1: - dependencies: - accepts: 2.0.0 - body-parser: 2.2.2 - content-disposition: 1.1.0 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.2.2 - debug: 4.4.3 - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 2.1.1 - fresh: 2.0.0 - http-errors: 2.0.1 - merge-descriptors: 2.0.0 - mime-types: 3.0.2 - on-finished: 2.4.1 - once: 1.4.0 - parseurl: 1.3.3 - proxy-addr: 2.0.7 - qs: 6.15.2 - range-parser: 1.2.1 - router: 2.2.0 - send: 1.2.1 - serve-static: 2.2.1 - statuses: 2.0.2 - type-is: 2.1.0 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - - exsolve@1.0.8: {} - - fast-check@3.23.2: - dependencies: - pure-rand: 6.1.0 - - fast-deep-equal@3.1.3: {} - - fast-safe-stringify@2.1.1: {} - - fast-uri@3.1.2: {} - fb-dotslash@0.5.8: optional: true @@ -3606,15 +2348,6 @@ snapshots: optionalDependencies: picomatch: 4.0.4 - file-type@21.3.4: - dependencies: - '@tokenizer/inflate': 0.4.1 - strtok3: 10.3.5 - token-types: 6.1.2 - uint8array-extras: 1.5.0 - transitivePeerDependencies: - - supports-color - fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -3633,85 +2366,26 @@ snapshots: - supports-color optional: true - finalhandler@2.1.1: - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - flow-enums-runtime@0.0.6: optional: true - foreground-child@3.3.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - - forwarded@0.2.0: {} - fresh@0.5.2: optional: true - fresh@2.0.0: {} - fsevents@2.3.3: optional: true - function-bind@1.1.2: {} - - generate-function@2.3.1: - dependencies: - is-property: 1.0.2 - gensync@1.0.0-beta.2: optional: true get-caller-file@2.0.5: {} - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.4 - math-intrinsics: 1.1.0 - - get-port-please@3.2.0: {} - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.2 - - giget@3.2.0: {} - - gopd@1.2.0: {} - - graceful-fs@4.2.11: {} - - grammex@3.1.12: {} - - graphmatch@1.1.1: {} + graceful-fs@4.2.11: + optional: true has-flag@4.0.0: optional: true - has-symbols@1.1.0: {} - - hasown@2.0.4: - dependencies: - function-bind: 1.1.2 - hermes-compiler@250829098.0.10: optional: true @@ -3731,8 +2405,6 @@ snapshots: hermes-estree: 0.35.0 optional: true - hono@4.12.24: {} - http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -3740,8 +2412,7 @@ snapshots: setprototypeof: 1.2.0 statuses: 2.0.2 toidentifier: 1.0.1 - - http-status-codes@2.3.0: {} + optional: true https-proxy-agent@7.0.6: dependencies: @@ -3751,26 +2422,19 @@ snapshots: - supports-color optional: true - iconv-lite@0.7.2: - dependencies: - safer-buffer: 2.1.2 - - ieee754@1.2.1: {} - image-size@1.2.1: dependencies: queue: 6.0.2 optional: true - inherits@2.0.4: {} + inherits@2.0.4: + optional: true invariant@2.2.4: dependencies: loose-envify: 1.4.0 optional: true - ipaddr.js@1.9.1: {} - is-docker@2.2.1: optional: true @@ -3779,18 +2443,13 @@ snapshots: is-number@7.0.0: optional: true - is-promise@4.0.0: {} - - is-property@1.0.2: {} - is-wsl@2.2.0: dependencies: is-docker: 2.2.1 optional: true - isexe@2.0.0: {} - - iterare@1.2.1: {} + isexe@2.0.0: + optional: true jest-get-type@29.6.3: optional: true @@ -3823,7 +2482,8 @@ snapshots: supports-color: 8.1.1 optional: true - jiti@2.7.0: {} + jiti@2.7.0: + optional: true js-tokens@4.0.0: optional: true @@ -3834,8 +2494,6 @@ snapshots: jsesc@3.1.0: optional: true - json-schema-traverse@1.0.0: {} - json-with-bigint@3.5.8: {} json5@2.2.3: @@ -3901,13 +2559,9 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 - load-esm@1.0.3: {} - lodash.throttle@4.1.1: optional: true - long@5.3.2: {} - loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -3918,8 +2572,6 @@ snapshots: yallist: 3.1.1 optional: true - lru.min@1.1.4: {} - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -3932,17 +2584,9 @@ snapshots: marky@1.3.0: optional: true - math-intrinsics@1.1.0: {} - - media-typer@0.3.0: {} - - media-typer@1.1.0: {} - memoize-one@5.2.1: optional: true - merge-descriptors@2.0.0: {} - merge-stream@2.0.0: optional: true @@ -4140,17 +2784,13 @@ snapshots: picomatch: 2.3.2 optional: true - mime-db@1.52.0: {} - - mime-db@1.54.0: {} - - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 + mime-db@1.54.0: + optional: true mime-types@3.0.2: dependencies: mime-db: 1.54.0 + optional: true mime@1.6.0: optional: true @@ -4158,46 +2798,16 @@ snapshots: mkdirp@1.0.4: optional: true - moo@0.5.3: {} - ms@2.0.0: optional: true - ms@2.1.3: {} - - multer@2.1.1: - dependencies: - append-field: 1.0.0 - busboy: 1.6.0 - concat-stream: 2.0.0 - type-is: 1.6.18 - - mysql2@3.15.3: - dependencies: - aws-ssl-profiles: 1.1.2 - denque: 2.1.0 - generate-function: 2.3.1 - iconv-lite: 0.7.2 - long: 5.3.2 - lru.min: 1.1.4 - named-placeholders: 1.1.6 - seq-queue: 0.0.5 - sqlstring: 2.3.3 - - named-placeholders@1.1.6: - dependencies: - lru.min: 1.1.4 + ms@2.1.3: + optional: true nanoid@3.3.12: {} - nearley@2.20.1: - dependencies: - commander: 2.20.3 - moo: 0.5.3 - railroad-diagrams: 1.0.0 - randexp: 0.4.6 - - negotiator@1.0.0: {} + negotiator@1.0.0: + optional: true node-fetch@2.7.0: dependencies: @@ -4217,14 +2827,8 @@ snapshots: flow-enums-runtime: 0.0.6 optional: true - object-assign@4.1.1: {} - - object-inspect@1.13.4: {} - obug@2.1.2: {} - ohash@2.0.11: {} - on-exit-leak-free@2.1.2: {} on-finished@2.3.0: @@ -4235,10 +2839,7 @@ snapshots: on-finished@2.4.1: dependencies: ee-first: 1.1.1 - - once@1.4.0: - dependencies: - wrappy: 1.0.2 + optional: true open@7.4.2: dependencies: @@ -4246,59 +2847,13 @@ snapshots: is-wsl: 2.2.0 optional: true - parseurl@1.3.3: {} - - path-key@3.1.1: {} - - path-to-regexp@8.4.2: {} - - pathe@2.0.3: {} - - perfect-debounce@2.1.0: {} - - pg-cloudflare@1.4.0: + parseurl@1.3.3: optional: true - pg-connection-string@2.13.0: {} - - pg-int8@1.0.1: {} - - pg-pool@3.14.0(pg@8.21.0): - dependencies: - pg: 8.21.0 - - pg-protocol@1.14.0: {} - - pg-server@0.1.8: - dependencies: - pg-protocol: 1.14.0 - - pg-types@2.2.0: - dependencies: - pg-int8: 1.0.1 - postgres-array: 2.0.0 - postgres-bytea: 1.0.1 - postgres-date: 1.0.7 - postgres-interval: 1.2.0 - - pg@8.21.0: - dependencies: - pg-connection-string: 2.13.0 - pg-pool: 3.14.0(pg@8.21.0) - pg-protocol: 1.14.0 - pg-types: 2.2.0 - pgpass: 1.0.5 - optionalDependencies: - pg-cloudflare: 1.4.0 - - pgpass@1.0.5: - dependencies: - split2: 4.2.0 + path-key@3.1.1: + optional: true - pgsql-ast-parser@12.0.2: - dependencies: - moo: 0.5.3 - nearley: 2.20.1 + pathe@2.0.3: {} picocolors@1.1.1: {} @@ -4327,32 +2882,12 @@ snapshots: sonic-boom: 4.2.1 thread-stream: 4.2.0 - pkg-types@2.3.1: - dependencies: - confbox: 0.2.4 - exsolve: 1.0.8 - pathe: 2.0.3 - postcss@8.5.15: dependencies: nanoid: 3.3.12 picocolors: 1.1.1 source-map-js: 1.2.1 - postgres-array@2.0.0: {} - - postgres-array@3.0.4: {} - - postgres-bytea@1.0.1: {} - - postgres-date@1.0.7: {} - - postgres-interval@1.2.0: - dependencies: - xtend: 4.0.2 - - postgres@3.4.7: {} - pretty-format@29.7.0: dependencies: '@jest/schemas': 29.6.3 @@ -4360,23 +2895,6 @@ snapshots: react-is: 18.3.1 optional: true - prisma@7.8.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3): - dependencies: - '@prisma/config': 7.8.0 - '@prisma/dev': 0.24.3(typescript@6.0.3) - '@prisma/engines': 7.8.0 - '@prisma/studio-core': 0.27.3(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - mysql2: 3.15.3 - postgres: 3.4.7 - optionalDependencies: - typescript: 6.0.3 - transitivePeerDependencies: - - '@types/react' - - '@types/react-dom' - - magicast - - react - - react-dom - process-warning@5.0.0: {} promise@8.3.0: @@ -4384,23 +2902,6 @@ snapshots: asap: 2.0.6 optional: true - proper-lockfile@4.1.2: - dependencies: - graceful-fs: 4.2.11 - retry: 0.12.0 - signal-exit: 3.0.7 - - proxy-addr@2.0.7: - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - - pure-rand@6.1.0: {} - - qs@6.15.2: - dependencies: - side-channel: 1.1.0 - queue@6.0.2: dependencies: inherits: 2.0.4 @@ -4408,26 +2909,8 @@ snapshots: quick-format-unescaped@4.0.4: {} - railroad-diagrams@1.0.0: {} - - randexp@0.4.6: - dependencies: - discontinuous-range: 1.0.0 - ret: 0.1.15 - - range-parser@1.2.1: {} - - raw-body@3.0.2: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.1 - iconv-lite: 0.7.2 - unpipe: 1.0.0 - - rc9@3.0.1: - dependencies: - defu: 6.1.7 - destr: 2.0.5 + range-parser@1.2.1: + optional: true react-devtools-core@6.1.5: dependencies: @@ -4438,11 +2921,6 @@ snapshots: - utf-8-validate optional: true - react-dom@19.2.7(react@19.2.7): - dependencies: - react: 19.2.7 - scheduler: 0.27.0 - react-is@18.3.1: optional: true @@ -4502,35 +2980,18 @@ snapshots: react-refresh@0.14.2: optional: true - react@19.2.7: {} - - readable-stream@3.6.2: - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - - readdirp@5.0.0: {} + react@19.2.7: + optional: true real-require@0.2.0: {} real-require@1.0.0: {} - reflect-metadata@0.2.2: {} - regenerator-runtime@0.13.11: optional: true - remeda@2.33.4: {} - require-directory@2.1.1: {} - require-from-string@2.0.2: {} - - ret@0.1.15: {} - - retry@0.12.0: {} - rolldown@1.0.3: dependencies: '@oxc-project/types': 0.133.0 @@ -4552,27 +3013,10 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.3 '@rolldown/binding-win32-x64-msvc': 1.0.3 - router@2.2.0: - dependencies: - debug: 4.4.3 - depd: 2.0.0 - is-promise: 4.0.0 - parseurl: 1.3.3 - path-to-regexp: 8.4.2 - transitivePeerDependencies: - - supports-color - - rxjs@7.8.2: - dependencies: - tslib: 2.8.1 - - safe-buffer@5.2.1: {} - safe-stable-stringify@2.5.0: {} - safer-buffer@2.1.2: {} - - scheduler@0.27.0: {} + scheduler@0.27.0: + optional: true semver@6.3.1: optional: true @@ -4599,24 +3043,6 @@ snapshots: - supports-color optional: true - send@1.2.1: - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 2.0.0 - http-errors: 2.0.1 - mime-types: 3.0.2 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - - seq-queue@0.0.5: {} - serialize-error@2.1.0: optional: true @@ -4630,60 +3056,22 @@ snapshots: - supports-color optional: true - serve-static@2.2.1: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 1.2.1 - transitivePeerDependencies: - - supports-color - - setprototypeof@1.2.0: {} + setprototypeof@1.2.0: + optional: true shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 + optional: true - shebang-regex@3.0.0: {} + shebang-regex@3.0.0: + optional: true shell-quote@1.8.4: optional: true - side-channel-list@1.0.1: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - - side-channel@1.1.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.1 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - siginfo@2.0.0: {} - signal-exit@3.0.7: {} - - signal-exit@4.1.0: {} - sonic-boom@4.2.1: dependencies: atomic-sleep: 1.0.0 @@ -4704,8 +3092,6 @@ snapshots: split2@4.2.0: {} - sqlstring@2.3.3: {} - stackback@0.0.2: {} stackframe@1.3.4: @@ -4719,32 +3105,21 @@ snapshots: statuses@1.5.0: optional: true - statuses@2.0.2: {} - - std-env@3.10.0: {} + statuses@2.0.2: + optional: true std-env@4.1.0: {} - streamsearch@1.1.0: {} - string-width@4.2.3: dependencies: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - string_decoder@1.3.0: - dependencies: - safe-buffer: 5.2.1 - strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 - strtok3@10.3.5: - dependencies: - '@tokenizer/token': 0.3.0 - supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -4789,47 +3164,25 @@ snapshots: is-number: 7.0.0 optional: true - toidentifier@1.0.1: {} - - token-types@6.1.2: - dependencies: - '@borewit/text-codec': 0.2.2 - '@tokenizer/token': 0.3.0 - ieee754: 1.2.1 + toidentifier@1.0.1: + optional: true tr46@0.0.3: {} - tslib@2.8.1: {} + tslib@2.8.1: + optional: true type-fest@0.7.1: optional: true - type-is@1.6.18: - dependencies: - media-typer: 0.3.0 - mime-types: 2.1.35 - - type-is@2.1.0: - dependencies: - content-type: 2.0.0 - media-typer: 1.1.0 - mime-types: 3.0.2 - - typedarray@0.0.6: {} - typescript@6.0.3: {} - uid@2.0.2: - dependencies: - '@lukeed/csprng': 1.1.0 - - uint8array-extras@1.5.0: {} - undici-types@7.24.6: {} universal-user-agent@7.0.3: {} - unpipe@1.0.0: {} + unpipe@1.0.0: + optional: true update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: @@ -4841,17 +3194,9 @@ snapshots: utf8@3.0.0: optional: true - util-deprecate@1.0.2: {} - utils-merge@1.0.1: optional: true - valibot@1.2.0(typescript@6.0.3): - optionalDependencies: - typescript: 6.0.3 - - vary@1.1.2: {} - vite@8.0.16(@types/node@25.9.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 @@ -4914,6 +3259,7 @@ snapshots: which@2.0.2: dependencies: isexe: 2.0.0 + optional: true why-is-node-running@2.3.0: dependencies: @@ -4926,13 +3272,9 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - wrappy@1.0.2: {} - ws@7.5.11: optional: true - xtend@4.0.2: {} - y18n@5.0.8: {} yallist@3.1.1: @@ -4967,9 +3309,4 @@ snapshots: yargs-parser: 21.1.1 optional: true - zeptomatch@2.1.0: - dependencies: - grammex: 3.1.12 - graphmatch: 1.1.1 - zod@4.4.3: {} diff --git a/scripts/benchmark-report.mjs b/scripts/benchmark-report.mjs index 992919b..939dc2c 100644 --- a/scripts/benchmark-report.mjs +++ b/scripts/benchmark-report.mjs @@ -44,6 +44,7 @@ export function buildBenchmarkEvidence({ }, generatedAt: new Date().toISOString(), headline: headline(comparisons), + scenarios: current.map((row) => ({ ...row, key: siteScenarioKey(row.label) })), } } @@ -149,22 +150,32 @@ function compareRows(key, baselineRow, currentRow) { function scenarioKey(label) { const normalized = label.toLowerCase() + if (normalized.startsWith("orm local plaintext")) { + return "orm local plaintext" + } if (normalized.startsWith("local plaintext")) { return "local plaintext" } if (normalized.startsWith("local encrypted")) { return "local encrypted" } - if (normalized.startsWith("postgres facade")) { - return "postgres facade" - } if (normalized.startsWith("github plaintext")) { return "github plaintext" } return normalized } +function siteScenarioKey(label) { + return scenarioKey(label).replaceAll(/\s+/g, "-") +} + function headline(comparisons) { + const orm = comparisons.find((row) => row.key === "orm local plaintext") + const raw = comparisons.find((row) => row.key === "local plaintext") + if (orm !== undefined && raw !== undefined) { + const overhead = ratio(raw.current.writesPerSecond, orm.current.writesPerSecond) + return `Raw engine is ${formatRatio(overhead)} of ORM writes/s (save() transaction overhead)` + } const localPlaintext = comparisons.find((row) => row.key === "local plaintext") if (localPlaintext === undefined) { return "Benchmark comparison generated" diff --git a/scripts/benchmark.mjs b/scripts/benchmark.mjs index 0f7f6c7..7086722 100644 --- a/scripts/benchmark.mjs +++ b/scripts/benchmark.mjs @@ -2,14 +2,25 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises" import { tmpdir } from "node:os" import { dirname, join } from "node:path" import { performance } from "node:perf_hooks" -import { Client } from "pg" import { createAesGcmCipher } from "../dist/src/crypto/aes-gcm.js" import { GitHubPlaintextStore } from "../dist/src/github/github-plaintext-store.js" -import { createGitDbServer } from "../dist/src/protocol/postgres-server.js" +import { createGitDbDataSource, defineEntity } from "../dist/src/orm/index.js" import { GitDbEngine } from "../dist/src/sql/engine.js" import { LocalEncryptedStore } from "../dist/src/storage/local-encrypted-store.js" import { LocalPlaintextStore } from "../dist/src/storage/local-plaintext-store.js" +const Team = defineEntity({ + columns: { id: "STRING", name: "STRING" }, + primaryKey: "id", + tableName: "teams", +}) + +const Person = defineEntity({ + columns: { id: "STRING", name: "STRING", team_id: "STRING" }, + primaryKey: "id", + tableName: "people", +}) + const args = new Set(process.argv.slice(2)) const rows = numberEnv("GITDB_BENCH_ROWS", 250) const githubRows = numberEnv("GITDB_BENCH_GITHUB_ROWS", 8) @@ -34,7 +45,14 @@ results.push( }), }), ) -results.push(await benchPostgresFacade(rows)) +results.push( + await benchOrm({ + label: "orm local plaintext", + rows, + snapshotPolicy: { mode: "interval", mutations: 100 }, + store: (root) => new LocalPlaintextStore({ root }), + }), +) if (args.has("--github")) { results.push(await benchGitHubPlaintext(githubRows)) @@ -44,6 +62,13 @@ if (process.env.GITDB_BENCH_OUTPUT !== undefined) { await writeJson(process.env.GITDB_BENCH_OUTPUT, results) } +if (process.env.GITDB_SITE_OUTPUT !== undefined) { + await writeJson(process.env.GITDB_SITE_OUTPUT, { + generatedAt: new Date().toISOString(), + scenarios: results.map((r) => ({ ...r, key: scenarioKey(r.label) })), + }) +} + process.stdout.write( args.has("--json") ? `${JSON.stringify(results, null, 2)}\n` : formatMarkdown(results), ) @@ -75,30 +100,49 @@ async function benchEngine(options) { } } -async function benchPostgresFacade(rowCount) { - const root = await mkdtemp(join(tmpdir(), "gitdb-pg-bench-")) - const store = new LocalEncryptedStore({ - cipher: createAesGcmCipher(Buffer.alloc(32, 9).toString("base64url")), - root, - }) - const engine = await GitDbEngine.open({ store }) - const server = await createGitDbServer({ engine, host: "127.0.0.1", port: 0 }) - const client = new Client({ - connectionString: `postgresql://127.0.0.1:${server.port}/main`, - }) +async function benchOrm(options) { + const root = await mkdtemp(join(tmpdir(), "gitdb-orm-bench-")) try { - await client.connect() - await createTables(client) + const dataSource = await createGitDbDataSource({ + entities: [Team, Person], + snapshotPolicy: options.snapshotPolicy, + store: options.store(root), + synchronize: true, + }) + const teams = dataSource.getRepository(Team) + const people = dataSource.getRepository(Person) + for (let i = 0; i < 10; i += 1) { + await teams.save({ id: `t${i}`, name: `Team ${i}` }) + } const writeMs = await time(async () => { - await insertRows(client, rowCount) + for (let i = 0; i < options.rows; i += 1) { + await people.save({ id: `p${i}`, name: `Person ${i}`, team_id: `t${i % 10}` }) + } }) const joinMs = await time(async () => { - await assertJoin(client, rowCount) + const result = await dataSource.query( + "SELECT people.name AS person, teams.name AS team FROM people JOIN teams ON people.team_id = teams.id ORDER BY people.name", + ) + if (result.length !== options.rows) { + throw new Error(`expected ${options.rows} joined rows, got ${result.length}`) + } + }) + const reopenMs = await time(async () => { + const reopened = await createGitDbDataSource({ + entities: [Team, Person], + snapshotPolicy: options.snapshotPolicy, + store: options.store(root), + synchronize: false, + }) + const rows = await reopened.query( + "SELECT people.name AS person, teams.name AS team FROM people JOIN teams ON people.team_id = teams.id ORDER BY people.name", + ) + if (rows.length !== options.rows) { + throw new Error(`expected ${options.rows} joined rows, got ${rows.length}`) + } }) - return result("postgres facade over local encrypted", rowCount, writeMs, joinMs, 0) + return result(options.label, options.rows, writeMs, joinMs, reopenMs) } finally { - await client.end() - await server.close() await rm(root, { force: true, recursive: true }) } } @@ -175,6 +219,15 @@ function result(label, rowCount, writeMs, joinMs, reopenMs) { } } +function scenarioKey(label) { + const normalized = label.toLowerCase() + if (normalized.startsWith("orm local plaintext")) return "orm-local-plaintext" + if (normalized.startsWith("local plaintext")) return "local-plaintext" + if (normalized.startsWith("local encrypted")) return "local-encrypted" + if (normalized.startsWith("github plaintext")) return "github-plaintext" + return normalized.replaceAll(/\s+/g, "-") +} + function formatMarkdown(items) { const lines = [ "| Scenario | Rows | Write ms | Writes/s | Join ms | Reopen ms |", @@ -197,10 +250,10 @@ function fixed(value) { return value.toFixed(2) } -function numberEnv(name, fallback) { +function numberEnv(name, defaultValue) { const value = process.env[name] if (value === undefined || value.trim().length === 0) { - return fallback + return defaultValue } const parsed = Number.parseInt(value, 10) if (!Number.isFinite(parsed) || parsed <= 0) { diff --git a/site/app.js b/site/app.js index fed5ba8..cd4e4a9 100644 --- a/site/app.js +++ b/site/app.js @@ -3,15 +3,8 @@ const numberFormatter = new Intl.NumberFormat("en-US", { minimumFractionDigits: 0, }) -const percentFormatter = new Intl.NumberFormat("en-US", { - maximumFractionDigits: 1, - minimumFractionDigits: 1, - signDisplay: "always", -}) - -const fallbackData = { - comparisons: [], - headline: "Run pnpm benchmark:compare to refresh benchmark evidence", +const emptyBenchmarkData = { + scenarios: [], } render(await loadBenchmark()) @@ -20,60 +13,67 @@ async function loadBenchmark() { try { const response = await fetch("./benchmark.json", { cache: "no-store" }) if (!response.ok) { - return fallbackData + return emptyBenchmarkData } return await response.json() } catch { - return fallbackData + return emptyBenchmarkData } } function render(data) { - const local = data.comparisons.find((item) => item.key === "local plaintext") - setText("hero-speedup", formatRatio(local?.writeSpeedup)) - setText("hero-current-wps", formatNumber(local?.current.writesPerSecond)) - setText("hero-baseline-wps", formatNumber(local?.baseline.writesPerSecond)) - renderRows(data.comparisons) - renderBars(data.comparisons) + const scenarios = data.scenarios ?? [] + const raw = scenarios.find((s) => s.key === "local-plaintext") + const orm = scenarios.find((s) => s.key === "orm-local-plaintext") + setText("hero-raw-wps", formatNumber(raw?.writesPerSecond)) + setText("hero-orm-wps", formatNumber(orm?.writesPerSecond)) + setText( + "hero-join-ms", + raw?.joinMs !== undefined ? `${numberFormatter.format(raw.joinMs)} ms` : "n/a", + ) + renderRows(scenarios) + renderBars(scenarios) } -function renderRows(comparisons) { +function renderRows(scenarios) { const target = document.getElementById("benchmark-rows") if (target === null) { return } target.replaceChildren() - if (comparisons.length === 0) { + if (scenarios.length === 0) { const row = document.createElement("tr") row.append(tableCell("Benchmark evidence pending", "left")) row.append(tableCell("n/a")) row.append(tableCell("n/a")) row.append(tableCell("n/a")) + row.append(tableCell("n/a")) target.append(row) return } - for (const item of comparisons) { + for (const item of scenarios) { const row = document.createElement("tr") - row.append(tableCell(item.scenario, "left")) - row.append(tableCell(formatNumber(item.baseline.writesPerSecond))) - row.append(tableCell(formatNumber(item.current.writesPerSecond))) - row.append(tableCell(formatPercent(item.writesPerSecondChangePct))) + row.append(tableCell(item.label, "left")) + row.append(tableCell(formatNumber(item.writesPerSecond))) + row.append(tableCell(`${formatNumber(item.writeMs)} ms`)) + row.append(tableCell(`${formatNumber(item.joinMs)} ms`)) + row.append(tableCell(item.reopenMs > 0 ? `${formatNumber(item.reopenMs)} ms` : "—")) target.append(row) } } -function renderBars(comparisons) { +function renderBars(scenarios) { const target = document.getElementById("chart-bars") if (target === null) { return } target.replaceChildren() - const max = maxThroughput(comparisons) + const max = scenarios.reduce((m, s) => Math.max(m, s.writesPerSecond ?? 0), 0) if (max === 0) { target.append(emptyState()) return } - for (const item of comparisons) { + for (const item of scenarios) { target.append(barRow(item, max)) } } @@ -84,13 +84,12 @@ function barRow(item, max) { const label = document.createElement("div") label.className = "bar-label" - label.append(span(item.scenario)) - label.append(span(formatPercent(item.writesPerSecondChangePct))) + label.append(span(item.label)) + label.append(span(`${formatNumber(item.writesPerSecond)} w/s`)) const bars = document.createElement("div") bars.className = "bars" - bars.append(bar("previous", item.baseline.writesPerSecond, max)) - bars.append(bar("current", item.current.writesPerSecond, max)) + bars.append(bar(item.key, item.writesPerSecond, max)) row.append(label) row.append(bars) @@ -99,7 +98,7 @@ function barRow(item, max) { function bar(kind, value, max) { const element = document.createElement("div") - element.className = `bar ${kind}` + element.className = `bar ${kind === "orm-local-plaintext" ? "orm" : "current"}` element.style.width = `${Math.max(4, (value / max) * 100)}%` return element } @@ -125,12 +124,6 @@ function emptyState() { return element } -function maxThroughput(comparisons) { - return comparisons.reduce((max, item) => { - return Math.max(max, item.baseline.writesPerSecond, item.current.writesPerSecond) - }, 0) -} - function setText(id, value) { const element = document.getElementById(id) if (element !== null) { @@ -141,11 +134,3 @@ function setText(id, value) { function formatNumber(value) { return typeof value === "number" ? numberFormatter.format(value) : "n/a" } - -function formatPercent(value) { - return typeof value === "number" ? `${percentFormatter.format(value)}%` : "n/a" -} - -function formatRatio(value) { - return typeof value === "number" ? `${numberFormatter.format(value)}x` : "n/a" -} diff --git a/site/assets/runtime-map.svg b/site/assets/runtime-map.svg index ff803f6..a10ca30 100644 --- a/site/assets/runtime-map.svg +++ b/site/assets/runtime-map.svg @@ -10,7 +10,7 @@ App code ORM repository - Postgres facade + SQL engine API diff --git a/site/benchmark.json b/site/benchmark.json index cc68a96..78ab540 100644 --- a/site/benchmark.json +++ b/site/benchmark.json @@ -14,21 +14,21 @@ "writesPerSecond": 173.14 }, "current": { - "joinMs": 3.2837920000000054, + "joinMs": 4.881749999999954, "label": "local plaintext throttled visible snapshots", - "reopenMs": 47.42029099999999, + "reopenMs": 77.61854200000005, "rows": 250, - "writeMs": 97.552042, - "writesPerSecond": 2562.7346683322116 + "writeMs": 146.747167, + "writesPerSecond": 1703.6104008740422 }, - "joinMsChangePct": 90.04006066120715, + "joinMsChangePct": 85.19335759781633, "key": "local plaintext", - "reopenMsChangePct": 66.14045626561943, + "reopenMsChangePct": 44.57797786504817, "rows": 250, "scenario": "local plaintext throttled visible snapshots", - "writeMsChangePct": 93.24385054366647, - "writeSpeedup": 14.801517086359084, - "writesPerSecondChangePct": 1380.1517086359086 + "writeMsChangePct": 89.83674998268579, + "writeSpeedup": 9.839496366374277, + "writesPerSecondChangePct": 883.9496366374277 }, { "baseline": { @@ -40,53 +40,56 @@ "writesPerSecond": 328.09 }, "current": { - "joinMs": 0.865791999999999, + "joinMs": 1.1787500000000364, "label": "local encrypted mutation log", - "reopenMs": 45.26150000000001, + "reopenMs": 78.30991699999993, "rows": 250, - "writeMs": 97.225708, - "writesPerSecond": 2571.3363794686893 + "writeMs": 234.367166, + "writesPerSecond": 1066.7023212628683 }, - "joinMsChangePct": 79.91201856148494, + "joinMsChangePct": 72.65081206496436, "key": "local encrypted", - "reopenMsChangePct": 85.96586152367368, + "reopenMsChangePct": 75.71860810517505, "rows": 250, "scenario": "local encrypted mutation log", - "writeMsChangePct": 87.2405532881009, - "writeSpeedup": 7.837289705473161, - "writesPerSecondChangePct": 683.7289705473161 - }, - { - "baseline": { - "joinMs": 19.74, - "label": "postgres facade over local encrypted", - "reopenMs": 0, - "rows": 250, - "writeMs": 987.93, - "writesPerSecond": 253.05 - }, - "current": { - "joinMs": 2.3100420000000668, - "label": "postgres facade over local encrypted", - "reopenMs": 0, - "rows": 250, - "writeMs": 148.463792, - "writesPerSecond": 1683.9122632675312 - }, - "joinMsChangePct": 88.29765957446774, - "key": "postgres facade", - "reopenMsChangePct": null, - "rows": 250, - "scenario": "postgres facade over local encrypted", - "writeMsChangePct": 84.9722356847145, - "writeSpeedup": 6.65446458513152, - "writesPerSecondChangePct": 565.446458513152 + "writeMsChangePct": 69.24275042979566, + "writeSpeedup": 3.251249112325485, + "writesPerSecondChangePct": 225.1249112325485 } ], "current": { "label": "current working tree", "source": ".gitdb/bench-current.json" }, - "generatedAt": "2026-06-13T16:15:58.844Z", - "headline": "Local plaintext writes are 14.80x of the previous documented run" + "generatedAt": "2026-06-14T01:35:43.313Z", + "headline": "Local plaintext writes are 9.84x of the previous documented run", + "scenarios": [ + { + "joinMs": 4.881749999999954, + "label": "local plaintext throttled visible snapshots", + "reopenMs": 77.61854200000005, + "rows": 250, + "writeMs": 146.747167, + "writesPerSecond": 1703.6104008740422, + "key": "local-plaintext" + }, + { + "joinMs": 1.1787500000000364, + "label": "local encrypted mutation log", + "reopenMs": 78.30991699999993, + "rows": 250, + "writeMs": 234.367166, + "writesPerSecond": 1066.7023212628683, + "key": "local-encrypted" + }, + { + "joinMs": 1.30266599999959, + "label": "orm local plaintext", + "reopenMs": 136.289667, + "rows": 250, + "writeMs": 5377.3372500000005, + "writesPerSecond": 46.49141171125169, + "key": "orm-local-plaintext" + } + ] } diff --git a/site/benchmark.md b/site/benchmark.md index de8f37c..db99375 100644 --- a/site/benchmark.md +++ b/site/benchmark.md @@ -1,5 +1,4 @@ | Scenario | Previous writes/s | Current writes/s | Change | Write ms change | Join ms change | | --- | ---: | ---: | ---: | ---: | ---: | -| local plaintext throttled visible snapshots | 173.14 | 2562.73 | +1380.15% | +93.24% | +90.04% | -| local encrypted mutation log | 328.09 | 2571.34 | +683.73% | +87.24% | +79.91% | -| postgres facade over local encrypted | 253.05 | 1683.91 | +565.45% | +84.97% | +88.30% | +| local plaintext throttled visible snapshots | 173.14 | 1703.61 | +883.95% | +89.84% | +85.19% | +| local encrypted mutation log | 328.09 | 1066.70 | +225.12% | +69.24% | +72.65% | diff --git a/site/favicon.svg b/site/favicon.svg new file mode 100644 index 0000000..f9dbfec --- /dev/null +++ b/site/favicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/site/index.html b/site/index.html index ea9a586..47b772d 100644 --- a/site/index.html +++ b/site/index.html @@ -6,8 +6,9 @@ GitDB - GitHub-backed database runtime + @@ -17,7 +18,7 @@ GitDB