Schemas & Stores
This reference documents the schema definition functions and store API for TypeGraph.
Schema Definition
Section titled “Schema Definition”defineNode(name, options)
Section titled “defineNode(name, options)”Creates a node type definition.
import { defineNode } from "@nicia-ai/typegraph";
function defineNode<K extends string, S extends z.ZodObject<any>>( name: K, options: { schema: S; description?: string; },): NodeType<K, S>;Parameters:
| Parameter | Type | Description |
|---|---|---|
name | string | Unique name for this node type |
options.schema | z.ZodObject | Zod object schema for node properties |
options.description | string | Optional description |
Example:
const Person = defineNode("Person", { schema: z.object({ name: z.string(), email: z.string().email().optional(), }), description: "A person in the system",});defineEdge(name, options?)
Section titled “defineEdge(name, options?)”Creates an edge type definition.
import { defineEdge } from "@nicia-ai/typegraph";
function defineEdge<K extends string, S extends z.ZodObject<any>>( name: K, options?: { schema?: S; description?: string; from?: NodeType[]; to?: NodeType[]; },): EdgeType<K, S>;Parameters:
| Parameter | Type | Description |
|---|---|---|
name | string | Unique name for this edge type |
options.schema | z.ZodObject | Optional Zod object schema (defaults to empty object) |
options.description | string | Optional description |
options.from | NodeType[] | Optional domain constraint (valid source node types) |
options.to | NodeType[] | Optional range constraint (valid target node types) |
Example:
const worksAt = defineEdge("worksAt", { schema: z.object({ role: z.string(), startDate: z.string().optional(), }),});
const knows = defineEdge("knows"); // No schema neededWith Domain/Range Constraints:
When from and to are specified, the edge carries its endpoint constraints intrinsically:
const worksAt = defineEdge("worksAt", { schema: z.object({ role: z.string(), startDate: z.string().optional(), }), from: [Person], // Domain: only Person can be the source to: [Company], // Range: only Company can be the target});Unconstrained Edges:
Edges without from/to are unconstrained — they can connect any node type to any node type:
const sameAs = defineEdge("sameAs");const related = defineEdge("related", { schema: z.object({ reason: z.string() }),});Direct use in defineGraph:
Any edge type can be used directly in defineGraph without an EdgeRegistration wrapper:
const graph = defineGraph({ id: "my_graph", nodes: { Person: { type: Person }, Company: { type: Company } }, edges: { worksAt, // Constrained — uses built-in from/to sameAs, // Unconstrained — connects any node to any node },});See Core Concepts for detailed documentation on domain/range constraints.
embedding(dimensions)
Section titled “embedding(dimensions)”Creates a Zod schema for vector embeddings with dimension validation.
import { embedding } from "@nicia-ai/typegraph";
function embedding<D extends number>(dimensions: D): EmbeddingSchema<D>;Parameters:
| Parameter | Type | Description |
|---|---|---|
dimensions | number | The number of dimensions (e.g., 384, 512, 768, 1536, 3072) |
Example:
const Document = defineNode("Document", { schema: z.object({ title: z.string(), content: z.string(), embedding: embedding(1536), // OpenAI ada-002 }),});
// Optional embeddingsconst Article = defineNode("Article", { schema: z.object({ content: z.string(), embedding: embedding(1536).optional(), }),});See Semantic Search for query usage.
externalRef(table)
Section titled “externalRef(table)”Creates a Zod schema for referencing external data sources. Use this for hybrid overlay patterns where TypeGraph stores relationships while your existing tables remain the source of truth.
import { externalRef } from "@nicia-ai/typegraph";
function externalRef<T extends string>(table: T): ExternalRefSchema<T>;Parameters:
| Parameter | Type | Description |
|---|---|---|
table | string | Identifier for the external table (e.g., “users”, “documents”) |
Example:
const Document = defineNode("Document", { schema: z.object({ source: externalRef("documents"), embedding: embedding(1536).optional(), }),});
// Create with explicit table referenceawait store.nodes.Document.create({ source: { table: "documents", id: "doc_123" },});
// Query the external referenceconst results = await store .query() .from("Document", "d") .select((ctx) => ctx.d.source) .execute();// results[0].source = { table: "documents", id: "doc_123" }createExternalRef(table)
Section titled “createExternalRef(table)”Factory helper to create external reference values without repeating the table name.
import { createExternalRef } from "@nicia-ai/typegraph";
function createExternalRef<T extends string>( table: T): (id: string) => ExternalRefValue<T>;Example:
const docRef = createExternalRef("documents");
await store.nodes.Document.create({ source: docRef("doc_123"), // { table: "documents", id: "doc_123" }});defineGraph(config)
Section titled “defineGraph(config)”Creates a graph definition combining nodes, edges, and ontology.
import { defineGraph } from "@nicia-ai/typegraph";
function defineGraph<G extends GraphDef>(config: { id: string; nodes: Record<string, NodeRegistration>; edges: Record<string, EdgeRegistration | EdgeType>; ontology?: OntologyRelation[];}): G;Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Unique identifier for this graph |
nodes | Record<string, NodeRegistration> | Node type registrations |
edges | Record<string, EdgeRegistration | EdgeType> | Edge registrations or edge types directly |
ontology | OntologyRelation[] | Optional semantic relationships |
Edge entries can be:
EdgeRegistration— explicit{ type, from, to }with optionalcardinalityEdgeTypewithfrom/to— uses built-in constraintsEdgeTypewithoutfrom/to— unconstrained, connects any node to any node
Example:
const graph = defineGraph({ id: "my_graph", nodes: { Person: { type: Person }, Company: { type: Company, onDelete: "cascade" }, }, edges: { worksAt: { type: worksAt, from: [Person], to: [Company], cardinality: "many", }, sameAs, // Unconstrained — any→any }, ontology: [disjointWith(Person, Company)],});Store Creation
Section titled “Store Creation”createStore(graph, backend, options?)
Section titled “createStore(graph, backend, options?)”Creates a store instance for a graph definition.
import { createStore } from "@nicia-ai/typegraph";
function createStore<G extends GraphDef>( graph: G, backend: GraphBackend, options?: StoreOptions): Store<G>;Options:
| Option | Type | Description |
|---|---|---|
hooks | StoreHooks | Observability hooks for monitoring operations |
schema | SqlSchema | Custom table name configuration |
queryDefaults.traversalExpansion | TraversalExpansion | Default ontology expansion mode for traversals (default: "inverse") |
Example:
const store = createStore(graph, backend);Override the default traversal expansion:
const store = createStore(graph, backend, { queryDefaults: { traversalExpansion: "none" },});createStoreWithSchema(graph, backend, options?)
Section titled “createStoreWithSchema(graph, backend, options?)”Creates a store and ensures the database schema is initialized or migrated. This is the recommended factory for production use.
import { createStoreWithSchema } from "@nicia-ai/typegraph";
function createStoreWithSchema<G extends GraphDef>( graph: G, backend: GraphBackend, options?: StoreOptions & SchemaManagerOptions): Promise<[Store<G>, SchemaValidationResult]>;Returns: A tuple of [store, validationResult]
The validation result indicates what happened:
status: "initialized"- Schema created for the first timestatus: "unchanged"- Schema matches, no changes neededstatus: "migrated"- Safe changes auto-applied (additive only)status: "pending"- Safe changes detected butautoMigrateisfalsestatus: "breaking"- Breaking changes detected, action required
Example:
const [store, result] = await createStoreWithSchema(graph, backend);
if (result.status === "initialized") { console.log("Schema initialized at version", result.version);} else if (result.status === "migrated") { console.log(`Migrated from v${result.fromVersion} to v${result.toVersion}`);} else if (result.status === "pending") { console.log(`Safe changes pending at version ${result.version}`);}Throws: MigrationError if breaking changes are detected and
throwOnBreaking is true (the default).
Store API
Section titled “Store API”The store provides typed node and edge collections via store.nodes.* and store.edges.*.
Node Collections
Section titled “Node Collections”Each node type has a collection with these methods:
Naming Guidelines
Section titled “Naming Guidelines”Method names follow what identifier is used to match an existing record:
| If you have… | Read-only | Get-or-create |
|---|---|---|
| ID | getById | upsertById |
| Unique constraint name + props | findByConstraint | getOrCreateByConstraint |
Edge endpoints (from, to) + optional matchOn | findByEndpoints | getOrCreateByEndpoints |
create(props, options?)
Section titled “create(props, options?)”Creates a new node.
store.nodes.Person.create( props: { name: string; email?: string }, options?: { id?: string; validFrom?: string; validTo?: string }): Promise<Node<Person>>;getById(id)
Section titled “getById(id)”Retrieves a node by ID.
store.nodes.Person.getById(id: NodeId<Person>): Promise<Node<Person> | undefined>;getByIds(ids)
Section titled “getByIds(ids)”Retrieves multiple nodes by ID in a single query. Returns results in input order,
with undefined for missing IDs.
store.nodes.Person.getByIds( ids: readonly NodeId<Person>[], options?: QueryOptions): Promise<readonly (Node<Person> | undefined)[]>;When the backend supports batch lookups (getNodes), this executes a single
SELECT ... WHERE id IN (...) query. Otherwise it falls back to sequential lookups.
const [alice, bob, unknown] = await store.nodes.Person.getByIds([ aliceId, bobId, "nonexistent",]);// alice: Node<Person>// bob: Node<Person>// unknown: undefinedupdate(id, props)
Section titled “update(id, props)”Updates node properties.
store.nodes.Person.update( id: NodeId<Person>, props: Partial<{ name: string; email?: string }>): Promise<Node<Person>>;delete(id)
Section titled “delete(id)”Soft-deletes a node.
store.nodes.Person.delete(id: NodeId<Person>): Promise<void>;hardDelete(id)
Section titled “hardDelete(id)”Permanently deletes a node. This is irreversible and should be used carefully.
store.nodes.Person.hardDelete(id: NodeId<Person>): Promise<void>;find(options?)
Section titled “find(options?)”Finds nodes of this kind with optional filtering and pagination.
store.nodes.Person.find(options?: { where?: (accessor) => Predicate; limit?: number; offset?: number;}): Promise<Node<Person>[]>;The optional where predicate uses the same accessor API as whereNode() in the query builder:
const activeUsers = await store.nodes.Person.find({ where: (p) => p.status.eq("active"), limit: 50,});count()
Section titled “count()”Counts nodes of this kind (excluding soft-deleted nodes).
store.nodes.Person.count(): Promise<number>;upsertById(id, props, options?)
Section titled “upsertById(id, props, options?)”Creates or updates a node by ID.
store.nodes.Person.upsertById( id: string, props: { name: string; email?: string }, options?: { validFrom?: string; validTo?: string }): Promise<Node<Person>>;Behavior:
- Creates a new node if no node with the ID exists
- Updates the existing node if one exists
- Un-deletes soft-deleted nodes (clears
deletedAt)
bulkCreate(items)
Section titled “bulkCreate(items)”Creates multiple nodes efficiently. Uses a single multi-row INSERT when the backend supports it.
store.nodes.Person.bulkCreate( items: readonly { props: { name: string; email?: string }; id?: string; validFrom?: string; validTo?: string; }[]): Promise<Node<Person>[]>;Use bulkInsert when you don’t need the created nodes back:
await store.nodes.Person.bulkInsert(batch);bulkInsert(items)
Section titled “bulkInsert(items)”Inserts multiple nodes without returning results. This is the dedicated fast path for bulk ingestion — wrapped in a transaction when the backend supports it.
store.nodes.Person.bulkInsert( items: readonly { props: { name: string; email?: string }; id?: string; validFrom?: string; validTo?: string; }[]): Promise<void>;bulkUpsertById(items)
Section titled “bulkUpsertById(items)”Creates or updates multiple nodes by ID.
store.nodes.Person.bulkUpsertById( items: readonly { id: string; props: { name: string; email?: string }; validFrom?: string; validTo?: string; }[]): Promise<Node<Person>[]>;bulkDelete(ids)
Section titled “bulkDelete(ids)”Soft-deletes multiple nodes.
store.nodes.Person.bulkDelete( ids: readonly NodeId<Person>[]): Promise<void>;getOrCreateByConstraint(constraintName, props, options?)
Section titled “getOrCreateByConstraint(constraintName, props, options?)”Looks up an existing node by a named uniqueness constraint. Returns the match if found, or creates a new node if not.
store.nodes.Person.getOrCreateByConstraint( constraintName: string, props: { name: string; email?: string }, options?: { ifExists?: "return" | "update" } // Default: "return"): Promise<{ node: Node<Person>; action: "created" | "found" | "updated" | "resurrected";}>;bulkGetOrCreateByConstraint(constraintName, items, options?)
Section titled “bulkGetOrCreateByConstraint(constraintName, items, options?)”Batch version of getOrCreateByConstraint. Returns results in input order.
store.nodes.Person.bulkGetOrCreateByConstraint( constraintName: string, items: readonly { props: { name: string; email?: string }; }[], options?: { ifExists?: "return" | "update" }): Promise< { node: Node<Person>; action: "created" | "found" | "updated" | "resurrected"; }[]>;findByConstraint(constraintName, props)
Section titled “findByConstraint(constraintName, props)”Looks up a node by a named uniqueness constraint without creating.
Returns the matching node or undefined. Soft-deleted nodes are excluded.
store.nodes.Person.findByConstraint( constraintName: string, props: { name: string; email?: string }): Promise<Node<Person> | undefined>;const alice = await store.nodes.Person.findByConstraint("email", { email: "alice@example.com", name: "Alice",});
if (alice) { console.log(alice.id, alice.name);}Throws NodeConstraintNotFoundError if the constraint name is not defined on the node type.
bulkFindByConstraint(constraintName, items)
Section titled “bulkFindByConstraint(constraintName, items)”Batch version of findByConstraint. Returns results in input order,
with undefined for non-matches. Deduplicates within-batch lookups automatically.
store.nodes.Person.bulkFindByConstraint( constraintName: string, items: readonly { props: { name: string; email?: string } }[]): Promise<(Node<Person> | undefined)[]>;const results = await store.nodes.Person.bulkFindByConstraint("email", [ { props: { email: "alice@example.com", name: "Alice" } }, { props: { email: "nobody@example.com", name: "Nobody" } }, { props: { email: "bob@example.com", name: "Bob" } },]);// results[0]: Node<Person> (Alice)// results[1]: undefined// results[2]: Node<Person> (Bob)Edge Collections
Section titled “Edge Collections”Each edge type has a type-safe collection. The from and to parameters are
constrained to only accept node types declared in the edge registration.
create(from, to, props)
Section titled “create(from, to, props)”Creates an edge. TypeScript enforces valid endpoint types.
// Given: worksAt: { type: worksAt, from: [Person], to: [Company] }
store.edges.worksAt.create( from: NodeRef<Person>, to: NodeRef<Company>, props: { role: string }): Promise<Edge<worksAt>>;
// Preferred: Pass node objects directlyawait store.edges.worksAt.create(alice, acme, { role: "Engineer" });
// Compile error - Company is not a valid 'from' typeawait store.edges.worksAt.create(acme, alice, { role: "Engineer" });Node References
Section titled “Node References”Both forms are exactly equivalent—TypeGraph extracts kind and id from either:
// Full node object (preferred - cleaner syntax)await store.edges.worksAt.create(alice, acme, { role: "Engineer" });
// Explicit reference (useful when you only have IDs)await store.edges.worksAt.create( { kind: "Person", id: aliceId }, { kind: "Company", id: acmeId }, { role: "Engineer" });Use the explicit { kind, id } form when you have IDs but not the full node objects (e.g., from a
previous query or external input).
getById(id)
Section titled “getById(id)”Retrieves an edge by ID.
store.edges.worksAt.getById(id: EdgeId<worksAt>): Promise<Edge<worksAt> | undefined>;getByIds(ids)
Section titled “getByIds(ids)”Retrieves multiple edges by ID in a single query. Returns results in input order,
with undefined for missing IDs.
store.edges.worksAt.getByIds( ids: readonly EdgeId<worksAt>[], options?: QueryOptions): Promise<readonly (Edge<worksAt> | undefined)[]>;const [edge1, edge2] = await store.edges.worksAt.getByIds([id1, id2]);update(id, props, options?)
Section titled “update(id, props, options?)”Updates edge properties.
store.edges.worksAt.update( id: EdgeId<worksAt>, props: Partial<{ role: string }>, options?: { validTo?: string }): Promise<Edge<worksAt>>;findFrom(from)
Section titled “findFrom(from)”Finds edges from a node.
store.edges.worksAt.findFrom( from: NodeRef<Person>): Promise<Edge<worksAt>[]>;findTo(to)
Section titled “findTo(to)”Finds edges to a node.
store.edges.worksAt.findTo( to: NodeRef<Company>): Promise<Edge<worksAt>[]>;find(options?)
Section titled “find(options?)”Finds edges with endpoint filtering.
store.edges.worksAt.find(options?: { from?: NodeRef<Person>; to?: NodeRef<Company>; limit?: number; offset?: number;}): Promise<Edge<worksAt>[]>;For edge property filters, use the query builder with whereEdge(...).
count(options?)
Section titled “count(options?)”Counts edges matching filters.
store.edges.worksAt.count(options?: { from?: NodeRef<Person>; to?: NodeRef<Company>;}): Promise<number>;delete(id)
Section titled “delete(id)”Soft-deletes an edge.
store.edges.worksAt.delete(id: EdgeId<worksAt>): Promise<void>;hardDelete(id)
Section titled “hardDelete(id)”Permanently deletes an edge. This is irreversible and should be used carefully.
store.edges.worksAt.hardDelete(id: EdgeId<worksAt>): Promise<void>;bulkCreate(items)
Section titled “bulkCreate(items)”Creates multiple edges efficiently. Uses a single multi-row INSERT when the backend supports it.
store.edges.worksAt.bulkCreate( items: readonly { from: NodeRef<Person>; to: NodeRef<Company>; props?: { role: string }; id?: string; validFrom?: string; validTo?: string; }[]): Promise<Edge<worksAt>[]>;Use bulkInsert for high-volume edge ingestion when you do not need returned payloads:
await store.edges.worksAt.bulkInsert(edgeBatch);bulkInsert(items)
Section titled “bulkInsert(items)”Inserts multiple edges without returning results. This is the dedicated fast path for bulk ingestion — wrapped in a transaction when the backend supports it.
store.edges.worksAt.bulkInsert( items: readonly { from: NodeRef<Person>; to: NodeRef<Company>; props?: { role: string }; id?: string; validFrom?: string; validTo?: string; }[]): Promise<void>;bulkDelete(ids)
Section titled “bulkDelete(ids)”Soft-deletes multiple edges.
store.edges.worksAt.bulkDelete( ids: readonly EdgeId<worksAt>[]): Promise<void>;bulkUpsertById(items)
Section titled “bulkUpsertById(items)”Creates or updates multiple edges by ID.
store.edges.worksAt.bulkUpsertById( items: readonly { id: EdgeId<worksAt>; from: NodeRef<Person>; to: NodeRef<Company>; props?: { role: string }; validFrom?: string; validTo?: string; }[]): Promise<Edge<worksAt>[]>;getOrCreateByEndpoints(from, to, props, options?)
Section titled “getOrCreateByEndpoints(from, to, props, options?)”Looks up an existing edge by endpoints (and optionally by property fields via matchOn).
Returns the match if found, or creates a new edge if not.
store.edges.worksAt.getOrCreateByEndpoints( from: NodeRef<Person>, to: NodeRef<Company>, props: { role: string }, options?: { matchOn?: readonly ("role")[]; // Default: [] ifExists?: "return" | "update"; // Default: "return" }): Promise<{ edge: Edge<worksAt>; action: "created" | "found" | "updated" | "resurrected";}>;bulkGetOrCreateByEndpoints(items, options?)
Section titled “bulkGetOrCreateByEndpoints(items, options?)”Batch version of getOrCreateByEndpoints. Returns results in input order.
store.edges.worksAt.bulkGetOrCreateByEndpoints( items: readonly { from: NodeRef<Person>; to: NodeRef<Company>; props: { role: string }; }[], options?: { matchOn?: readonly ("role")[]; ifExists?: "return" | "update"; }): Promise< { edge: Edge<worksAt>; action: "created" | "found" | "updated" | "resurrected"; }[]>;findByEndpoints(from, to, options?)
Section titled “findByEndpoints(from, to, options?)”Looks up an edge by its endpoints without creating. Returns the matching edge or undefined. Soft-deleted edges are excluded.
When matchOn is omitted, returns the first live edge between the two endpoints.
When matchOn is provided, filters by the specified property fields.
store.edges.knows.findByEndpoints( from: NodeRef<Person>, to: NodeRef<Person>, options?: { matchOn?: readonly ("relationship" | "since")[]; props?: Partial<{ relationship: string; since: string }>; }): Promise<Edge<knows> | undefined>;// Find any edge between Alice and Bobconst edge = await store.edges.knows.findByEndpoints(alice, bob);
// Find the specific "colleague" edge between Alice and Bobconst colleague = await store.edges.knows.findByEndpoints(alice, bob, { matchOn: ["relationship"], props: { relationship: "colleague" },});Transactions
Section titled “Transactions”store.transaction(fn)
Section titled “store.transaction(fn)”Executes a callback within an atomic transaction. All operations succeed together or are
rolled back together. The transaction context (tx) provides the same nodes.* and
edges.* collection API as the store itself.
await store.transaction(async (tx) => { const person = await tx.nodes.Person.create({ name: "Alice" }); const company = await tx.nodes.Company.create({ name: "Acme" }); await tx.edges.worksAt.create(person, company, { role: "Engineer" });});Return values
Section titled “Return values”The callback’s return value is forwarded to the caller:
const personId = await store.transaction(async (tx) => { const person = await tx.nodes.Person.create({ name: "Alice" }); return person.id;});// personId is available hereRollback and error propagation
Section titled “Rollback and error propagation”If the callback throws, the transaction is rolled back and the error re-throws to the caller. No partial writes are persisted.
try { await store.transaction(async (tx) => { await tx.nodes.Person.create({ name: "Alice" }); throw new Error("something went wrong"); // Alice is NOT persisted — the entire transaction is rolled back });} catch (error) { // error.message === "something went wrong"}Nesting
Section titled “Nesting”Transactions do not nest. The transaction context intentionally omits the
transaction() method, so attempting to start a transaction inside another transaction is
a compile-time error. If you need to compose transactional operations, pass the tx
context through your call chain.
Backend support
Section titled “Backend support”Not all backends support atomic transactions. Cloudflare D1, for example, does not —
calling store.transaction() on a D1-backed store throws a ConfigurationError. Check
support at runtime with:
if (backend.capabilities.transactions) { await store.transaction(async (tx) => { /* ... */ });} else { // fall back to individual operations with manual error handling}store.clear()
Section titled “store.clear()”Hard-deletes all data for the current graph: nodes, edges, uniqueness entries, embeddings, and schema versions. Resets collection caches so the store is immediately reusable.
store.clear(): Promise<void>;Wrapped in a transaction when the backend supports it. Does not affect other graphs sharing the same backend.
// Wipe all data and start freshawait store.clear();
// Store is immediately reusableconst person = await store.nodes.Person.create({ name: "Alice" });Query Builder
Section titled “Query Builder”store.query()
Section titled “store.query()”Creates a query builder. See Query Builder for full documentation.
const results = await store .query() .from("Person", "p") .whereNode("p", (p) => p.name.startsWith("A")) .select((ctx) => ctx.p) .execute();Execution methods (see Execute for details):
| Method | Returns | Description |
|---|---|---|
execute() | Promise<readonly T[]> | Run query, return all results |
first() | Promise<T | undefined> | Return first result or undefined |
count() | Promise<number> | Count matching results |
exists() | Promise<boolean> | Check if any results exist |
paginate(options) | Promise<PaginatedResult<T>> | Cursor-based pagination |
stream(options?) | AsyncIterable<T> | Stream results in batches |
prepare() | PreparedQuery<T> | Pre-compile query for repeated execution with parameters |
Registry Access
Section titled “Registry Access”store.registry
Section titled “store.registry”Access to the type registry for ontology lookups. The registry is an internal type;
use store.registry directly without importing its type.
See Ontology for registry methods.
Observability Hooks
Section titled “Observability Hooks”TypeGraph supports observability hooks for monitoring and logging store operations.
StoreHooks
Section titled “StoreHooks”Configuration for observability callbacks:
import type { HookContext, QueryHookContext, OperationHookContext, StoreHooks,} from "@nicia-ai/typegraph";type StoreHooks = Readonly<{ onQueryStart?: (ctx: QueryHookContext) => void; onQueryEnd?: (ctx: QueryHookContext, result: { rowCount: number; durationMs: number }) => void; onOperationStart?: (ctx: OperationHookContext) => void; onOperationEnd?: (ctx: OperationHookContext, result: { durationMs: number }) => void; onError?: (ctx: HookContext, error: Error) => void;}>;
type HookContext = Readonly<{ operationId: string; graphId: string; startedAt: Date;}>;
type QueryHookContext = HookContext & Readonly<{ sql: string; params: readonly unknown[]; }>;
type OperationHookContext = HookContext & Readonly<{ operation: "create" | "update" | "delete"; entity: "node" | "edge"; kind: string; id: string; }>;Note: Batch operations (
bulkCreate,bulkInsert,bulkUpsertById) skip per-item operation hooks for throughput. Query hooks still fire normally.
Example:
import { createStore, type StoreHooks } from "@nicia-ai/typegraph";
const hooks: StoreHooks = { onQueryStart: (ctx) => { console.log(`[${ctx.operationId}] SQL: ${ctx.sql}`); }, onQueryEnd: (ctx, result) => { console.log(`[${ctx.operationId}] ${result.rowCount} rows in ${result.durationMs}ms`); }, onOperationStart: (ctx) => { console.log(`[${ctx.operationId}] ${ctx.operation} ${ctx.entity}:${ctx.kind}`); }, onOperationEnd: (ctx, result) => { console.log(`[${ctx.operationId}] Completed in ${result.durationMs}ms`); }, onError: (ctx, error) => { console.error(`[${ctx.operationId}] Error:`, error.message); },};
const store = createStore(graph, backend, { hooks });
// Operations now trigger hooksawait store.nodes.Person.create({ name: "Alice" });// Logs:// [op-abc123] create node:Person// [op-abc123] SQL: INSERT INTO ...// [op-abc123] 1 rows in 2ms// [op-abc123] Completed in 5ms