@biref/scanner
v0.2.0
Published
Paradigm-agnostic database introspection and typed query builder. Bidirectional reference detection, codegen-driven autocomplete, and Prisma-style fluent queries against any table the scanner finds.
Maintainers
Readme
@biref/scanner
Scan any database, inspect every relationship in both directions, generate a typed schema, and write Prisma-style fluent queries with fully hydrated nested results. Zero runtime dependencies.
Supported adapters
| Adapter | Driver | Status |
| --- | --- | --- |
| Postgres | pg | Stable |
| MySQL / MariaDB | mysql2 | Stable |
Install
# Postgres
pnpm add @biref/scanner pg
# MySQL
pnpm add @biref/scanner mysql2Quick start
Postgres
import pg from 'pg';
import { Biref, postgresAdapter } from '@biref/scanner';
import type { BirefSchema } from './biref/biref.schema';
const client = new pg.Client({ connectionString: 'postgres://localhost/mydb' });
await client.connect();
const biref = Biref.builder()
.withAdapter(postgresAdapter.create(client))
.build();
const model = await biref.scan({ namespaces: 'all' });
const rows = await biref
.query<BirefSchema>(model)
.public.users
.select('id', 'email')
.where('status', 'eq', 'active')
.include('orders', (order) =>
order
.select('id', 'total')
.include('order_items', (item) => item.select('variant_id', 'quantity')),
)
.findMany();MySQL
import mysql from 'mysql2/promise';
import { Biref, mysqlAdapter } from '@biref/scanner';
import type { BirefSchema } from './biref/biref.schema';
const connection = await mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'secret',
database: 'mydb',
});
const biref = Biref.builder()
.withAdapter(mysqlAdapter.create(connection))
.build();
const model = await biref.scan();
const rows = await biref
.query<BirefSchema>(model)
.mydb.users
.select('id', 'email')
.where('is_active', 'eq', 1)
.include('orders', (order) => order.select('id', 'total'))
.findMany();What makes it different
Most introspection tools only surface the foreign keys an entity declares. @biref/scanner walks the graph once and attaches relationships in both directions:
- outbound: this entity holds the FK pointing outward
- inbound: another entity holds a FK pointing here
// Who references users?
const incoming = model.inboundRelationshipsOf('public', 'users');
// -> orders, invoices, sessions, api_keys, ...
// The query builder exposes both directions under friendly names.
// .include('orders', ...) on users walks the inbound hop.
// .include('user', ...) on orders walks the outbound hop.Pipeline
- Wire an adapter (bring your own driver)
- Scan the data store into a paradigm-neutral
DataModel - Generate a typed schema via
biref genCLI - Query with a Prisma-style fluent API with full type narrowing
Codegen
# Postgres
pnpm exec biref gen --url postgres://user:pass@localhost/mydb --all-namespaces
# MySQL
pnpm exec biref gen --url mysql://user:pass@localhost/mydb
# Split mode (one file per entity)
pnpm exec biref gen --url postgres://localhost/mydb --split --all-namespacesOutput:
biref/
biref.schema.ts # regenerated every run
biref.schema.overrides.ts # scaffolded once, yours to editType your JSON columns via overrides:
// biref.schema.overrides.ts
export interface Overrides {
'public.users': {
profile: { plan: 'free' | 'pro'; prefs: { darkMode: boolean } };
};
}Core concepts
| Concept | Description | | --- | --- | | Adapter | Bundles an Introspector, QueryEngine, RawQueryRunner, and RecordParser for a specific database | | DataModel | Paradigm-neutral schema produced by a scan. Every entity with relationships in both directions | | TypedChain | Fluent query builder with compile-time narrowing on select, where, include | | QueryPlan | Immutable tree built by the chain. One SQL query per include level |
Query builder
const q = biref.query<BirefSchema>(model);
// Select + filter + order + limit
const active = await q.public.products
.select('id', 'sku', 'name')
.where('active', 'eq', true)
.orderBy('sku', 'asc')
.limit(10)
.findMany();
// Nested includes (recursive)
const usersWithOrders = await q.public.users
.select('id', 'email')
.include('orders', (order) =>
order
.select('id', 'total', 'status')
.include('order_items', (item) =>
item.select('product_id', 'quantity'),
),
)
.findMany();
// Wildcard include
const everything = await q.public.users
.select('id', 'email')
.include('*')
.findMany();Filter operators
| Category | Operators |
| --- | --- |
| string, uuid, enum | eq, neq, in, not-in, like, ilike, is-null, is-not-null |
| integer, decimal, date, timestamp, time | eq, neq, lt, lte, gt, gte, between, in, not-in, is-null, is-not-null |
| boolean | eq, neq, is-null, is-not-null |
Local development
# Docker containers for testing
docker compose -f docker/postgres/docker-compose.yml up -d # localhost:5432
docker compose -f docker/mysql/docker-compose.yml up -d # localhost:3306
# Both use: user=biref password=biref_pass database=biref_demoRequirements
- Node.js 20+
- Your own database driver (
pgormysql2). Zero runtime dependencies.
License
MIT
