Common Patterns
Recipes are short, focused patterns that solve a specific problem in a few code blocks. For complete, end-to-end implementations, see Examples.
| Pattern | Use Case |
|---|---|
| RBAC | Permission checks through role hierarchies |
| Social Network | Feeds, followers, friend recommendations |
| Content Versioning | Temporal queries and audit trails |
| Tagging System | Flexible categorization with tag clouds |
| Tree Navigation | Hierarchical menus, org charts, file systems |
| Weighted Relationships | Scoring, relevance, confidence levels |
| Soft Deletes | Safe deletion with relationship cleanup |
| Unique Constraints | Preventing duplicates |
Role-Based Access Control (RBAC)
Section titled “Role-Based Access Control (RBAC)”TypeGraph’s traversal capabilities make it excellent for modeling permission systems, where access can be inherited through roles or groups.
Schema Definition
Section titled “Schema Definition”import { z } from "zod";import { defineNode, defineEdge, defineGraph } from "@nicia-ai/typegraph";
// 1. Define Nodesconst User = defineNode("User", { schema: z.object({ username: z.string() }),});
const Role = defineNode("Role", { schema: z.object({ name: z.string() }),});
const Permission = defineNode("Permission", { schema: z.object({ action: z.string(), resource: z.string() }),});
const Resource = defineNode("Resource", { schema: z.object({ type: z.string(), externalId: z.string() }),});
// 2. Define Edgesconst hasRole = defineEdge("hasRole");const hasPermission = defineEdge("hasPermission");const appliesTo = defineEdge("appliesTo");
// 3. Define Graph (endpoints are specified here, not in defineEdge)const rbacGraph = defineGraph({ id: "rbac_system", nodes: { User: { type: User }, Role: { type: Role }, Permission: { type: Permission }, Resource: { type: Resource }, }, edges: { hasRole: { type: hasRole, from: [User], to: [Role] }, hasPermission: { type: hasPermission, from: [Role, User], to: [Permission] }, appliesTo: { type: appliesTo, from: [Permission], to: [Resource] }, },});Checking Permissions
Section titled “Checking Permissions”To check if a user has a specific permission, we can query for a path from the User to the Permission, either directly or through a Role.
async function checkPermission(userId: string, action: string, resourceId: string) { const result = await store .query() .from("User", "u") .whereNode("u", (p) => p.id.eq(userId)) // Traverse optional roles .optionalTraverse("hasRole", "r_edge") .to("Role", "r") // From either User or Role, look for permissions .traverse("hasPermission", "p_edge") .to("Permission", "p") .whereNode("p", (p) => p.action.eq(action)) .execute();
return result.length > 0;}Social Network (Followers & Feeds)
Section titled “Social Network (Followers & Feeds)”Modeling social features requires efficient handling of relationships and recursive queries for recommendations.
Schema Definition
Section titled “Schema Definition”const User = defineNode("User", { schema: z.object({ handle: z.string() }),});
const Post = defineNode("Post", { schema: z.object({ content: z.string(), timestamp: z.string() }),});
const follows = defineEdge("follows");const authored = defineEdge("authored");
const socialGraph = defineGraph({ id: "social", nodes: { User: { type: User }, Post: { type: Post }, }, edges: { follows: { type: follows, from: [User], to: [User] }, authored: { type: authored, from: [User], to: [Post] }, },});Generating a Feed
Section titled “Generating a Feed”Retrieve posts from users that the current user follows, ordered by time.
const feed = await store .query() .from("User", "me") .whereNode("me", (u) => u.id.eq(currentUserId)) .traverse("follows", "f") .to("User", "author") .traverse("authored", "p") .to("Post", "post") .select((ctx) => ({ author: ctx.author.handle, content: ctx.post.content, date: ctx.post.timestamp, })) .orderBy("post", "timestamp", "desc") .execute();Friend Recommendations
Section titled “Friend Recommendations”Find “Friends of Friends” that the user doesn’t follow yet.
const recommendations = await store .query() .from("User", "me") .whereNode("me", (u) => u.id.eq(currentUserId)) .traverse("follows", "f1") .to("User", "friend") .traverse("follows", "f2") .to("User", "fof") // Exclude people I already follow (simplified - in practice use EXCEPT or client filtering) .select((ctx) => ({ handle: ctx.fof.handle, })) .limit(10) .execute();Content Versioning with History
Section titled “Content Versioning with History”TypeGraph has built-in support for temporal data. Every node and edge tracks valid_from and
valid_to timestamps, allowing you to travel through time without complex schema changes.
Enabling Temporal Mode
Section titled “Enabling Temporal Mode”Ensure your graph definition allows for history. By default, TypeGraph uses
temporalMode: "current", which only returns currently valid data.
const cmsGraph = defineGraph({ id: "cms", nodes: { /* ... */ }, edges: { /* ... */ }, defaults: { // This allows us to query past states temporalMode: "current", // Default, but can be overridden per query },});Updating Content
Section titled “Updating Content”When you update a node, TypeGraph automatically:
- Marks the old row as valid until
now(). - Inserts a new row valid from
now().
// 1. Create initial versionconst article = await store.nodes.Article.create({ title: "Draft 1", content: "Work in progress...",});
// 2. Update it (automatically versions)await store.nodes.Article.update(article.id, { title: "Final Version", content: "Ready to publish!",});Querying Past States
Section titled “Querying Past States”You can query the state of the graph as it existed at any point in time using asOf.
// Get the current version (Final Version)const current = await store .query() .from("Article", "a") .whereNode("a", (a) => a.id.eq(article.id)) .select((ctx) => ctx.a) .execute();
// Get the version from 5 minutes ago (Draft 1)const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();
const past = await store .query() .from("Article", "a") .temporal("asOf", fiveMinutesAgo) .whereNode("a", (a) => a.id.eq(article.id)) .select((ctx) => ctx.a) .execute();Audit Logs
Section titled “Audit Logs”To see the full history of changes for a specific node, you can use includeEnded.
const history = await store .query() .from("Article", "a") .temporal("includeEnded") // Include historical rows .whereNode("a", (a) => a.id.eq(article.id)) .orderBy("a", "valid_from", "desc") .select((ctx) => ({ title: ctx.a.title, validFrom: ctx.a.valid_from, validTo: ctx.a.valid_to, })) .execute();Tagging System
Section titled “Tagging System”A flexible tagging system where items can have multiple tags, and you can query by tag combinations.
Schema
Section titled “Schema”const Item = defineNode("Item", { schema: z.object({ title: z.string(), type: z.string() }),});
const Tag = defineNode("Tag", { schema: z.object({ name: z.string(), color: z.string().optional() }),});
const taggedWith = defineEdge("taggedWith");
const graph = defineGraph({ id: "tagging", nodes: { Item, Tag }, edges: { taggedWith: { type: taggedWith, from: [Item], to: [Tag] } },});Find Items by Tag
Section titled “Find Items by Tag”const photoshopItems = await store .query() .from("Tag", "t") .whereNode("t", (t) => t.name.eq("photoshop")) .traverse("taggedWith", "e", { direction: "in" }) .to("Item", "i") .select((ctx) => ctx.i) .execute();Tag Cloud (Count Items per Tag)
Section titled “Tag Cloud (Count Items per Tag)”import { count, field } from "@nicia-ai/typegraph";
const tagCounts = await store .query() .from("Item", "i") .traverse("taggedWith", "e") .to("Tag", "t") .groupBy("t", "name") .aggregate({ tag: field("t", "name"), count: count("i"), }) .execute();
// Sort by count descendingconst tagCloud = tagCounts.toSorted((a, b) => b.count - a.count);Items with Multiple Tags (AND)
Section titled “Items with Multiple Tags (AND)”// Find items tagged with BOTH "javascript" AND "tutorial"const jsTag = await store .query() .from("Tag", "t") .whereNode("t", (t) => t.name.eq("javascript")) .select((ctx) => ctx.t) .first();
const tutorialTag = await store .query() .from("Tag", "t") .whereNode("t", (t) => t.name.eq("tutorial")) .select((ctx) => ctx.t) .first();
if (!jsTag || !tutorialTag) { return []; // Tags don't exist}
const items = await store .query() .from("Item", "i") .traverse("taggedWith", "e1") .to("Tag", "t1") .whereNode("t1", (t) => t.id.eq(jsTag.id)) .traverse("taggedWith", "e2", { direction: "in" }) .to("Item", "i2") .traverse("taggedWith", "e3") .to("Tag", "t2") .whereNode("t2", (t) => t.id.eq(tutorialTag.id)) .select((ctx) => ctx.i) .execute();Tree Navigation
Section titled “Tree Navigation”Hierarchical structures like menus, org charts, or file systems.
Schema
Section titled “Schema”const Category = defineNode("Category", { schema: z.object({ name: z.string(), slug: z.string(), depth: z.number().default(0), }),});
const parentOf = defineEdge("parentOf");
const graph = defineGraph({ id: "categories", nodes: { Category }, edges: { parentOf: { type: parentOf, from: [Category], to: [Category] } },});Get All Ancestors (Breadcrumb)
Section titled “Get All Ancestors (Breadcrumb)”const breadcrumb = await store .query() .from("Category", "c") .whereNode("c", (c) => c.slug.eq("electronics/phones/iphone")) .traverse("parentOf", "e") .recursive({ path: "path" }) .to("Category", "ancestor") .select((ctx) => ({ name: ctx.ancestor.name, slug: ctx.ancestor.slug, })) .execute();// Returns: [{ name: "Phones", slug: "..." }, { name: "Electronics", slug: "..." }, ...]Get All Descendants
Section titled “Get All Descendants”const allChildren = await store .query() .from("Category", "root") .whereNode("root", (c) => c.slug.eq("electronics")) .traverse("parentOf", "e", { direction: "in" }) .recursive({ depth: "level" }) .to("Category", "child") .select((ctx) => ({ name: ctx.child.name, level: ctx.level, })) .orderBy((ctx) => ctx.level, "asc") .execute();Build a Tree Structure
Section titled “Build a Tree Structure”async function buildTree(rootSlug: string): Promise<TreeNode> { const descendants = await store .query() .from("Category", "root") .whereNode("root", (c) => c.slug.eq(rootSlug)) .traverse("parentOf", "e", { direction: "in" }) .recursive({ maxHops: 10 }) .to("Category", "child") .select((ctx) => ({ id: ctx.child.id, name: ctx.child.name, parentId: ctx.e.fromId, })) .execute();
// Build tree in memory const nodeMap = new Map<string, TreeNode>(); for (const d of descendants) { nodeMap.set(d.id, { ...d, children: [] }); } for (const d of descendants) { if (d.parentId && nodeMap.has(d.parentId)) { nodeMap.get(d.parentId)!.children.push(nodeMap.get(d.id)!); } } return nodeMap.get(rootSlug)!;}Weighted Relationships
Section titled “Weighted Relationships”Edges with scores for relevance, confidence, or priority.
Schema
Section titled “Schema”const Document = defineNode("Document", { schema: z.object({ title: z.string() }),});
const relatedTo = defineEdge("relatedTo", { schema: z.object({ score: z.number().min(0).max(1), type: z.enum(["similar", "cites", "extends"]), }),});
const graph = defineGraph({ id: "documents", nodes: { Document }, edges: { relatedTo: { type: relatedTo, from: [Document], to: [Document] } },});Find Highly Related Documents
Section titled “Find Highly Related Documents”const related = await store .query() .from("Document", "d") .whereNode("d", (d) => d.id.eq(documentId)) .traverse("relatedTo", "e") .to("Document", "r") .whereEdge("e", (e) => e.score.gte(0.8)) .select((ctx) => ({ title: ctx.r.title, score: ctx.e.score, type: ctx.e.type, })) .orderBy((ctx) => ctx.score, "desc") .execute();Aggregate Relationship Scores
Section titled “Aggregate Relationship Scores”import { avg, count, field } from "@nicia-ai/typegraph";
const docStats = await store .query() .from("Document", "d") .traverse("relatedTo", "e") .to("Document", "r") .groupByNode("d") .aggregate({ docId: field("d", "id"), title: field("d", "title"), relationCount: count("e"), avgScore: avg("e", "score"), }) .execute();Soft Deletes with Cascade
Section titled “Soft Deletes with Cascade”Delete nodes while preserving relationships for undo capability.
Mark as Deleted
Section titled “Mark as Deleted”// TypeGraph uses soft deletes by defaultawait store.nodes.Document.delete(documentId);
// The node still exists but has deleted_at set// Queries automatically filter it outRestore Deleted Nodes
Section titled “Restore Deleted Nodes”// upsertById "un-deletes" soft-deleted nodesawait store.nodes.Document.upsertById(documentId, { title: "Restored Document", content: "...",});Find Deleted Nodes
Section titled “Find Deleted Nodes”// Use temporal queries to see deleted nodesconst deletedDocs = await store .query() .from("Document", "d") .temporal("includeEnded") .whereNode("d", (d) => d.deletedAt.isNotNull()) .select((ctx) => ({ id: ctx.d.id, title: ctx.d.title, deletedAt: ctx.d.deletedAt, })) .execute();Cascade Delete Pattern
Section titled “Cascade Delete Pattern”async function cascadeDelete(documentId: string): Promise<void> { await store.transaction(async (tx) => { // Find all related edges const edges = await tx .query() .from("Document", "d") .whereNode("d", (d) => d.id.eq(documentId)) .traverse("relatedTo", "e") .to("Document", "r") .select((ctx) => ({ edgeId: ctx.e.id })) .execute();
// Delete edges first for (const { edgeId } of edges) { await tx.edges.relatedTo.delete(edgeId); }
// Then delete the node await tx.nodes.Document.delete(documentId); });}Enforcing Unique Constraints
Section titled “Enforcing Unique Constraints”Prevent duplicate nodes or relationships.
Schema-Level Uniqueness
Section titled “Schema-Level Uniqueness”const User = defineNode("User", { schema: z.object({ email: z.string().email(), username: z.string(), }),});
const graph = defineGraph({ id: "users", nodes: { User: { type: User, unique: [ { name: "user_email", fields: ["email"], scope: "kind", collation: "caseInsensitive" }, { name: "user_username", fields: ["username"], scope: "kind", collation: "caseSensitive" }, ], }, }, edges: {},});Use getOrCreateByConstraint
Section titled “Use getOrCreateByConstraint”async function createOrUpdateUserByEmail( email: string, username: string): Promise<{ user: Node<typeof User>; action: "created" | "found" | "updated" | "resurrected";}> { return store.nodes.User.getOrCreateByConstraint( "user_email", { email, username }, { ifExists: "update" } );}Use getOrCreateByEndpoints for Edge Deduplication
Section titled “Use getOrCreateByEndpoints for Edge Deduplication”async function followUser(followerId: string, followeeId: string): Promise<void> { const follower = await store.nodes.User.getById(followerId); const followee = await store.nodes.User.getById(followeeId); if (!follower || !followee) { throw new Error("User not found"); }
await store.edges.follows.getOrCreateByEndpoints( follower, followee, {}, { ifExists: "return" } );}Next Steps
Section titled “Next Steps”For complete, end-to-end implementations, see the Examples section:
- Document Management - CMS with semantic search
- Product Catalog - Categories, variants, inventory
- Workflow Engine - State machines with approvals
- Audit Trail - Complete change tracking
- Multi-Tenant SaaS - Tenant isolation patterns