@valv/core
v0.7.2
Published
ORM + Access Control Layer for AI Agents
Downloads
1,333
Readme
@valv/core
The database-agnostic core of valv: the query grammar an LLM emits, the validator that checks it against your schema and policy, the policy injector, the shared SQL emitter, and the tool layer.
Most users install an adapter, not this package directly —
@valv/clickhouseor@valv/prismawrap core with introspection and a dialect. See the root README for the full guide. Reach for@valv/coredirectly only to build a custom adapter.
What this package exports
| Area | Exports |
|---|---|
| Orchestration | Valv (instantiated by adapters via createValv) |
| Query grammar | QuerySchema; types Query, Expr, SelectItem |
| Write grammar | InsertSchema, UpdateSchema, DeleteSchema; types Insert, Update, Delete, InjectedMutation |
| Policy | PolicyFn, PolicyResult, FieldPolicy, DefaultContext |
| Emission | emit, emitInsert/emitUpdate/emitDelete, the Dialect interface, BASE_FUNCTIONS, FnDef, ArgSpec |
| Output shape | resultSchema, ResultColumn |
| Tool formats | anthropic, openai, gemini formatters; NeutralTool, ToolToggle |
| Adapter contract | ValvAdapter, SchemaMap, CompiledQuery, BoundParam, MutationResult |
| Errors | ValidationError, PolicyViolationError; serializeResult |
Building an adapter
Everything above the database is shared, so a new adapter is three methods plus a small dialect. The shared emit does clause assembly, parenthesisation, and parameter ordering — your dialect only says how to quote identifiers and render placeholders.
import type { ValvAdapter, SchemaMap, Query, CompiledQuery, FnDef, Dialect } from "@valv/core"
import { emit, BASE_FUNCTIONS } from "@valv/core"
const myDialect: Dialect = {
quoteId: (id) => `"${id.replace(/"/g, '""')}"`,
placeholder: (i) => `$${i + 1}`,
// functions: { ...dialect-specific aggregates }
}
class MyAdapter implements ValvAdapter {
async introspect(): Promise<SchemaMap> {
// describe your tables → resources, fields (with coarse `type` + `nativeType`), relations
}
compile(query: Query, catalog: SchemaMap): CompiledQuery {
return emit(query, catalog, myDialect)
}
async execute(sql: string, params?: unknown[]): Promise<unknown[]> {
// run the parameterized statement, return rows
}
functions(): Record<string, FnDef> {
return { ...BASE_FUNCTIONS, ...myDialect.functions }
}
// Optional — implement to support writes. The mutation is already validated
// and policy-injected; emit it with emitInsert/emitUpdate/emitDelete and run it.
// mutate?(m: InjectedMutation, catalog: SchemaMap): Promise<MutationResult>
}Validation and policy injection never reach the adapter — security stays in core. A Dialect can also declare extra functions (FnDef: argument signature, return type, render), which become callable in the query grammar and are surfaced to the model through the query tool's enum. Writes are optional: implement mutate (the mutation arrives already validated and policy-injected) to opt in, or omit it for a read-only adapter.
Type-safe resource names
InferResources derives resource names from a typed client, so a misspelled policy key is a compile error:
import type { InferResources } from "@valv/core"
const valv = await createValv<typeof prisma, Ctx>(prisma) // policy keys autocompleteLicense
MIT
