@linklabjs/core
v0.1.3
Published
LinkLab core — semantic navigation graph engine
Maintainers
Readme
@linklabjs/core
The graph is the map. The Trail is the traveler.
"The Trail defines the path, the history, and the intention.
The graph knows the possibilities."
LinkLab associates two concepts:
- The compiled graph — the map: entities, relations, optimal routes
- The Trail — the traveler: navigation, context, history, intention
The map knows all paths. The traveler decides where to go — and by traveling, enriches the map.
The problem LinkLab solves
In every application, we write the same SQL joins by hand:
-- Get all actors in films directed by Nolan
SELECT people.*
FROM directors
INNER JOIN credits ON directors.id = credits.personId AND credits.jobId = 2
INNER JOIN movies ON credits.movieId = movies.id
INNER JOIN credits c2 ON movies.id = c2.movieId AND c2.jobId = 1
INNER JOIN people ON c2.personId = people.id
WHERE directors.name = 'Nolan'With LinkLab:
cinema.directors('Nolan').movies.actorsLinkLab generates the SQL, finds the optimal path in the graph, and improves continuously from usage traces.
Installation
npm install @linklabjs/coreQuick start
import { Graph } from '@linklabjs/core'
import compiledGraph from './linklab/netflix/netflix.json'
import * as dataset from './data'
const graph = new Graph(compiledGraph, { dataset })
const netflix = graph.domain()
// Fluent navigation
const actors = await netflix.movies(278).actors
const films = await netflix.directors('Nolan').movies
const colleagues = await netflix.actors('DiCaprio').movies.directorsThe result is a plain JavaScript array — map, filter, sort as usual:
const titles = await netflix.directors('Nolan').movies
.then(films => films.filter(f => f.release_year > 2000))
.then(films => films.map(f => f.title))
// ['Interstellar', 'Inception', 'The Dark Knight'...]How it works
Your database or JSON files
↓ linklab build
{alias}.json (compiled graph — precalculated routes)
↓ QueryEngine
SQL generated automatically
↓ NavigationEngine
Fluent API: cinema.directors('Nolan').movies.actorslinklab build is a CLI command from @linklabjs/cli. It produces the compiled graph that @linklabjs/core consumes at runtime.
Semantic views
When the same entity appears in multiple roles — actors, directors, writers all being people — LinkLab detects this at compile time and generates semantic views automatically:
netflix.movies(278).people → everyone (all roles)
netflix.movies(278).actors → actors only
netflix.movies(278).director → director only
netflix.movies(278).writers → writers onlypeople('Christopher Nolan').director and directors('Christopher Nolan') are equivalent — same entity, filtered by role. No separate endpoint to maintain.
API levels
Level 1 cinema.directors('Nolan').movies.actors
→ semantic facade, transparent, 80% of use cases
Level 2 graph.from('Pigalle').to('Alesia').path(Strategy.Shortest)
→ paths, strategies, Dijkstra
Level 3 graph.entities / .relations / .weights
→ introspection, debug, dashboards
Level 4 graph.weight(edge).set(value) / .compile()
→ metaprogramming, CalibrationJobLevel 1 — Semantic facade
new Graph(compiledGraph, options?) → Graph
Main entry point. Builds a navigable graph.
import { Graph } from '@linklabjs/core'
const graph = new Graph(compiledGraph, {
compiled?: CompiledGraph, // precalculated routes
dataset?: Record<string, any[]>, // JSON data in memory
provider?: Provider, // PostgresProvider for real database
})graph.domain() → DomainProxy
Returns the transparent semantic proxy (Level 1).
const cinema = graph.domain()
const cast = await cinema.movies(278).people
const films = await cinema.directors('Nolan').movies
const found = await cinema.movies({ title: 'Inception' })Level 2 — Pathfinding
graph.from(nodeId) → PathBuilder
const builder = graph.from('Pigalle').to('Alesia')
builder.paths() // all paths — Shortest by default
builder.paths(Strategy.Comfort()) // +8 min per transfer
builder.path() // best path only
builder.links // subgraph between two nodesStrategy
import { Strategy } from '@linklabjs/core'
Strategy.Shortest() // minimal raw weight (default)
Strategy.Comfort() // +8 min per transfer
Strategy.Custom(penalty) // +penalty per transferLevel 3 — Introspection
graph.entities // GraphNode[] — all nodes
graph.relations // GraphEdge[] — all edges
graph.schema // Record<string, string> — node types
graph.weights // Map<string, number> — current weightsFastify plugin — REST + HATEOAS
import Fastify from 'fastify'
import { linklabPlugin } from '@linklabjs/core'
const app = Fastify()
await app.register(linklabPlugin, {
graph: compiledGraph,
prefix: '/api',
dataLoader: { provider: postgresProvider },
onEngine: (engine, req) => {
engine.hooks.on('access.check', async (ctx) => {
if (!req.user) return { cancelled: true, reason: 'unauthenticated' }
})
}
})
// These routes work automatically — no configuration:
// GET /api/movies
// GET /api/movies/278
// GET /api/movies/278/people
// GET /api/directors/2/moviesResponse includes _links generated from the graph:
{
"id": 504,
"name": "Tim Robbins",
"_links": {
"self": { "href": "/api/movies/278/people/504" },
"up": { "href": "/api/movies/278" },
"movies": { "href": "/api/movies/278/people/504/movies" },
"credits": { "href": "/api/movies/278/people/504/credits" }
}
}Low-level API
QueryEngine
import { QueryEngine } from '@linklabjs/core'
const engine = new QueryEngine(compiledGraph)
engine.getRoute(from, to) // RouteInfo
engine.generateSQL(options: QueryOptions) // string — readable SQL
engine.executeInMemory(options, dataset) // any[] — JSON execution
engine.generateJSONPipeline(options) // object — debuginterface QueryOptions {
from: string
to: string
filters?: Record<string, any> // WHERE conditions
semantic?: string // semantic view label — ex: 'actor'
}PathFinder
import { PathFinder } from '@linklabjs/core'
const finder = new PathFinder(graph)
finder.findShortestPath(from, to) // PathDetails | null
finder.findAllPaths(from, to, maxPaths?) // Path[]
finder.hasPath(from, to) // boolean
finder.getReachableNodes(from, maxDepth?) // Set<string>
finder.getPathWeight(path) // number
finder.getStats() // { nodes, edges, avgDegree }GraphCompiler
import { GraphCompiler } from '@linklabjs/core'
const compiler = new GraphCompiler({
weightThreshold?: number, // pruning threshold (default: 1000)
keepFallbacks?: boolean, // keep alternative routes
maxFallbacks?: number, // max alternatives per route
})
compiler.compile(graph, metrics): CompiledGraphCore types
interface GraphNode {
id: string
type: string
label?: string
[key: string]: any
}
interface GraphEdge {
from: string
to: string
weight: number
name?: string
via?: string
metadata?: Record<string, any>
}
interface CompiledGraph {
version: string
compiledAt: string
nodes: GraphNode[]
routes: RouteInfo[]
}
interface RouteInfo {
from: string
to: string
semantic?: boolean
label?: string
primary: {
path: string[]
edges: RouteStep[]
weight: number
joins: number
}
fallbacks: RouteInfo['primary'][]
}
interface Provider {
query<T>(sql: string, params?: any[]): Promise<T[]>
close(): Promise<void>
}Recommended imports
import {
Graph,
Strategy,
PathFinder,
QueryEngine,
GraphCompiler,
NavigationEngine,
linklabPlugin,
} from '@linklabjs/core'
import type {
GraphNode,
GraphEdge,
CompiledGraph,
RouteInfo,
QueryOptions,
} from '@linklabjs/core'Examples
| Example | Source | Demonstrates |
|---------|--------|-------------|
| dvdrental | PostgreSQL | FK relations, semantic views, full pipeline |
| netflix | JSON | Pivot detection, semantic views (actors/directors/writers) |
| cinema | JSON | Minimal graph, REPL starting point |
| metro | GTFS open data | Dijkstra, real RATP weights, strategies |
| musicians | Manual | Cycles, minHops, via filter |
See the examples folder.
Custom formatters
Extend BaseFormatter to transform raw navigation results into domain-readable output:
import { BaseFormatter } from '@linklabjs/core'
import type { NavigationPath } from '@linklabjs/core'
export class MyFormatter extends BaseFormatter {
format(path: NavigationPath): string {
return [
`Path: ${path.nodes.join(' → ')}`,
`Weight: ${path.totalWeight}`,
].join('\n')
}
}Not an ORM
LinkLab does not map tables to objects. It does not manage migrations. It does not hide your SQL.
It compiles a navigation graph from your existing schema and resolves paths through it. The generated SQL is readable — visible in the REPL and in QueryEngine.generateSQL().
License
MIT — Charley Simon
