api-database-surrealdb
SurrealDB multi-model database - SurrealQL queries, record links, graph relations, live queries, schema definitions, authentication, TypeScript SDK
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-database-surrealdb/skills/api-database-surrealdb
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
SurrealDB Patterns
Quick Guide: Use the
surrealdbSDK (v2+) withnew Surreal()andconnect(). Model relationships with record links for simple pointers andRELATEfor graph edges with metadata. UseSCHEMAFULLtables in production withDEFINE FIELDconstraints. Always use parameterized queries ($variable) to prevent injection. Record IDs aretable:id-- they are immutable and first-class values in SurrealQL. Live queries push changes without polling.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use parameterized queries with $variables for ALL user input -- string interpolation in SurrealQL enables injection attacks)
(You MUST use new RecordId("table", "id") in SDK v2 -- plain "table:id" strings are NOT automatically parsed as record IDs)
(You MUST call db.use({ namespace, database }) or pass namespace/database in connect() options BEFORE any queries -- queries without a selected namespace/database silently fail or error)
(You MUST NOT rely on SCHEMALESS tables in production -- use SCHEMAFULL with DEFINE FIELD to enforce data integrity at the database layer)
(You MUST NOT use UPDATE/DELETE with WHERE on large tables without indexes -- SurrealDB currently does not use indexes for UPDATE/DELETE WHERE clauses (use subquery workaround))
</critical_requirements>
Auto-detection: SurrealDB, Surreal, surrealdb, SurrealQL, RELATE, RecordId, record link, LIVE SELECT, SCHEMAFULL, SCHEMALESS, DEFINE TABLE, DEFINE FIELD, DEFINE ACCESS, surql, graph traversal, ->relation->, <-relation<-
When to use:
- Connecting to SurrealDB and executing queries via the JavaScript SDK
- Modeling data with record links and graph edges (
RELATE) - Defining schemas with
SCHEMAFULLtables and field constraints - Building real-time features with live queries
- Implementing authentication with
DEFINE ACCESSand record-level permissions - Multi-tenant architectures using namespaces and databases
Key patterns covered:
- SDK connection setup (v2 API with
Surreal,connect,RecordId,Table) - CRUD operations with type-safe queries
- Record links vs graph edges (when to use each)
- Schema definitions (
DEFINE TABLE,DEFINE FIELD, permissions) - Live queries for real-time subscriptions
When NOT to use:
- Heavy analytical/OLAP workloads (use a columnar database)
- Simple key-value caching (use a dedicated cache)
- Mature relational schemas that require decades of SQL ecosystem tooling
Detailed Resources:
- For decision frameworks and anti-patterns, see reference.md
Core Patterns:
- examples/core.md - SDK setup, connection, CRUD, TypeScript typing, RecordId
Graph & Relations:
- examples/graph-relations.md - Record links, RELATE, graph traversal, edge metadata
Schema & Auth:
- examples/schema-auth.md - DEFINE TABLE/FIELD, SCHEMAFULL, permissions, DEFINE ACCESS, authentication
Live Queries & Transactions:
- examples/live-queries.md - LIVE SELECT, subscriptions, transactions, events
<red_flags>
RED FLAGS
High Priority Issues:
- Using string interpolation instead of
$parametersin SurrealQL queries -- enables injection attacks - Using
"table:id"strings instead ofnew RecordId("table", "id")in SDK v2 -- strings are not auto-parsed as record IDs - Running queries without selecting namespace/database -- queries silently fail or return errors
- Using
SCHEMALESStables in production without explicit field definitions -- data integrity not enforced
Medium Priority Issues:
UPDATE table SET ... WHERE conditionon large tables without indexes -- SurrealDB does not use indexes for UPDATE/DELETE WHERE (useUPDATE (SELECT id FROM table WHERE condition) SET ...as workaround)- Using
UPSERTwithout a unique index --UPSERTis much more performant with unique indexes (avoids table scan) - Embedding unbounded arrays as record links -- arrays can grow without limit; use graph edges for unbounded relationships
- Not setting
DURATION FOR TOKENandDURATION FOR SESSIONonDEFINE ACCESS-- tokens/sessions without expiry are a security risk
Common Mistakes:
- Creating duplicate record IDs silently fails or errors depending on context -- use
INSERT ... ON DUPLICATE KEY UPDATEorUPSERTfor idempotent operations - Expecting
record:idstrings to sort numerically --record:1,record:10,record:2sorts lexicographically; use numeric IDs (record:1,record:2,record:10) or ULID/UUID for temporal sorting - Forgetting that record IDs are immutable -- you cannot change a record's ID after creation; you must create a new record and delete the old one
- Using
rand(),ulid(), oruuid()inDEFINE FUNCTIONbodies -- these generate the same value per function call, causing duplicate key errors on subsequent calls - Confusing
DEFINE FIELD ... VALUE(recalculated on create/update) withDEFINE FIELD ... COMPUTED(recalculated on access, v3.0+) - Setting
idfield inCREATE table:specific_id SET id = "other"-- the explicit record ID takes precedence and theidin SET is silently discarded
Gotchas & Edge Cases:
- Fields defined with
VALUEare recalculated alphabetically -- if fieldbdepends on fielda, naming matters FLEXIBLE TYPEon aSCHEMAFULLtable allows schemaless nested objects -- useful for JSON metadata but bypasses type checking on that subtreeLIVE SELECTwith complexWHEREfilters may not fire for all edge cases -- test your filters thoroughlylocalhostin connection strings can fail on Node.js 18+ due to IPv6 preference -- use127.0.0.1- Numeric string IDs (
"10") display as backtick-escaped (table:\10``) to differentiate from numeric IDs (table:10) - Record References (
DEFINE FIELD ... REFERENCE) are experimental (require--allow-experimental record_references) -- do not use in production
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use parameterized queries with $variables for ALL user input -- string interpolation in SurrealQL enables injection attacks)
(You MUST use new RecordId("table", "id") in SDK v2 -- plain "table:id" strings are NOT automatically parsed as record IDs)
(You MUST call db.use({ namespace, database }) or pass namespace/database in connect() options BEFORE any queries -- queries without a selected namespace/database silently fail or error)
(You MUST NOT rely on SCHEMALESS tables in production -- use SCHEMAFULL with DEFINE FIELD to enforce data integrity at the database layer)
(You MUST NOT use UPDATE/DELETE with WHERE on large tables without indexes -- SurrealDB currently does not use indexes for UPDATE/DELETE WHERE clauses (use subquery workaround))
Failure to follow these rules will cause injection vulnerabilities, silent query failures, or data integrity issues.
</critical_reminders>
Files (skills)
-
examples
-
core.md 11.5 KB
# SurrealDB Core Examples > Connection patterns, CRUD operations, TypeScript typing, and RecordId usage. See [SKILL.md](../SKILL.md) for core concepts. **Graph patterns:** See [graph-relations.md](graph-relations.md). **Schema & auth:** See [schema-auth.md](schema-auth.md). **Live queries:** See [live-queries.md](live-queries.md). --- ## Pattern 1: Connection Setup ### Good Example -- Production Connection ```typescript import Surreal from "surrealdb"; async function connectDatabase(): Promise<Surreal> { const url = process.env.SURREALDB_URL; if (!url) { throw new Error("SURREALDB_URL environment variable is required"); } const namespace = process.env.SURREALDB_NAMESPACE; const database = process.env.SURREALDB_DATABASE; if (!namespace || !database) { throw new Error( "SURREALDB_NAMESPACE and SURREALDB_DATABASE environment variables are required", ); } const db = new Surreal(); try { await db.connect(url, { namespace, database, }); // Authenticate (use DEFINE ACCESS for app users, root only for admin) await db.signin({ username: process.env.SURREALDB_USER ?? "root", password: process.env.SURREALDB_PASS ?? "", }); return db; } catch (error) { await db.close(); throw error; } } export { connectDatabase }; ``` **Why good:** Environment variables for all config, namespace/database set at connection time, error handling closes connection on failure, typed return ### Good Example -- Connection State Monitoring ```typescript const db = new Surreal(); const unsubConnected = db.subscribe("connected", () => { console.log("SurrealDB connected"); }); const unsubDisconnected = db.subscribe("disconnected", () => { console.warn("SurrealDB disconnected"); }); const unsubError = db.subscribe("error", (error) => { console.error("SurrealDB error:", error); }); // Cleanup subscriptions when done function cleanup(): void { unsubConnected(); unsubDisconnected(); unsubError(); } export { cleanup }; ``` **Why good:** SDK v2 event subscriptions via `subscribe()`, returns unsubscribe functions for cleanup, different log levels for different events ### Good Example -- Graceful Shutdown ```typescript async function disconnectDatabase(db: Surreal): Promise<void> { await db.close(); console.log("SurrealDB connection closed"); } process.on("SIGINT", async () => { await disconnectDatabase(db); process.exit(0); }); process.on("SIGTERM", async () => { await disconnectDatabase(db); process.exit(0); }); export { disconnectDatabase }; ``` ### Bad Example -- Missing Namespace/Database ```typescript // BAD: No namespace or database selected const db = new Surreal(); await db.connect("http://localhost:8000"); await db.signin({ username: "root", password: "root" }); // This query will fail silently or error -- no namespace/database context const users = await db.query("SELECT * FROM user"); ``` **Why bad:** No `namespace`/`database` in connect options or `use()` call, queries fail without context, uses `localhost` (IPv6 issues on Node.js 18+) --- ## Pattern 2: CRUD Operations ### Good Example -- Create Records ```typescript import { RecordId, Table } from "surrealdb"; interface User { id: RecordId; name: string; email: string; role: "admin" | "user" | "moderator"; created_at: string; } // Create with auto-generated ID const newUser = await db.create<User>(new Table("user"), { name: "Alice", email: "alice@example.com", role: "user", }); // Returns: { id: RecordId { table: "user", id: "abc123..." }, name: "Alice", ... } // Create with specific ID const admin = await db.create<User>(new RecordId("user", "admin-alice"), { name: "Alice", email: "alice@example.com", role: "admin", }); // Bulk insert with conflict handling (SurrealQL) const BATCH_SIZE = 100; const users = generateUsers(BATCH_SIZE); await db.query( `INSERT INTO user $users ON DUPLICATE KEY UPDATE name = $input.name, email = $input.email`, { users }, ); ``` **Why good:** TypeScript generics for return type, `Table` for auto-ID creation, `RecordId` for specific IDs, `INSERT ... ON DUPLICATE KEY UPDATE` for idempotent bulk operations, named constant for batch size ### Good Example -- Select and Query ```typescript const PAGE_SIZE = 20; // Select by ID (fastest -- direct record lookup) const user = await db.select<User>(new RecordId("user", "alice")); // Select all from table const allUsers = await db.select<User[]>(new Table("user")); // Parameterized query with pagination async function getUsers( page: number = 1, role?: string, ): Promise<{ data: User[]; total: number }> { const offset = (page - 1) * PAGE_SIZE; const [data, countResult] = await db.query<[User[], [{ count: number }]]>( `SELECT * FROM user WHERE ($role = NONE OR role = $role) ORDER BY created_at DESC LIMIT $limit START $offset; SELECT count() AS count FROM user WHERE ($role = NONE OR role = $role) GROUP ALL;`, { role: role ?? null, limit: PAGE_SIZE, offset }, ); return { data, total: countResult[0]?.count ?? 0, }; } export { getUsers }; ``` **Why good:** Direct `select()` for known IDs (fastest path), parameterized queries prevent injection, parallel count query, `NONE` check for optional filters, named constant for page size ### Good Example -- Update and Merge ```typescript // Full replacement (v2 chainable) -- all fields must be provided await db.update(new RecordId("user", "alice")).content({ name: "Alice B.", email: "alice-b@example.com", role: "admin", created_at: "2025-01-01T00:00:00Z", }); // Partial update (merge) -- only specified fields change await db.merge(new RecordId("user", "alice"), { role: "moderator", }); // Or via chainable: await db.update(new RecordId("user", "alice")).merge({ role: "moderator" }); // Conditional update via SurrealQL with index workaround for large tables await db.query( `UPDATE (SELECT id FROM user WHERE role = $old_role) SET role = $new_role`, { old_role: "moderator", new_role: "user" }, ); ``` **Why good:** `update().content()` for full replacement (v2 chainable API), `merge()` for partial updates, subquery pattern for bulk conditional updates (workaround for UPDATE WHERE not using indexes) ### Good Example -- Delete ```typescript // Delete by ID await db.delete(new RecordId("user", "alice")); // Bulk delete via SurrealQL with subquery (index-aware) const DAYS_INACTIVE = 90; await db.query( `DELETE (SELECT id FROM user WHERE last_active < time::now() - $days + "d")`, { days: DAYS_INACTIVE }, ); ``` **Why good:** Direct delete by `RecordId`, subquery pattern for bulk deletes to use indexes, named constant for threshold ### Bad Example -- String Interpolation ```typescript // BAD: Injection risk with every line const email = userInput.email; const users = await db.query(`SELECT * FROM user WHERE email = '${email}'`); await db.query(`DELETE user:${userInput.id}`); await db.query( `UPDATE user SET name = '${userInput.name}' WHERE id = user:${userInput.id}`, ); ``` **Why bad:** Every line is vulnerable to SurrealQL injection, attacker can escape string and execute arbitrary statements (DROP TABLE, DEFINE ACCESS) --- ## Pattern 3: TypeScript Integration ### Good Example -- Typed Query Results ```typescript import type { RecordId } from "surrealdb"; // Define interfaces matching your SurrealDB schema interface Post { id: RecordId; title: string; content: string; author: RecordId; // Record link to user table tags: string[]; published: boolean; created_at: string; } interface PostWithAuthor { id: RecordId; title: string; author_name: string; author_email: string; } // Type-safe query with result typing const posts = await db.query<[PostWithAuthor[]]>( `SELECT id, title, author.name AS author_name, author.email AS author_email FROM post WHERE published = true ORDER BY created_at DESC LIMIT $limit`, { limit: 10 }, ); // Multi-statement with tuple typing const [recentPosts, topAuthors] = await db.query< [Post[], { author: RecordId; post_count: number }[]] >( `SELECT * FROM post WHERE published = true ORDER BY created_at DESC LIMIT 10; SELECT author, count() AS post_count FROM post GROUP BY author ORDER BY post_count DESC LIMIT 5;`, ); ``` **Why good:** Separate interfaces for different query shapes, `RecordId` type for record links, tuple typing for multi-statement queries, dot notation traverses record links in SELECT ### Good Example -- RecordId Handling ```typescript import { RecordId, Table } from "surrealdb"; // Create RecordId instances const userId = new RecordId("user", "alice"); const postId = new RecordId("post", "first-post"); // RecordId properties console.log(userId.table); // Table { name: "user" } console.log(userId.id); // "alice" // Use in queries as parameters const result = await db.query<[Post[]]>( `SELECT * FROM post WHERE author = $author`, { author: userId }, ); // Create dynamic RecordId from user input (safely) function toRecordId(table: string, id: string): RecordId { return new RecordId(table, id); } // Array-based IDs for composite keys const weatherId = new RecordId("weather", ["London", "2025-01-15T08:00:00Z"]); ``` **Why good:** `RecordId` class for type-safe record references, access `.table` and `.id` properties, parameterized use in queries, array-based IDs for composite keys --- ## Pattern 4: Range Queries with Record IDs ### Good Example -- ID Range Scanning ```typescript // Numeric ID ranges -- no table scan needed const batch = await db.query<[User[]]>(`SELECT * FROM user:1..=1000`); // Array-based ID ranges for time-series data const londonWeather = await db.query<[WeatherReading[]]>( `SELECT * FROM weather:['London', NONE]..=['London', time::now()]`, ); // Parameterized range query const readings = await db.query<[WeatherReading[]]>( `SELECT * FROM weather:[$city, $start]..=[$city, $end]`, { city: "London", start: "2025-01-01T00:00:00Z", end: "2025-01-31T23:59:59Z", }, ); ``` **Why good:** Record ID ranges avoid table scans entirely, array-based IDs enable efficient time-series queries partitioned by key, parameterized for safety ### Bad Example -- Full Table Scan for Range Data ```typescript // BAD: Full table scan when record ID ranges would be more efficient const readings = await db.query<[WeatherReading[]]>( `SELECT * FROM weather WHERE city = $city AND timestamp >= $start AND timestamp <= $end`, { city: "London", start: "2025-01-01", end: "2025-01-31" }, ); // Requires indexes on city + timestamp to be efficient // With array-based IDs, the range scan is free ``` **Why bad:** Requires composite index to avoid full table scan, array-based record IDs provide free range scanning by design --- ## Pattern 5: Upsert and Idempotent Operations ### Good Example -- Upsert Pattern ```typescript // Upsert by specific record ID -- create if missing, update if exists await db.query( `UPSERT user:alice SET name = "Alice", email = "alice@example.com", role = "admin", updated_at = time::now()`, ); // Insert with conflict resolution await db.query( `INSERT INTO user { id: user:alice, name: "Alice", email: "alice@example.com" } ON DUPLICATE KEY UPDATE name = $input.name, updated_at = time::now()`, ); ``` **Why good:** `UPSERT` for single-record idempotency, `INSERT ... ON DUPLICATE KEY UPDATE` for bulk operations with conflict handling, `time::now()` for automatic timestamps --- _For graph patterns, see [graph-relations.md](graph-relations.md). For schema definitions, see [schema-auth.md](schema-auth.md). For live queries, see [live-queries.md](live-queries.md)._ -
graph-relations.md 8.4 KB
# SurrealDB Graph & Relations Examples > Record links, RELATE edges, graph traversal patterns, and relationship modeling. See [SKILL.md](../SKILL.md) for core concepts. **Core patterns:** See [core.md](core.md). **Schema & auth:** See [schema-auth.md](schema-auth.md). **Live queries:** See [live-queries.md](live-queries.md). --- ## Pattern 1: Record Links (Simple Pointers) Record links are fields that store a `RecordId` pointing directly to another record. SurrealDB fetches linked records from disk via dot notation -- no JOIN required. ### Good Example -- Record Link Fields ```surql -- Create records with link fields CREATE person:alice SET name = "Alice", company = company:acme, manager = person:bob, skills = [skill:typescript, skill:surrealdb]; CREATE company:acme SET name = "Acme Corp", founded = d"2020-01-01"; -- Traverse links with dot notation (automatic remote fetch) SELECT name, company.name AS company_name FROM person:alice; -- Returns: [{ name: "Alice", company_name: "Acme Corp" }] -- Multi-level traversal SELECT name, manager.company.name AS manager_company FROM person:alice; -- Array link traversal SELECT skills.name AS skill_names FROM person:alice; -- Returns skill names from all linked skill records ``` **Why good:** Links are direct disk lookups (no table scan), dot notation traverses transparently across tables, works on single values and arrays ### Bad Example -- Storing ID as String ```surql -- BAD: Storing the ID as a plain string instead of a record link CREATE person:alice SET name = "Alice", company = "company:acme"; -- This is a STRING, not a record link -- Dot notation won't work on strings SELECT company.name FROM person:alice; -- Returns: NONE (string has no .name property) ``` **Why bad:** String values are not record links -- dot notation traversal fails silently, returning NONE instead of the linked record's data --- ## Pattern 2: Graph Edges with RELATE Graph edges are full records stored in a relation table. They support metadata, bidirectional traversal, and type constraints via `DEFINE TABLE TYPE RELATION`. ### Good Example -- Creating Edges ```surql -- Simple edge RELATE person:alice->follows->person:bob; -- Edge with metadata RELATE person:alice->follows->person:carol SET followed_at = time::now(), notifications = true; -- Multiple edges in one statement RELATE person:alice->likes->post:hello_world SET liked_at = time::now(); RELATE person:alice->likes->post:surrealdb_guide SET liked_at = time::now(); -- Typed relation table (enforces valid endpoints) DEFINE TABLE follows TYPE RELATION IN person OUT person; DEFINE TABLE likes TYPE RELATION IN person OUT post; -- ENFORCED ensures referenced records exist DEFINE TABLE follows TYPE RELATION IN person OUT person ENFORCED; ``` **Why good:** `RELATE` creates edge records with `in`/`out` fields automatically, `SET` adds metadata, `TYPE RELATION` constrains valid endpoints, `ENFORCED` prevents dangling references ### Good Example -- Forward and Reverse Traversal ```surql -- Forward: who does Alice follow? SELECT ->follows->person.name AS following FROM person:alice; -- Returns: [{ following: ["Bob", "Carol"] }] -- Reverse: who follows Bob? SELECT <-follows<-person.name AS followers FROM person:bob; -- Returns: [{ followers: ["Alice"] }] -- Bidirectional (for symmetric relationships like "knows") DEFINE TABLE knows TYPE RELATION IN person OUT person; RELATE person:alice->knows->person:bob; SELECT <->knows<->person.name AS connections FROM person:alice; -- Returns: [{ connections: ["Bob"] }] -- Filter during traversal SELECT ->follows->person.* AS following FROM person:alice WHERE ->follows->person.role = "admin"; -- Multi-hop traversal (friends of friends) SELECT ->follows->person->follows->person.name AS fof FROM person:alice; ``` **Why good:** Arrow syntax for direction, dot notation on target for field selection, bidirectional with `<->`, multi-hop in a single query without separate joins ### Good Example -- Querying Edge Metadata ```surql -- Query the edge table directly SELECT *, in.name AS follower, out.name AS followed FROM follows WHERE in = person:alice; -- Filter edges by metadata SELECT ->follows[WHERE notifications = true]->person.name AS notified_following FROM person:alice; -- Aggregate on edges SELECT out.name AS followed, count() AS follower_count FROM follows GROUP BY out ORDER BY follower_count DESC LIMIT 10; ``` **Why good:** Edges are full records (queryable with SELECT), bracket filters on traversal path, aggregation on edge tables for analytics ### Bad Example -- Using Record Links for Bidirectional Relationships ```surql -- BAD: Manually maintaining bidirectional links UPDATE person:alice SET friends += person:bob; UPDATE person:bob SET friends += person:alice; -- Must manually keep both sides in sync -- fragile -- If one update fails, the relationship is inconsistent ``` **Why bad:** Manual bidirectional maintenance is error-prone, no atomicity guarantee, use `RELATE` with `<->` traversal instead --- ## Pattern 3: Relationship Modeling Decisions ### Good Example -- Social Graph ```surql -- Define typed relation tables DEFINE TABLE follows TYPE RELATION IN person OUT person SCHEMAFULL; DEFINE FIELD followed_at ON follows TYPE datetime VALUE time::now(); DEFINE FIELD notifications ON follows TYPE bool DEFAULT true; DEFINE TABLE blocks TYPE RELATION IN person OUT person SCHEMAFULL; DEFINE FIELD blocked_at ON blocks TYPE datetime VALUE time::now(); -- Complex social query: mutual followers SELECT ->follows->person AS i_follow, <-follows<-person AS follows_me FROM person:alice; -- Find mutual follows (both directions) -- People Alice follows who also follow Alice SELECT ->follows->person INTERSECT <-follows<-person AS mutuals FROM person:alice; ``` **Why good:** Typed relation tables enforce valid connections, metadata on edges (timestamps, preferences), `INTERSECT` for set operations on traversal results ### Good Example -- Access Control Graph ```surql -- Model permission inheritance via graph DEFINE TABLE member_of TYPE RELATION IN user OUT team SCHEMAFULL; DEFINE FIELD role ON member_of TYPE string ASSERT $value IN ["viewer", "editor", "admin"]; DEFINE FIELD joined_at ON member_of TYPE datetime VALUE time::now(); DEFINE TABLE owns TYPE RELATION IN team OUT resource SCHEMAFULL; DEFINE FIELD permission ON owns TYPE string ASSERT $value IN ["read", "write", "admin"]; -- Create team membership RELATE user:alice->member_of->team:engineering SET role = "admin"; RELATE user:bob->member_of->team:engineering SET role = "viewer"; -- Create resource ownership RELATE team:engineering->owns->resource:api_server SET permission = "admin"; -- Check if user has access to resource (graph traversal) SELECT ->member_of->team->owns->resource AS accessible_resources, ->member_of[WHERE role = "admin"]->team.name AS admin_teams FROM user:alice; ``` **Why good:** Graph models permission inheritance naturally, edge metadata stores role/permission level, traversal checks access without manual joins, bracket filters on edges --- ## Pattern 4: Recursive and Advanced Traversal ### Good Example -- Org Chart Traversal ```surql -- Define hierarchical relationship DEFINE TABLE reports_to TYPE RELATION IN employee OUT employee; RELATE employee:alice->reports_to->employee:ceo; RELATE employee:bob->reports_to->employee:alice; RELATE employee:carol->reports_to->employee:alice; -- Direct reports (one level) SELECT <-reports_to<-employee.name AS direct_reports FROM employee:alice; -- Full chain to top (recursive-style) SELECT name, ->reports_to->employee.name AS manager, ->reports_to->employee->reports_to->employee.name AS skip_manager FROM employee:bob; ``` **Why good:** Graph naturally models hierarchies, arrow syntax chains for multi-level traversal, each hop adds one `->edge->target` segment ### Good Example -- Wildcard Edge Traversal ```surql -- Discover all outgoing relationships from a record (any edge type) SELECT id, ->?->? AS all_connections FROM person:alice; -- Discover all incoming relationships SELECT id, <-?<-? AS all_referrers FROM resource:api_server; -- Useful for debugging and schema discovery SELECT id, ->?->?.id AS outgoing_ids FROM person:alice; ``` **Why good:** Wildcard `?` matches any edge table and target, useful for schema exploration and debugging, returns all connected records regardless of edge type --- _For core patterns, see [core.md](core.md). For schema definitions, see [schema-auth.md](schema-auth.md). For live queries, see [live-queries.md](live-queries.md)._ -
live-queries.md 9 KB
# SurrealDB Live Queries & Transactions Examples > Live queries, real-time subscriptions, transactions, events, and custom functions. See [SKILL.md](../SKILL.md) for core concepts. **Core patterns:** See [core.md](core.md). **Graph patterns:** See [graph-relations.md](graph-relations.md). **Schema & auth:** See [schema-auth.md](schema-auth.md). --- ## Pattern 1: Live Queries (Real-Time Subscriptions) ### Good Example -- Table Subscription (SDK v2) ```typescript import Surreal, { Table } from "surrealdb"; interface ChatMessage { id: RecordId; channel: string; author: RecordId; content: string; created_at: string; } // Subscribe to all changes on a table const live = await db.live(new Table("chat_message")); // Callback-based subscription live.subscribe((message) => { switch (message.action) { case "CREATE": console.log("New message:", message.value); break; case "UPDATE": console.log("Message updated:", message.value); break; case "DELETE": console.log("Message deleted, id:", message.recordId); break; } }); // Cleanup when done await live.kill(); ``` **Why good:** SDK v2 `live()` returns a subscription object, callback receives `LiveMessage` with `action`/`value`/`recordId`, `kill()` for cleanup, action-based switch for different event types ### Good Example -- Filtered Live Query (SurrealQL) ```typescript // Subscribe to specific records only (server-side filtering via SurrealQL) const [liveId] = await db.query<[string]>( `LIVE SELECT * FROM chat_message WHERE channel = $channel`, { channel: "general" }, ); // Async iterator pattern (preferred in SDK v2) const liveFeed = await db.live(new Table("notification")); for await (const { action, value } of liveFeed) { if (action === "CREATE") { showNotification(value); } } ``` **Why good:** Server-side WHERE filtering reduces network traffic, async iterator for stream processing, `for await` pattern integrates naturally with TypeScript control flow ### Good Example -- Live Query Cleanup Pattern ```typescript class RealtimeManager { private subscriptions: Array<{ kill: () => Promise<void> }> = []; async subscribe( db: Surreal, table: string, callback: (message: LiveMessage) => void, ): Promise<void> { const live = await db.live(new Table(table)); live.subscribe(callback); this.subscriptions.push(live); } async cleanup(): Promise<void> { await Promise.all(this.subscriptions.map((sub) => sub.kill())); this.subscriptions = []; } } export { RealtimeManager }; ``` **Why good:** Centralized subscription management, `cleanup()` kills all live queries on disconnect, prevents subscription leaks ### Bad Example -- Unfiltered Live Query on Large Table ```typescript // BAD: Subscribing to ALL changes on a high-traffic table const live = await db.live(new Table("audit_log")); live.subscribe((message) => { // Fires for EVERY insert into audit_log -- could be thousands per second console.log(message.action, message.value); }); // No cleanup -- subscription leaks on disconnect ``` **Why bad:** No WHERE filter means every change triggers the callback (high-traffic tables can overwhelm the client), no `kill()` call causes subscription leak --- ## Pattern 2: Transactions ### Good Example -- Multi-Statement Transaction (SurrealQL) ```surql BEGIN TRANSACTION; -- Transfer funds between accounts LET $source = (SELECT * FROM account:alice); LET $dest = (SELECT * FROM account:bob); IF $source.balance < $amount { THROW "Insufficient funds"; }; UPDATE account:alice SET balance -= $amount; UPDATE account:bob SET balance += $amount; CREATE transfer SET from = account:alice, to = account:bob, amount = $amount, transferred_at = time::now(); COMMIT TRANSACTION; ``` **Why good:** `BEGIN`/`COMMIT` wraps multiple operations atomically, `THROW` rolls back the transaction on error, `LET` for intermediate values, all operations succeed or all fail ### Good Example -- Transaction via SDK ```typescript const TRANSFER_AMOUNT = 50.0; async function transferFunds( db: Surreal, fromId: string, toId: string, amount: number, ): Promise<void> { await db.query( `BEGIN TRANSACTION; LET $source = (SELECT balance FROM account:$from_id); IF $source.balance < $amount { THROW "Insufficient funds in source account"; }; UPDATE account:$from_id SET balance -= $amount; UPDATE account:$to_id SET balance += $amount; CREATE transfer SET from_account = type::record("account", $from_id), to_account = type::record("account", $to_id), amount = $amount, transferred_at = time::now(); COMMIT TRANSACTION;`, { from_id: fromId, to_id: toId, amount, }, ); } export { transferFunds }; ``` **Why good:** Parameterized transaction, `THROW` for validation, `type::record()` to construct record IDs from parameters, atomic commit/rollback ### Good Example -- Transaction with RETURN ```surql BEGIN TRANSACTION; LET $order = CREATE order SET customer = $customer_id, items = $items, status = "pending", total = $total; -- Decrement stock for each item FOR $item IN $items { LET $product = (SELECT stock FROM product WHERE id = $item.product_id); IF $product.stock < $item.quantity { THROW "Insufficient stock for " + <string> $item.product_id; }; UPDATE $item.product_id SET stock -= $item.quantity; }; RETURN $order; COMMIT TRANSACTION; ``` **Why good:** `RETURN` provides the created order back to the caller from within the transaction, `FOR` loop processes items, stock validation prevents overselling ### Bad Example -- No Transaction for Related Operations ```surql -- BAD: Two operations that should be atomic but aren't UPDATE account:alice SET balance -= 100; -- If the server crashes here, Alice lost money and Bob didn't receive it UPDATE account:bob SET balance += 100; ``` **Why bad:** No transaction wrapper means partial failure leaves data inconsistent, use `BEGIN TRANSACTION` / `COMMIT TRANSACTION` for atomic operations --- ## Pattern 3: Events (Database Triggers) ### Good Example -- Audit Logging with Events ```surql -- Trigger on any change to the user table DEFINE EVENT user_audit ON user WHEN $event IN ["CREATE", "UPDATE", "DELETE"] THEN { CREATE audit_log SET table_name = "user", record_id = $value.id, event_type = $event, timestamp = time::now(), before = $before, after = $after; }; -- Trigger only on specific conditions DEFINE EVENT price_alert ON product WHEN $event = "UPDATE" AND $before.price != $after.price THEN { CREATE notification SET message = "Price changed for " + <string> $value.id, old_price = $before.price, new_price = $after.price, created_at = time::now(); }; ``` **Why good:** `$event` is the action type, `$before`/`$after` capture old and new values, conditional triggers with `WHEN`, events fire within the same transaction as the triggering operation --- ## Pattern 4: Custom Functions ### Good Example -- Reusable Business Logic ```surql -- Custom function for full-text search with pagination DEFINE FUNCTION fn::search_posts($term: string, $page: int, $limit: int) { LET $offset = ($page - 1) * $limit; RETURN SELECT id, title, content, search::score(1) AS relevance FROM post WHERE title @1@ $term OR content @1@ $term ORDER BY relevance DESC LIMIT $limit START $offset; }; -- Usage SELECT * FROM fn::search_posts("surrealdb tutorial", 1, 20); ``` **Why good:** Encapsulates query logic, parameterized for reuse, returns structured results, pagination built in ### Gotcha -- Random ID in Functions ```surql -- BAD: rand()/ulid()/uuid() generate the SAME value per function call DEFINE FUNCTION fn::create_with_id() { -- This generates the same ULID on every call, causing duplicate key errors CREATE thing:ulid() SET data = "test"; CREATE thing:ulid() SET data = "test2"; -- Both get the SAME ulid! }; -- WORKAROUND: Pass the ID as a parameter DEFINE FUNCTION fn::create_with_param($id: string) { CREATE type::record("thing", $id) SET data = "test"; }; ``` **Why bad:** Random ID functions (`rand()`, `ulid()`, `uuid()`) in custom function bodies generate the same value per function invocation, causing duplicate key errors on subsequent creates within the same function --- ## Pattern 5: Change Feeds ### Good Example -- Change Data Capture ```surql -- Enable change feed on a table (retain 7 days of changes) DEFINE TABLE order CHANGEFEED 7d INCLUDE ORIGINAL; -- Query changes since a timestamp SHOW CHANGES FOR TABLE order SINCE d"2025-01-01T00:00:00Z" LIMIT 100; ``` **Why good:** `CHANGEFEED` retains history for replay/sync, `INCLUDE ORIGINAL` preserves pre-change values, `SHOW CHANGES` for consuming changes, useful for CDC pipelines and event sourcing **When to use:** Event sourcing, audit trails, data sync between services, replaying state changes. When you need the change history itself, not just the current state. --- _For core patterns, see [core.md](core.md). For graph patterns, see [graph-relations.md](graph-relations.md). For schema definitions, see [schema-auth.md](schema-auth.md)._ -
schema-auth.md 11.5 KB
# SurrealDB Schema & Authentication Examples > DEFINE TABLE, DEFINE FIELD, SCHEMAFULL mode, permissions, DEFINE ACCESS, authentication patterns. See [SKILL.md](../SKILL.md) for core concepts. **Core patterns:** See [core.md](core.md). **Graph patterns:** See [graph-relations.md](graph-relations.md). **Live queries:** See [live-queries.md](live-queries.md). --- ## Pattern 1: SCHEMAFULL Table Definition ### Good Example -- Complete Table Schema ```surql -- Strict schema enforcement DEFINE TABLE user SCHEMAFULL; -- Field definitions with types, defaults, and validation DEFINE FIELD name ON user TYPE string ASSERT string::len($value) >= 2 AND string::len($value) <= 100; DEFINE FIELD email ON user TYPE string VALUE string::lowercase($value) ASSERT string::is::email($value); DEFINE FIELD role ON user TYPE string DEFAULT "user" ASSERT $value IN ["admin", "user", "moderator"]; DEFINE FIELD age ON user TYPE option<int> ASSERT $value = NONE OR ($value >= 0 AND $value <= 150); DEFINE FIELD tags ON user TYPE array<string> DEFAULT []; DEFINE FIELD metadata ON user FLEXIBLE TYPE object; DEFINE FIELD created_at ON user TYPE datetime VALUE time::now() READONLY; DEFINE FIELD updated_at ON user TYPE datetime VALUE time::now(); -- Indexes DEFINE INDEX email_idx ON user FIELDS email UNIQUE; DEFINE INDEX role_idx ON user FIELDS role; ``` **Why good:** `SCHEMAFULL` enforces all fields, `ASSERT` for validation, `VALUE` with `string::lowercase` for transformation, `READONLY` prevents modification after creation, `FLEXIBLE TYPE object` for schemaless subtree within strict table, `option<int>` for nullable field, `DEFAULT` for sensible defaults ### Good Example -- Relation Table Schema ```surql -- Typed relation table with constraints DEFINE TABLE follows TYPE RELATION IN user OUT user SCHEMAFULL ENFORCED; DEFINE FIELD followed_at ON follows TYPE datetime VALUE time::now() READONLY; DEFINE FIELD notifications ON follows TYPE bool DEFAULT true; DEFINE FIELD strength ON follows TYPE string DEFAULT "normal" ASSERT $value IN ["close", "normal", "acquaintance"]; ``` **Why good:** `TYPE RELATION IN user OUT user` constrains endpoints, `ENFORCED` prevents edges to nonexistent records, metadata fields with defaults and validation ### Bad Example -- SCHEMALESS in Production ```surql -- BAD: No field enforcement in production DEFINE TABLE user SCHEMALESS; -- Anyone can insert arbitrary fields CREATE user SET name = "Alice", emal = "alice@example.com", -- Typo goes undetected role = "superadmin", -- Invalid role accepted _internal_notes = "hack"; -- Unintended field stored ``` **Why bad:** Typos in field names silently create new fields, no validation on values, arbitrary fields stored without restriction, data integrity impossible to guarantee --- ## Pattern 2: Computed and Derived Fields ### Good Example -- VALUE vs COMPUTED ```surql DEFINE TABLE product SCHEMAFULL; DEFINE FIELD name ON product TYPE string; DEFINE FIELD price ON product TYPE float ASSERT $value >= 0; DEFINE FIELD quantity ON product TYPE int ASSERT $value >= 0; DEFINE FIELD discount_pct ON product TYPE float DEFAULT 0 ASSERT $value >= 0 AND $value <= 100; -- VALUE: recalculated on CREATE and UPDATE DEFINE FIELD updated_at ON product TYPE datetime VALUE time::now(); -- VALUE with field dependencies DEFINE FIELD display_name ON product TYPE string VALUE string::uppercase(name); -- COMPUTED (v3.0+): recalculated on every ACCESS (read) DEFINE FIELD total_value ON product COMPUTED price * quantity; DEFINE FIELD discounted_price ON product COMPUTED price * (1 - discount_pct / 100); -- VALUE using $before for change tracking DEFINE FIELD previous_price ON product TYPE option<float> VALUE $before.price; ``` **Why good:** `VALUE` recalculates on write (good for timestamps, derived strings), `COMPUTED` recalculates on read (good for live calculations), `$before` captures previous value on update ### Important Gotcha ```surql -- Fields with VALUE compute in ALPHABETICAL ORDER -- Field "b_total" computes BEFORE "a_price" is processed in the same CREATE -- If b_total depends on a_price, the dependency may not resolve correctly -- SAFE: name dependencies so alphabetical order matches dependency order DEFINE FIELD a_base_price ON product TYPE float; DEFINE FIELD b_tax ON product TYPE float VALUE a_base_price * 0.1; DEFINE FIELD c_total ON product TYPE float VALUE a_base_price + b_tax; ``` **Why good:** Named to respect alphabetical processing order, each field depends only on alphabetically-prior fields --- ## Pattern 3: Table and Field Permissions ### Good Example -- Record-Level Access Control ```surql -- Users can read public data, modify only their own records DEFINE TABLE post SCHEMAFULL PERMISSIONS FOR select WHERE published = true OR author = $auth.id FOR create WHERE $auth.id != NONE FOR update WHERE author = $auth.id FOR delete WHERE author = $auth.id OR $auth.role = "admin"; DEFINE FIELD title ON post TYPE string ASSERT string::len($value) >= 1; DEFINE FIELD content ON post TYPE string; DEFINE FIELD author ON post TYPE record<user> VALUE $auth.id READONLY; DEFINE FIELD published ON post TYPE bool DEFAULT false; DEFINE FIELD created_at ON post TYPE datetime VALUE time::now() READONLY; -- Field-level permissions (restrict sensitive data) DEFINE FIELD email ON user TYPE string PERMISSIONS FOR select WHERE id = $auth.id OR $auth.role = "admin" FOR update WHERE id = $auth.id; ``` **Why good:** Table permissions per operation type, `$auth.id` for ownership checks, `$auth.role` for admin access, `READONLY` author field set from auth context, field-level permissions for sensitive data like email ### Good Example -- Pre-Computed View Table ```surql -- Materialized view that auto-updates DEFINE TABLE post_stats TYPE NORMAL AS SELECT author, count() AS post_count, math::mean(<float> published) AS publish_rate FROM post GROUP BY author; -- Query the view instead of aggregating on every request SELECT * FROM post_stats WHERE author = user:alice; ``` **Why good:** `AS SELECT` creates an auto-updating materialized view, avoids running expensive aggregation on every read, `TYPE NORMAL` makes it queryable like a regular table --- ## Pattern 4: DEFINE ACCESS (Authentication) ### Good Example -- Record-Based Authentication ```surql -- Define access method for application users DEFINE ACCESS account ON DATABASE TYPE RECORD SIGNUP ( CREATE user SET email = string::lowercase($email), password = crypto::argon2::generate($password), name = $name, role = "user", created_at = time::now() ) SIGNIN ( SELECT * FROM user WHERE email = string::lowercase($email) AND crypto::argon2::compare(password, $password) ) WITH JWT ALGORITHM HS512 KEY "your-secret-key-min-64-chars-long-for-hs512-algorithm-security" WITH REFRESH DURATION FOR TOKEN 15m FOR SESSION 12h; ``` ```typescript // SDK usage -- signup await db.signup({ access: "account", variables: { email: "alice@example.com", password: "secure-password-123", name: "Alice", }, }); // SDK usage -- signin const token = await db.signin({ access: "account", variables: { email: "alice@example.com", password: "secure-password-123", }, }); // After signin, $auth is available in permissions // $auth.id = user:xxx (the authenticated user's record ID) ``` **Why good:** `crypto::argon2` for password hashing (not MD5/SHA), `string::lowercase` normalizes email, short token duration (15m) limits token theft impact, refresh tokens for long sessions, `$auth` available in all subsequent permission checks ### Good Example -- JWT-Based Authentication (External Provider) ```surql -- Validate tokens from external auth provider DEFINE ACCESS external_auth ON DATABASE TYPE JWT ALGORITHM RS256 URL "https://auth.example.com/.well-known/jwks.json"; -- Or with a static key DEFINE ACCESS api_auth ON DATABASE TYPE JWT ALGORITHM HS256 KEY "your-shared-secret"; ``` **Why good:** `URL` for JWKS-based dynamic key rotation (production), RS256 asymmetric algorithm prevents key exposure, static key option for simpler setups ### Bad Example -- Weak Authentication ```surql -- BAD: Multiple security issues DEFINE ACCESS account ON DATABASE TYPE RECORD SIGNUP ( CREATE user SET email = $email, -- Not normalized password = $password, -- Stored in PLAINTEXT role = $role -- User can set their own role! ) SIGNIN ( SELECT * FROM user WHERE email = $email AND password = $password ) WITH JWT ALGORITHM HS256 KEY "secret" DURATION FOR TOKEN 30d; -- Token valid for 30 days ``` **Why bad:** Password stored in plaintext (must use `crypto::argon2::generate`), user controls their own role (privilege escalation), email not normalized, weak JWT key, excessively long token duration --- ## Pattern 5: Indexes and Performance ### Good Example -- Index Definitions ```surql -- Unique index DEFINE INDEX email_unique ON user FIELDS email UNIQUE; -- Standard index for frequent queries DEFINE INDEX role_idx ON user FIELDS role; -- Composite index DEFINE INDEX user_role_created ON user FIELDS role, created_at; -- Full-text search index DEFINE ANALYZER custom_analyzer TOKENIZERS blank, class FILTERS lowercase, snowball(english); DEFINE INDEX post_search ON post FIELDS title, content SEARCH ANALYZER custom_analyzer BM25; -- Vector index for embeddings DEFINE INDEX embedding_idx ON document FIELDS embedding MTREE DIMENSION 1536; ``` **Why good:** `UNIQUE` for constraints, composite index for multi-field queries, BM25 full-text search with custom analyzer, MTREE for vector similarity search ### Good Example -- Full-Text Search Query ```surql -- Search with BM25 scoring SELECT *, search::score(1) AS relevance FROM post WHERE title @1@ "surrealdb tutorial" ORDER BY relevance DESC LIMIT 20; -- Highlight matching terms SELECT *, search::highlight("<b>", "</b>", 1) AS highlighted FROM post WHERE content @1@ "graph database" LIMIT 10; ``` **Why good:** `@1@` binds to index reference 1, `search::score` for relevance ranking, `search::highlight` for result snippets, `LIMIT` prevents unbounded results --- ## Pattern 6: Multi-Tenancy with Namespaces ### Good Example -- Tenant Isolation ```surql -- Root-level: create tenant namespaces DEFINE NAMESPACE tenant_acme; DEFINE NAMESPACE tenant_globex; -- Within each namespace, create identical schema -- (run for each tenant namespace) USE NS tenant_acme DB production; DEFINE TABLE user SCHEMAFULL; DEFINE FIELD name ON user TYPE string; DEFINE FIELD email ON user TYPE string; DEFINE INDEX email_idx ON user FIELDS email UNIQUE; ``` ```typescript // Switch tenant context at runtime async function switchTenant(db: Surreal, tenantId: string): Promise<void> { await db.use({ namespace: `tenant_${tenantId}`, database: "production", }); } // Tenant-scoped queries -- data is fully isolated await switchTenant(db, "acme"); const acmeUsers = await db.query<[User[]]>("SELECT * FROM user"); await switchTenant(db, "globex"); const globexUsers = await db.query<[User[]]>("SELECT * FROM user"); // acmeUsers and globexUsers are completely separate datasets ``` **Why good:** Namespace-level isolation guarantees data separation, identical schema per tenant, runtime context switching, no cross-tenant data leakage possible at the database level --- _For core patterns, see [core.md](core.md). For graph patterns, see [graph-relations.md](graph-relations.md). For live queries, see [live-queries.md](live-queries.md)._
-
-
reference.md 10.7 KB
# SurrealDB Reference > Decision frameworks, quick reference, SurrealQL operators, and ID generation strategies. See [SKILL.md](SKILL.md) for core concepts and [examples/](examples/) for code examples. --- ## Decision Framework ### Data Relationship: Link vs Edge vs Embed ``` Does the relationship need metadata (timestamp, weight, role)? ├─ YES → Use graph edges (RELATE) └─ NO → Is the relationship bidirectional? ├─ YES → Use graph edges (RELATE with <->) └─ NO → Is the related data always accessed with the parent? ├─ YES → Is it bounded (won't grow unbounded)? │ ├─ YES → Embed as nested object or record link field │ └─ NO → Use graph edges (unbounded arrays hit performance limits) └─ NO → Use record links (field = record:id) ``` ### Schema Mode: SCHEMAFULL vs SCHEMALESS ``` Are you in production? ├─ YES → SCHEMAFULL with DEFINE FIELD constraints └─ NO → Are you prototyping? ├─ YES → SCHEMALESS (fast iteration) └─ NO → Need some flexibility within strict schema? └─ SCHEMAFULL + FLEXIBLE TYPE on specific fields ``` ### Record ID Strategy ``` Do you need temporal sorting? ├─ YES → ULID (CREATE table:ulid()) └─ NO → Do you need globally unique IDs? ├─ YES → UUID v7 (CREATE table:uuid()) └─ NO → Do you need composite range queries? ├─ YES → Array-based IDs (table:['region', timestamp]) └─ NO → Do you need human-readable IDs? ├─ YES → String IDs (user:alice, product:widget-pro) └─ NO → Default random IDs (CREATE table) ``` ### Query Strategy ``` Are you reading data? ├─ YES → Do you know the exact record ID? │ ├─ YES → SELECT * FROM record:id (fastest -- direct lookup) │ └─ NO → Do you need to filter? │ ├─ YES → Is there an index on the filter field? │ │ ├─ YES → SELECT with WHERE (uses index) │ │ └─ NO → Add index or use full table scan (slow on large tables) │ └─ NO → SELECT * FROM table LIMIT $n └─ NO → Are you updating? ├─ YES → Is it a single record by ID? │ ├─ YES → UPDATE record:id SET ... (fast) │ └─ NO → UPDATE (SELECT id FROM table WHERE ...) SET ... (workaround for index use) └─ NO → Are you creating relationships? ├─ YES → Need metadata? → RELATE a->edge->b SET ... └─ NO → CREATE or INSERT ``` --- ## SurrealQL Quick Reference ### CRUD Statements | Statement | Description | Example | | --------- | ----------------------------- | ------------------------------------------------------------------------------- | | `CREATE` | Create record(s) | `CREATE user SET name = "Alice"` | | `INSERT` | Insert with conflict handling | `INSERT INTO user { name: "Alice" } ON DUPLICATE KEY UPDATE name = $input.name` | | `SELECT` | Query records | `SELECT * FROM user WHERE active = true` | | `UPDATE` | Replace all fields | `UPDATE user:alice SET name = "Alice B."` | | `UPSERT` | Create or update | `UPSERT user:alice SET name = "Alice", email = "a@b.com"` | | `DELETE` | Remove records | `DELETE user:alice` | | `RELATE` | Create graph edge | `RELATE user:a->follows->user:b` | ### Record ID Formats | Format | Example | Use Case | | ---------------- | ------------------------------------------- | --------------------------- | | Random (default) | `user:a1b2c3d4e5f6g7h8i9j0` | General purpose | | String | `user:alice` | Human-readable | | Numeric | `user:42` | Sequential, integer sorting | | ULID | `user:ulid()` | Temporally sortable | | UUID v7 | `user:uuid()` | Globally unique, sortable | | Array-based | `weather:['London', d'2025-01-01']` | Composite range queries | | Object-based | `log:{ ts: d'2025-01-01', level: 'error' }` | Multi-key lookups | ### Comparison Operators | Operator | Description | Example | | --------------- | ----------------------- | ----------------------------------- | | `=` / `==` | Equal | `WHERE age = 25` | | `!=` | Not equal | `WHERE status != "deleted"` | | `>` / `>=` | Greater than (or equal) | `WHERE age >= 18` | | `<` / `<=` | Less than (or equal) | `WHERE price < 100` | | `IN` / `NOT IN` | In set | `WHERE role IN ["admin", "mod"]` | | `CONTAINS` | Array contains value | `WHERE tags CONTAINS "typescript"` | | `CONTAINSALL` | Array contains all | `WHERE tags CONTAINSALL ["a", "b"]` | | `CONTAINSANY` | Array contains any | `WHERE tags CONTAINSANY ["a", "b"]` | | `~` / `!~` | Regex match | `WHERE email ~ "^admin@"` | | `@@` | Full-text search match | `WHERE content @@ "search term"` | ### Graph Traversal Syntax | Pattern | Direction | Example | | ---------------- | ---------------------------- | ------------------------------------------------- | | `->edge->target` | Forward | `SELECT ->follows->person.name FROM person:alice` | | `<-edge<-source` | Reverse | `SELECT <-follows<-person.name FROM person:bob` | | `<->edge<->any` | Bidirectional | `SELECT <->knows<->person.name FROM person:alice` | | `->?->?` | Any outgoing edge and target | `SELECT ->?->? FROM person:alice` | ### DEFINE Statements | Statement | Purpose | Example | | ------------------ | ---------------------------- | ---------------------------------------------------------- | | `DEFINE NAMESPACE` | Create namespace | `DEFINE NAMESPACE myapp` | | `DEFINE DATABASE` | Create database | `DEFINE DATABASE production` | | `DEFINE TABLE` | Define table schema | `DEFINE TABLE user SCHEMAFULL` | | `DEFINE FIELD` | Define field type/validation | `DEFINE FIELD email ON user TYPE string` | | `DEFINE INDEX` | Create index | `DEFINE INDEX email_idx ON user FIELDS email UNIQUE` | | `DEFINE ACCESS` | Auth method | `DEFINE ACCESS account ON DATABASE TYPE RECORD ...` | | `DEFINE EVENT` | Table trigger | `DEFINE EVENT log ON user WHEN $event = "CREATE" THEN ...` | | `DEFINE FUNCTION` | Custom function | `DEFINE FUNCTION fn::greet($name: string) { ... }` | | `DEFINE PARAM` | Global parameter | `DEFINE PARAM $env VALUE "production"` | ### Built-in Functions (Common) | Category | Function | Description | | -------- | --------------------------------------- | ---------------------------- | | Time | `time::now()` | Current datetime | | Time | `time::format($dt, $fmt)` | Format datetime | | Crypto | `crypto::argon2::generate($pass)` | Hash password | | Crypto | `crypto::argon2::compare($hash, $pass)` | Verify password | | String | `string::lowercase($s)` | Lowercase | | String | `string::is::email($s)` | Validate email | | String | `string::html::encode($s)` | HTML encode (XSS prevention) | | Math | `math::mean($arr)` | Average | | Array | `array::len($arr)` | Array length | | Type | `type::record($table, $id)` | Create record ID from parts | | Count | `count()` | Count in GROUP BY | --- ## Connection Options ### Development ```typescript await db.connect("http://127.0.0.1:8000", { namespace: "myapp", database: "development", }); await db.signin({ username: "root", password: "root" }); ``` ### Production ```typescript await db.connect("https://your-instance.surrealdb.com", { namespace: "myapp", database: "production", renewAccess: true, authentication: () => ({ access: "account", variables: { email: userEmail, pass: userPassword }, }), }); ``` --- ## Multi-Tenancy Pattern ``` Root level ├─ Namespace: "tenant_acme" │ ├─ Database: "production" │ └─ Database: "staging" ├─ Namespace: "tenant_globex" │ ├─ Database: "production" │ └─ Database: "staging" └─ Namespace: "shared" └─ Database: "config" ``` Each namespace is fully isolated -- users, tables, and data cannot cross namespace boundaries without root-level access. Use `db.use({ namespace, database })` to switch context. --- ## Index Types | Type | Syntax | Use Case | | --------- | ---------------------------------------------------------------------- | ---------------------- | | Standard | `DEFINE INDEX idx ON table FIELDS field` | Equality/range queries | | Unique | `DEFINE INDEX idx ON table FIELDS field UNIQUE` | Unique constraints | | Composite | `DEFINE INDEX idx ON table FIELDS a, b` | Multi-field queries | | Full-text | `DEFINE INDEX idx ON table FIELDS field SEARCH ANALYZER analyzer BM25` | Text search | | Vector | `DEFINE INDEX idx ON table FIELDS field MTREE DIMENSION 3` | Vector similarity | ### Index Limitations (Current) - Indexes are **not used** for `UPDATE ... WHERE` or `DELETE ... WHERE` -- use subquery pattern: `UPDATE (SELECT id FROM table WHERE condition) SET ...` - Query planner automatically selects indexes for `SELECT ... WHERE` - `UPSERT` is significantly faster with a unique index (index lookup vs table scan) -
SKILL.md 11.8 KB
--- name: api-database-surrealdb description: SurrealDB multi-model database - SurrealQL queries, record links, graph relations, live queries, schema definitions, authentication, TypeScript SDK --- # SurrealDB Patterns > **Quick Guide:** Use the `surrealdb` SDK (v2+) with `new Surreal()` and `connect()`. Model relationships with record links for simple pointers and `RELATE` for graph edges with metadata. Use `SCHEMAFULL` tables in production with `DEFINE FIELD` constraints. Always use parameterized queries (`$variable`) to prevent injection. Record IDs are `table:id` -- they are immutable and first-class values in SurrealQL. Live queries push changes without polling. --- <critical_requirements> ## CRITICAL: Before Using This Skill > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST use parameterized queries with `$variables` for ALL user input -- string interpolation in SurrealQL enables injection attacks)** **(You MUST use `new RecordId("table", "id")` in SDK v2 -- plain `"table:id"` strings are NOT automatically parsed as record IDs)** **(You MUST call `db.use({ namespace, database })` or pass namespace/database in `connect()` options BEFORE any queries -- queries without a selected namespace/database silently fail or error)** **(You MUST NOT rely on `SCHEMALESS` tables in production -- use `SCHEMAFULL` with `DEFINE FIELD` to enforce data integrity at the database layer)** **(You MUST NOT use `UPDATE`/`DELETE` with `WHERE` on large tables without indexes -- SurrealDB currently does not use indexes for UPDATE/DELETE WHERE clauses (use subquery workaround))** </critical_requirements> --- **Auto-detection:** SurrealDB, Surreal, surrealdb, SurrealQL, RELATE, RecordId, record link, LIVE SELECT, SCHEMAFULL, SCHEMALESS, DEFINE TABLE, DEFINE FIELD, DEFINE ACCESS, surql, graph traversal, ->relation->, <-relation<- **When to use:** - Connecting to SurrealDB and executing queries via the JavaScript SDK - Modeling data with record links and graph edges (`RELATE`) - Defining schemas with `SCHEMAFULL` tables and field constraints - Building real-time features with live queries - Implementing authentication with `DEFINE ACCESS` and record-level permissions - Multi-tenant architectures using namespaces and databases **Key patterns covered:** - SDK connection setup (v2 API with `Surreal`, `connect`, `RecordId`, `Table`) - CRUD operations with type-safe queries - Record links vs graph edges (when to use each) - Schema definitions (`DEFINE TABLE`, `DEFINE FIELD`, permissions) - Live queries for real-time subscriptions **When NOT to use:** - Heavy analytical/OLAP workloads (use a columnar database) - Simple key-value caching (use a dedicated cache) - Mature relational schemas that require decades of SQL ecosystem tooling **Detailed Resources:** - For decision frameworks and anti-patterns, see [reference.md](reference.md) **Core Patterns:** - [examples/core.md](examples/core.md) - SDK setup, connection, CRUD, TypeScript typing, RecordId **Graph & Relations:** - [examples/graph-relations.md](examples/graph-relations.md) - Record links, RELATE, graph traversal, edge metadata **Schema & Auth:** - [examples/schema-auth.md](examples/schema-auth.md) - DEFINE TABLE/FIELD, SCHEMAFULL, permissions, DEFINE ACCESS, authentication **Live Queries & Transactions:** - [examples/live-queries.md](examples/live-queries.md) - LIVE SELECT, subscriptions, transactions, events --- <philosophy> ## Philosophy SurrealDB is a multi-model database combining document, graph, and relational paradigms with a SQL-inspired query language (SurrealQL). The core principle: **model your data the way you think about it -- records link to records, relationships carry metadata, and schemas enforce integrity without separate migration tools.** **Core principles:** 1. **Record IDs are first-class** -- Every record has a `table:id` identity that doubles as a direct pointer. SurrealDB fetches linked records from disk without table scans. 2. **Graph when you need metadata, link when you don't** -- Record links (`friends = [person:tobie]`) are lightweight pointers. Graph edges (`RELATE person:a->follows->person:b`) store relationship context (timestamps, weights, roles). 3. **Schema-full for production** -- `SCHEMAFULL` tables with `DEFINE FIELD` constraints enforce types, validation, and defaults at the database layer. Use `SCHEMALESS` only for rapid prototyping. 4. **Permissions at every level** -- Namespace, database, table, and field-level permissions. `DEFINE ACCESS` with `SIGNUP`/`SIGNIN` enables end-user authentication without a separate auth service. 5. **Real-time by default** -- `LIVE SELECT` pushes changes to subscribers as they commit. No polling, no message broker. 6. **Parameterize everything** -- SurrealQL variables (`$email`, `$limit`) prevent injection and improve query plan caching. </philosophy> --- <patterns> ## Core Patterns ### Pattern 1: SDK Connection SDK v2 uses `new Surreal()` -- always set namespace/database at connection time and use `127.0.0.1` (not `localhost`, which can fail with IPv6 on Node.js 18+). ```typescript import Surreal from "surrealdb"; const db = new Surreal(); await db.connect("http://127.0.0.1:8000", { namespace: "myapp", database: "production", }); await db.signin({ username: "root", password: "root" }); ``` Full connection patterns (production config, event monitoring, graceful shutdown): [examples/core.md](examples/core.md) --- ### Pattern 2: CRUD with RecordId SDK v2 requires `RecordId` objects -- plain strings are NOT automatically parsed as record IDs. Use `Table` for table-scoped operations, `RecordId` for specific records. ```typescript import { RecordId, Table } from "surrealdb"; const created = await db.create<User>(new Table("user"), { name: "Alice", role: "user", }); const user = await db.select<User>(new RecordId("user", "alice")); await db.merge(new RecordId("user", "alice"), { role: "admin" }); await db.delete(new RecordId("user", "alice")); ``` Full CRUD patterns (create, select, update, delete, bulk operations): [examples/core.md](examples/core.md) --- ### Pattern 3: Parameterized Queries Always bind user input as `$parameters` -- never interpolate strings into SurrealQL. Multi-statement queries return typed tuples. ```typescript const users = await db.query<[User[]]>( `SELECT * FROM user WHERE role = $role LIMIT $limit`, { role: "admin", limit: 20 }, ); // BAD: enables SurrealQL injection await db.query(`SELECT * FROM user WHERE email = '${userInput}'`); ``` Full query patterns (pagination, multi-statement, RecordId parameters): [examples/core.md](examples/core.md) --- ### Pattern 4: Record Links (Lightweight Pointers) Record links are field-level pointers fetched via dot notation -- no JOINs required. Use for simple, unidirectional references without relationship metadata. ```surql CREATE person:alice SET best_friend = person:bob, friends = [person:bob, person:carol]; SELECT best_friend.name AS friend_name FROM person:alice; ``` **When NOT to use:** When you need relationship metadata, bidirectional traversal, or relationship-level permissions -- use graph edges instead. Full record link patterns: [examples/graph-relations.md](examples/graph-relations.md) --- ### Pattern 5: Graph Edges with RELATE Graph edges are full records in a relation table, supporting metadata, bidirectional traversal (`<->`), and schema constraints via `DEFINE TABLE TYPE RELATION`. ```surql RELATE person:alice->follows->person:bob SET followed_at = time::now(), strength = "close"; SELECT ->follows->person.name AS following FROM person:alice; -- forward SELECT <-follows<-person.name AS followers FROM person:bob; -- reverse ``` **When to use:** Relationships needing metadata, bidirectional queries, social graphs, access control graphs. Full graph patterns (typed relations, edge metadata, recursive traversal): [examples/graph-relations.md](examples/graph-relations.md) </patterns> --- <red_flags> ## RED FLAGS **High Priority Issues:** - Using string interpolation instead of `$parameters` in SurrealQL queries -- enables injection attacks - Using `"table:id"` strings instead of `new RecordId("table", "id")` in SDK v2 -- strings are not auto-parsed as record IDs - Running queries without selecting namespace/database -- queries silently fail or return errors - Using `SCHEMALESS` tables in production without explicit field definitions -- data integrity not enforced **Medium Priority Issues:** - `UPDATE table SET ... WHERE condition` on large tables without indexes -- SurrealDB does not use indexes for UPDATE/DELETE WHERE (use `UPDATE (SELECT id FROM table WHERE condition) SET ...` as workaround) - Using `UPSERT` without a unique index -- `UPSERT` is much more performant with unique indexes (avoids table scan) - Embedding unbounded arrays as record links -- arrays can grow without limit; use graph edges for unbounded relationships - Not setting `DURATION FOR TOKEN` and `DURATION FOR SESSION` on `DEFINE ACCESS` -- tokens/sessions without expiry are a security risk **Common Mistakes:** - Creating duplicate record IDs silently fails or errors depending on context -- use `INSERT ... ON DUPLICATE KEY UPDATE` or `UPSERT` for idempotent operations - Expecting `record:id` strings to sort numerically -- `record:1`, `record:10`, `record:2` sorts lexicographically; use numeric IDs (`record:1`, `record:2`, `record:10`) or ULID/UUID for temporal sorting - Forgetting that record IDs are immutable -- you cannot change a record's ID after creation; you must create a new record and delete the old one - Using `rand()`, `ulid()`, or `uuid()` in `DEFINE FUNCTION` bodies -- these generate the same value per function call, causing duplicate key errors on subsequent calls - Confusing `DEFINE FIELD ... VALUE` (recalculated on create/update) with `DEFINE FIELD ... COMPUTED` (recalculated on access, v3.0+) - Setting `id` field in `CREATE table:specific_id SET id = "other"` -- the explicit record ID takes precedence and the `id` in SET is silently discarded **Gotchas & Edge Cases:** - Fields defined with `VALUE` are recalculated alphabetically -- if field `b` depends on field `a`, naming matters - `FLEXIBLE TYPE` on a `SCHEMAFULL` table allows schemaless nested objects -- useful for JSON metadata but bypasses type checking on that subtree - `LIVE SELECT` with complex `WHERE` filters may not fire for all edge cases -- test your filters thoroughly - `localhost` in connection strings can fail on Node.js 18+ due to IPv6 preference -- use `127.0.0.1` - Numeric string IDs (`"10"`) display as backtick-escaped (`table:\`10\``) to differentiate from numeric IDs (`table:10`) - Record References (`DEFINE FIELD ... REFERENCE`) are experimental (require `--allow-experimental record_references`) -- do not use in production </red_flags> --- <critical_reminders> ## CRITICAL REMINDERS > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST use parameterized queries with `$variables` for ALL user input -- string interpolation in SurrealQL enables injection attacks)** **(You MUST use `new RecordId("table", "id")` in SDK v2 -- plain `"table:id"` strings are NOT automatically parsed as record IDs)** **(You MUST call `db.use({ namespace, database })` or pass namespace/database in `connect()` options BEFORE any queries -- queries without a selected namespace/database silently fail or error)** **(You MUST NOT rely on `SCHEMALESS` tables in production -- use `SCHEMAFULL` with `DEFINE FIELD` to enforce data integrity at the database layer)** **(You MUST NOT use `UPDATE`/`DELETE` with `WHERE` on large tables without indexes -- SurrealDB currently does not use indexes for UPDATE/DELETE WHERE clauses (use subquery workaround))** **Failure to follow these rules will cause injection vulnerabilities, silent query failures, or data integrity issues.** </critical_reminders>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.