A type-safe TypeScript ORM for SurrealDB that provides full type inference, a fluent query builder, comprehensive CRUD operations, and first-class support for graph relationships, and database functions.
- Type-safe schema definitions - Define your database schema using intuitive
t.*builders - Automatic type inference - Get full TypeScript types without code generation
- Fluent query builder - Chain
.select(),.where(),.return()with full type safety - Complete CRUD operations - SELECT, CREATE, UPDATE, DELETE, and UPSERT queries
- Graph relationships - First-class support for edges and graph traversal
- Rich type system - Objects, arrays, unions, literals, options, and more
- SurrealDB functions - Integrated string, array, and record operations
bun add surqlize
# or
npm install surqlizeimport { Surreal } from "surrealdb";
import { orm, table, t } from "surqlize";
// Define a table schema
const user = table("user", {
name: t.string(),
email: t.string(),
age: t.number(),
created: t.date(),
});
// Create ORM instance
const db = orm(new Surreal(), user);
// Build type-safe queries
const query = db
.select("user")
.where((user) => user.age.gte(18))
.return((user) => ({
name: user.name,
email: user.email,
}));
// TypeScript knows the exact return type!
type Result = t.infer<typeof query>;
// Result: Array<{ name: string; email: string }>Define tables using the table() function with a rich type system:
import { table, t } from "surqlize";
const user = table("user", {
// Basic types
name: t.string(),
age: t.number(),
isActive: t.bool(),
created: t.date(),
userId: t.uuid(),
// Complex objects
address: t.object({
street: t.string(),
city: t.string(),
zipCode: t.string(),
}),
// Arrays
tags: t.array(t.string()),
scores: t.array(t.number()),
// Mixed-type arrays (tuples)
mixedData: t.array([t.string(), t.number(), t.bool()]),
// Optional fields
bio: t.option(t.string()),
// Record references (foreign keys)
authorId: t.record("author"),
// Union types
status: t.union([
t.literal("active"),
t.literal("inactive"),
t.literal("pending"),
]),
// Literals
role: t.literal("admin"),
});Note: Every table automatically includes an id field of type RecordId<TableName>.
Define graph edges to model relationships between tables:
import { edge, table, t } from "surqlize";
const user = table("user", {
name: t.string(),
email: t.string(),
});
const post = table("post", {
title: t.string(),
content: t.string(),
});
// Define an edge from user to post
const authored = edge("user", "authored", "post", {
created: t.date(),
role: t.union([t.literal("author"), t.literal("co-author")]),
});
const db = orm(new Surreal(), user, post, authored);Automatic fields: Edges automatically include:
id: RecordId of the edgein: RecordId of the source table (user)out: RecordId of the target table (post)
// Select all records
const allUsers = db.select("user");
// Select with WHERE clause
const adults = db
.select("user")
.where((user) => user.age.gte(18));
// Project specific fields with RETURN
const userNames = db
.select("user")
.return((user) => ({
fullName: user.name,
email: user.email,
}));
// Pagination
const paginatedUsers = db
.select("user")
.start(10)
.limit(20);
// Select a single record by ID (returns array with 0 or 1 item)
const specificUser = await db.select(new RecordId("user", "john"));
// To get the first item, use .val() or .at(0):
const specificUser = await db.select(new RecordId("user", "john")).then.val();
// Or get a specific item:
const specificUser = await db.select(new RecordId("user", "john")).then.at(0);
// Nested queries (JOIN-like)
const postsWithAuthors = db.select("post").return((post) => ({
title: post.title,
author: post.authorId.select().return((author) => ({
name: author.name,
email: author.email,
})),
}));// Order by single field
const sorted = db.select("user")
.orderBy("age", "DESC");
// Order by multiple fields
const multiSort = db.select("user")
.orderBy("lastName", "ASC")
.orderBy("firstName", "ASC");
// Order by with callback (for nested fields)
const nestedSort = db.select("user")
.orderBy(user => user.name.last, "ASC");
// Numeric sorting
const numericSort = db.select("user")
.orderByNumeric("age", "DESC");
// Collation sorting
const collateSort = db.select("user")
.orderByCollate("name", "ASC");// Group by field(s)
const grouped = db.select("post")
.groupBy("author");
// Group all (for table-wide aggregates)
const totalCount = db.select("user")
.groupAll();// Fetch linked records
const withAuthor = db.select("post")
.fetch("author");
// Fetch multiple relations
const deepFetch = db.select("post")
.fetch("author", "comments");// Split array field into multiple records
const splitTags = db.select("post")
.split("tags");
// Split multiple arrays
const multiSplit = db.select("post")
.split("tags", "categories");// Set timeout duration
const withTimeout = db.select("user")
.where(user => user.age.gt(18))
.timeout("5s");// Complex query with multiple clauses
const complexQuery = db.select("post")
.where(post => post.title.startsWith("Hello"))
.split("tags")
.orderBy("created", "DESC")
.limit(20)
.fetch("author")
.timeout("10s");Create a new record with a specific id or a generated id.
// Create with SET
const newUser = await db.create("user").set({
name: "Alice",
email: "alice@example.com",
age: 30,
created: new Date(),
});
// Create with CONTENT
const newPost = await db.create("post").content({
title: "Hello World",
body: "First post!",
authorId: new RecordId("user", "alice"),
published: true,
});
// Create with explicit ID
const user = await db.create("user", "alice123").set({
name: "Alice",
email: "alice@example.com",
});
// Control return value
const created = await db.create("user")
.set({ name: "Bob" })
.return("after"); // or "before", "none", "diff"Insert one or multiple records with support for bulk operations and conflict handling.
// Insert single record (object style)
await db.insert("user", {
name: "Alice",
email: "alice@example.com",
age: 30,
});
// Bulk insert (object style)
await db.insert("user", [
{ name: "Alice", email: "alice@example.com", age: 30 },
{ name: "Bob", email: "bob@example.com", age: 25 },
{ name: "Charlie", email: "charlie@example.com", age: 28 },
]);
// VALUES tuple syntax
await db.insert("user")
.fields(["name", "email", "age"])
.values(
["Alice", "alice@example.com", 30],
["Bob", "bob@example.com", 25]
);
// IGNORE duplicates (skip conflicts silently)
await db.insert("user", userData).ignore();
// ON DUPLICATE KEY UPDATE (update on conflict)
await db.insert("user", {
id: "alice",
name: "Alice",
age: 30
})
.onDuplicate({
age: { "+=": 1 },
lastSeen: new Date(),
});
// With operators in ON DUPLICATE
await db.insert("post", posts)
.onDuplicate({
views: { "+=": 1 },
tags: { "+=": ["updated"] },
});
// With RETURN clause
const inserted = await db.insert("user", data).return("after");
// With RETURN projection
const insertedNames = await db.insert("user", data)
.return(u => ({ name: u.name }));Create a record if it doesn't exist, update records if matching records exist.
// Upsert with SET
await db.upsert("user", "alice")
.set({
name: "Alice",
email: "alice@example.com",
age: 30,
});
// Upsert with operators (atomic increment)
await db.upsert("pageview", "homepage")
.set({
count: { "+=": 1 },
lastViewed: new Date(),
});
// Upsert with MERGE
await db.upsert("user", "alice")
.merge({ lastLogin: new Date() });
// Bulk upsert with WHERE
await db.upsert("user")
.where((u) => u.email.eq("alice@example.com"))
.set({ lastSeen: new Date() });Update a record or multiple records in a table.
// Update with SET
await db.update("user", "alice")
.set({ age: 31 });
// Bulk update with WHERE
await db.update("user")
.where((u) => u.age.lt(18))
.set({ status: "minor" });
// Array and number operators
await db.update("user", "alice")
.set({
age: { "+=": 1 }, // Increment
tags: { "+=": ["developer"] }, // Append to array
oldTags: { "-=": ["beginner"] }, // Remove from array
});
// CONTENT (replace entire record)
await db.update("user", "alice")
.content({
name: "Alice Smith",
email: "alice@example.com",
age: 31,
});
// MERGE (partial update)
await db.update("user", "alice")
.merge({ email: "newemail@example.com" });
// PATCH (JSON Patch operations)
await db.update("user", "alice")
.patch([
{ op: "replace", path: "/age", value: 32 },
{ op: "remove", path: "/oldField" },
]);
// UNSET (remove fields)
await db.update("user", "alice")
.set({ name: "Alice" })
.unset(["oldField1", "oldField2"]);
// Return modified records
const updated = await db.update("user")
.where((u) => u.age.gt(65))
.set({ status: "senior" })
.return("after");Create graph edges between records using defined edge schemas.
// Single edge between two records
const edge = await db.relate(
"authored",
new RecordId("user", "alice"),
new RecordId("post", "hello-world")
);
// With edge data using content()
const friendship = await db.relate(
"knows",
new RecordId("user", "user1"),
new RecordId("user", "user2")
).content({
since: new Date(),
strength: 5,
});
// With edge data using set()
const likes = await db.relate(
"likes",
new RecordId("user", "userId"),
new RecordId("post", "postId")
).set({
created: new Date(),
rating: 5,
});
// Cartesian product: create multiple edges
// Creates: alice->authored->post1, alice->authored->post2,
// bob->authored->post1, bob->authored->post2
const edges = await db.relate(
"authored",
[new RecordId("user", "alice"), new RecordId("user", "bob")],
[new RecordId("post", "post1"), new RecordId("post", "post2")]
);
// Control return mode
await db.relate(
"authored",
new RecordId("user", "user"),
new RecordId("post", "post")
).content({ created: new Date() })
.return("after"); // or "before", "none", "diff"
// With return projection
const edgeInfo = await db.relate(
"follows",
new RecordId("user", "follower"),
new RecordId("user", "followee")
).set({ since: new Date() })
.return(edge => ({
id: edge.id,
from: edge.in,
to: edge.out,
since: edge.since,
}));
// Using with query results
const userQuery = db.select("user", "alice");
const postQuery = db.select("post", "hello");
await db.relate("authored", userQuery, postQuery);// Delete single record (returns array with 0 or 1 item)
await db.delete("user", "alice");
// Bulk delete with WHERE
await db.delete("user")
.where((u) => u.age.lt(13));
// Return deleted records
const deleted = await db.delete("user")
.where((u) => u.status.eq("inactive"))
.return("before");
// Delete with projection
const deletedNames = await db.delete("user")
.where((u) => u.email.endsWith("@spam.com"))
.return((u) => ({ name: u.name }));Execute multiple queries as a single atomic operation in one round-trip. No intermediate results are available — all queries succeed or all fail together.
// Multiple queries in a single atomic operation
const [user, updated, allUsers] = await db.batch(
db.create("user").set({ name: "Alice", age: 30 }),
db.update("user", "bob").set({ age: 31 }),
db.select("user"),
);
// Results are fully typed as a tupleYou can also inspect the generated SurrealQL before executing:
const b = db.batch(
db.create("user").set({ name: "Alice" }),
db.update("user", "bob").set({ age: 31 }),
);
console.log(b.toString());
// BEGIN TRANSACTION; CREATE user SET name = $_v0; UPDATE user:bob SET age = $_v1; COMMIT TRANSACTION;
// Execute when ready
const [created, updated] = await b;Open a server-side transaction, execute queries one-by-one with intermediate results, and decide whether to commit or cancel based on the outcomes.
The callback form automatically commits on success and cancels on error:
const result = await db.transaction(async (tx) => {
const user = await tx.create("user").set({
name: "Alice",
age: 30,
});
// Use intermediate results to make decisions
if (user.age > 25) {
await tx.update("user", user.id).set({ status: "senior" });
}
return user;
});
// Transaction is committed automaticallyFor full control, use the manual form:
const tx = await db.transaction();
try {
const user = await tx.create("user").set({ name: "Alice" });
await tx.relate("authored", user.id, new RecordId("post", "hello"));
await tx.commit();
} catch (e) {
await tx.cancel();
throw e;
}The transaction object (tx) has all the same query-builder methods as the main db instance — select, create, insert, update, upsert, delete, and relate.
All queries in Surqlize return arrays, even when selecting by a specific record ID. To access the first item from a query result, use .val() or .at(index):
// .val() - Returns the first item or undefined
const user = await db.select("user", "alice").then.val();
// user: User | undefined
// .at(index) - Returns the item at the specified index or undefined
const firstUser = await db.select("user").then.at(0);
const secondUser = await db.select("user").then.at(1);
const lastUser = await db.select("user").then.at(-1); // negative indexing supported
// Working with arrays directly
const users = await db.select("user", "alice");
// users: User[]
if (users.length > 0) {
const user = users[0];
}
// Use with update, delete, and upsert
const updated = await db.update("user", "alice")
.set({ age: 31 })
.return("after")
.then.val();
const deleted = await db.delete("user", "alice")
.return("before")
.then.val();All types support these comparison operators:
db.select("user").where((user) =>
// Equality
user.name.eq("John") // =
user.age.ne(25) // !=
user.email.ex("john@example.com") // == (exact match)
// Comparison
user.age.gt(18) // >
user.age.gte(21) // >=
user.age.lt(65) // <
user.age.lte(64) // <=
// Array membership
user.status.inside(["active", "pending"]) // IN
user.status.notInside(["banned", "deleted"]) // NOT IN
// Logical operators
user.age.gte(18).and(user.isActive.eq(true))
user.role.eq("admin").or(user.role.eq("moderator"))
user.isActive.not()
// Truthiness checks
user.bio.trueish() // !! (double negation - checks for truthy value)
user.archived.falseish() // ! (negation - checks for falsy value)
);For complex conditions, use the standalone and() and or() combiners. These make precedence explicit and produce correctly parenthesized SurrealQL:
import { orm, table, t, and, or } from "surqlize";
// Simple compound: age >= 18 AND email ends with @example.com
db.select("user").where((user) =>
and(user.age.gte(18), user.email.endsWith("@example.com"))
);
// WHERE (age >= 18 AND string::ends_with(email, "@example.com"))
// OR with multiple options
db.select("user").where((user) =>
or(user.role.eq("admin"), user.role.eq("moderator"), user.role.eq("owner"))
);
// WHERE (role = "admin" OR role = "moderator" OR role = "owner")
// Nested: AND with inner OR for grouped conditions
db.select("user").where((user) =>
and(
user.age.gte(18),
or(user.role.eq("admin"), user.role.eq("moderator")),
user.email.endsWith("@example.com"),
)
);
// WHERE (age >= 18 AND (role = "admin" OR role = "moderator") AND string::ends_with(email, "@example.com"))
// Chaining .and() / .or() on individual conditions also works
db.select("user").where((user) =>
user.age.gte(18).and(user.name.first.eq("Alice"))
);
// WHERE (age >= 18 AND name.first = "Alice")Both and() and or() require at least two conditions and accept any number of additional conditions. Nesting them produces correctly parenthesized output, so precedence is always explicit.
db.select("user").where((user) =>
user.email.startsWith("admin@")
user.name.endsWith("son")
user.email.contains("@example.com")
user.email.isEmail()
);
db.select("user").return((user) => ({
fullName: user.firstName.join(" ", user.lastName),
nameLength: user.name.len(),
upper: user.name.uppercase(),
lower: user.email.lowercase(),
trimmed: user.name.trim(),
slug: user.name.slug(),
words: user.name.words(),
reversed: user.name.reverse(),
replaced: user.email.replace("@old.com", "@new.com"),
parts: user.email.split("@"),
}));Additional string functions include capitalize, repeat, slice, matches, distance functions (distanceLevenshtein, distanceHamming, etc.), HTML functions (htmlEncode, htmlSanitize), validation (isUrl, isDomain, isUuid, etc.), semver operations, and similarity scoring.
db.select("user").where((user) =>
// Single element checks
user.tags.contains("typescript") // Array contains element
user.tags.containsNot("java") // Array doesn't contain element
// Multiple element checks
user.tags.containsAll(["javascript", "typescript"]) // Contains all elements
user.tags.containsAny(["rust", "go", "python"]) // Contains any element
user.tags.containsNone(["php", "perl"]) // Contains none of elements
// Inside checks (array subset operations)
user.tags.allInside(allowedTags) // All elements are in allowedTags
user.tags.anyInside(popularTags) // Any element is in popularTags
user.tags.noneInside(bannedTags) // No elements are in bannedTags
// Empty check
user.tags.isEmpty() // Array is empty
);
db.select("post").return((post) => ({
title: post.title,
firstTag: post.tags.at(0), // Get element at index
tagCount: post.tags.len(), // Array length
first: post.tags.first(), // First element
last: post.tags.last(), // Last element
sorted: post.tags.sort(), // Sort array
unique: post.tags.distinct(), // Unique values
flat: post.tags.flatten(), // Flatten nested arrays
reversed: post.tags.reverse(), // Reverse array
}));Additional array functions include mutation (add, append, prepend, push, pop, insert, remove, fill, swap), set operations (combine, complement, concat, difference, intersect, union, transpose), boolean operations (booleanAnd, booleanOr, logicalAnd, etc.), and search functions (findIndex, filterIndex, max, min).
db.select("user").return((user) => ({
absAge: user.age.abs(),
rounded: user.age.round(),
ceiling: user.age.ceil(),
floored: user.age.floor(),
squareRoot: user.age.sqrt(),
squared: user.age.pow(2),
fixed: user.age.fixed(2),
clamped: user.age.clamp(0, 100),
radians: user.age.deg2rad(),
sine: user.age.sin(),
cosine: user.age.cos(),
naturalLog: user.age.ln(),
log10: user.age.log10(),
}));Additional number functions include tan, cot, acos, asin, atan, acot, log, log2, rad2deg, sign, lerp, lerpangle.
db.select("user").return((user) => ({
year: user.created.year(),
month: user.created.month(),
day: user.created.day(),
hour: user.created.hour(),
minute: user.created.minute(),
second: user.created.second(),
weekDay: user.created.wday(),
dayOfYear: user.created.yday(),
unix: user.created.unix(),
millis: user.created.millis(),
formatted: user.created.format("%Y-%m-%d"),
isLeap: user.created.isLeapYear(),
}));Additional date functions include week, micros, nano, and rounding functions (timeCeil, timeFloor, timeRound).
When working with optional values (created with t.option()), you can use map() to transform the value if it exists:
const user = table("user", {
name: t.string(),
bio: t.option(t.string()),
});
db.select("user").return((user) => ({
name: user.name,
// Transform bio to uppercase if it exists
bioUpper: user.bio.map((b) => b.toUpperCase()),
// Chain multiple operations
bioLength: user.bio.map((b) => b.len()),
}));When you have a record reference, you can perform nested queries:
const post = table("post", {
title: t.string(),
authorId: t.record("user"),
});
// Nested query with .select()
const query = db.select("post").return((post) => ({
title: post.title,
author: post.authorId.select().return((author) => ({
name: author.name,
email: author.email,
})),
}));
// TypeScript infers the complete nested type!
type Result = t.infer<typeof query>;
// Result: Array<{
// title: string;
// author: { name: string; email: string } | undefined;
// }>Standalone functions are not called on a field but used independently within query callbacks. Functions with value parameters extract the query context automatically from the first value. Zero-arg functions and constants require an explicit context source (any Workable from the callback).
import { count, math, time, crypto, rand, parse } from "surqlize";
// Count and aggregation
db.select("user")
.groupAll()
.return((user) => ({
total: count(user),
adults: count(user, user.age.gte(18)),
avgAge: math.mean(user.age),
totalAge: math.sum(user.age),
maxAge: math.max(user.age),
}));
// Time and crypto
db.select("user").return((user) => ({
now: time.now(user),
emailHash: crypto.sha256(user.email),
randomId: rand.uuid(user),
emailDomain: parse.emailHost(user.email),
}));
// Math constants (zero-arg, need context source)
db.select("user").return((user) => ({
pi: math.pi(user),
e: math.e(user),
tau: math.tau(user),
}));Available standalone function families: count, math (aggregation + constants), time, crypto, rand, duration, type_, encoding, geo, http, meta, object, parse, search, session, set_, sleep, value, vector, bytes, not.
Control what gets returned from mutations:
// Return nothing
await db.update("user", "alice").set({ age: 31 }).return("none");
// Return state before modification
const before = await db.update("user", "alice")
.set({ age: 31 })
.return("before");
// Return state after modification (default)
const after = await db.update("user", "alice")
.set({ age: 31 })
.return("after");
// Return diff of changes
const diff = await db.update("user", "alice")
.set({ age: 31 })
.return("diff");
// Return specific fields with projection
const projection = await db.update("user", "alice")
.set({ age: 31, email: "new@email.com" })
.return((u) => ({ name: u.name, age: u.age }));const users = await db.select("user")
.where((u) => u.age.gt(18))
.timeout("5s");
await db.update("user", "alice")
.set({ age: 31 })
.timeout("10s");Use operators for atomic operations:
// Increment/decrement numbers
db.update("user", "alice").set({
age: { "+=": 1 },
score: { "-=": 10 },
});
// Add/remove from arrays
db.update("post", "post1").set({
tags: { "+=": ["typescript", "database"] },
oldTags: { "-=": ["deprecated"] },
});Extract TypeScript types from your queries using t.infer<>:
// Infer query result type
const query = db.select("user").return((user) => ({
name: user.name,
age: user.age,
}));
type QueryResult = t.infer<typeof query>;
// QueryResult: Array<{ name: string; age: number }>
// Infer table type
const userTable = table("user", {
name: t.string(),
age: t.number(),
});
type User = t.infer<typeof userTable>;
// User: { id: RecordId<"user">; name: string; age: number }
// Infer individual type definitions
const emailType = t.string();
type Email = t.infer<typeof emailType>;
// Email: stringInspect generated SurrealQL:
import { displayContext, __display } from "surqlize";
const query = db.select("user").where((u) => u.age.gte(18));
const ctx = displayContext();
const sql = query[__display](ctx);
console.log(sql); // Generated SurrealQL
console.log(ctx.variables); // Parameterized valuesSurqlize provides type-safe graph traversal through the lookup system:
const user = table("user", { name: t.string() });
const post = table("post", { title: t.string() });
const authored = edge("user", "authored", "post", {});
const db = orm(new Surreal(), user, post, authored);
// TypeScript knows which edges connect to which tables
db.lookup.to; // { user: ["authored"], authored: ["post"], post: [] }
db.lookup.from; // { user: [], authored: ["user"], post: ["authored"] }
// Use in queries for type-safe graph navigation
// (This feature is under active development)Here's a complete example showcasing multiple features:
const user = table("user", {
name: t.object({
first: t.string(),
last: t.string(),
}),
age: t.number(),
email: t.string(),
tags: t.array(t.string()),
bio: t.option(t.string()),
});
const post = table("post", {
title: t.string(),
content: t.string(),
authorId: t.record("user"),
created: t.date(),
});
const authored = edge("user", "authored", "post", {
created: t.date(),
});
const db = orm(new Surreal(), user, post, authored);
// Complex query with nested data and string operations
const query = db
.select("post")
.where((post) =>
post.title.startsWith("Guide").and(
post.created.gte(new Date("2024-01-01"))
)
)
.return((post) => ({
title: post.title,
author: post.authorId.select().return((author) => ({
fullName: author.name.first.join(" ", author.name.last),
age: author.age,
hasBio: author.bio.trueish(),
})),
}))
.orderBy("created", "DESC")
.limit(10);
// Fully typed result
type Result = t.infer<typeof query>;
// Fetch resolves record references into full objects
const posts = await db
.select("post")
.fetch("authorId")
.execute();
// posts[0].authorId is now the full user object, not a RecordIdSurqlize accepts any SurrealSession (or Surreal, which extends it), enabling multiple ORM instances scoped to different sessions over a single connection. Each session maintains its own namespace, database, authentication state, and variables.
import { Surreal } from "surrealdb";
import { orm, table, t } from "surqlize";
const user = table("user", { name: t.string(), age: t.number() });
const surreal = new Surreal();
await surreal.connect("ws://localhost:8000");
await surreal.signin({ username: "root", password: "root" });
// Create separate sessions for different tenants
const tenantA = await surreal.newSession();
await tenantA.signin({ username: "root", password: "root" });
await tenantA.use({ namespace: "app", database: "tenant_a" });
const tenantB = await surreal.newSession();
await tenantB.signin({ username: "root", password: "root" });
await tenantB.use({ namespace: "app", database: "tenant_b" });
// Same schema, same connection, different databases
const dbA = orm(tenantA, user);
const dbB = orm(tenantB, user);
await dbA.create("user").set({ name: "Alice", age: 30 });
await dbB.create("user").set({ name: "Bob", age: 25 });Use forkSession() to clone an existing session (inheriting its namespace, database, auth, and variables) and then diverge:
const surreal = new Surreal();
await surreal.connect("ws://localhost:8000");
await surreal.signin({ username: "root", password: "root" });
await surreal.use({ namespace: "app", database: "main" });
// Fork inherits namespace, database, auth, and variables
const session = await surreal.forkSession();
await session.authenticate(userToken);
const db = orm(session, user);
const users = await db.select("user");
// Clean up when done
await session.closeSession();Since SurrealSession implements Symbol.asyncDispose, sessions work with await using for automatic cleanup:
{
await using session = await surreal.forkSession();
await session.authenticate(userToken);
const db = orm(session, user);
const users = await db.select("user");
// session automatically disposed when scope exits
}| Feature | Surqlize | SurrealDB.js | Prisma | Drizzle | TypeORM |
|---|---|---|---|---|---|
| SurrealDB support | ✅ | ✅ | ❌ | ❌ | ❌ |
| Schema definition | ✅ Code-first | ❌ | ✅ Schema file | ✅ Code-first | |
| Type inference | ✅ Full | ✅ With codegen | ✅ Full | ||
| CRUD operations | ✅ All operations | ✅ | ✅ | ✅ | ✅ |
| Graph and edges | ✅ Native | ✅ Manual | ❌ | ❌ | ❌ |
| Query builder | ✅ Type-safe | ✅ Type-safe | ✅ Query builder | ||
| Database Functions | ✅ Integrated | ✅ SQL functions | ✅ Query functions | ||
| Nested Queries | ✅ Type-safe | ✅ Relations | ✅ Joins | ✅ Relations | |
| Fluent API | ✅ | ❌ | ❌ | ✅ | ✅ |
Why Surqlize?
- Native SurrealDB support: Built specifically for SurrealDB's unique features including graph relationships, flexible schemas, and SurrealQL
- No code generation: Full type inference using TypeScript's type system—no codegen required
- Fluent API: Natural, chainable syntax that mirrors SurrealQL while providing complete type safety
- Graph-first: Edges and relationships are first-class citizens, not an afterthought
- Complete CRUD: Full support for SELECT, CREATE, UPSERT, UPDATE, RELATE, and DELETE operations
This project is in active development. Planned features include:
- SurrealDB functions - All 25 built-in function families (string, array, math, time, crypto, rand, and more)
- Advanced query clauses - ORDER BY, GROUP BY, FETCH, SPLIT
- Transaction support - Batch and interactive transactions
- Multi-session support - Multiple sessions over a single connection
- Runtime validation - Validate data at runtime using schema definitions
- Advanced graph traversal - Path finding, recursive queries, graph algorithms
- Performance optimizations - Query caching, connection pooling
- Schema migrations - Version control for database schemas
- Documentation site - Comprehensive guides and API reference
# Install dependencies
bun install
# Build the project
bun run build
# Run the example file
bun run examples/demo.ts
# Run tests
bun run test:unit # Unit tests
bun run test:integration # Integration tests (requires SurrealDB)
bun run type-check # TypeScript type checking
# Lint and format
bun run qc # Check for issues
bun run qa # Auto-fix issues
bun run qau # Auto-fix with unsafe changesContributions are welcome! This project is in an experimental stage, so expect breaking changes. If you'd like to contribute:
- Open an issue to discuss your idea
- Fork the repository
- Create a feature branch
- Submit a pull request
Please ensure your code passes the linting checks (bun run qc).
Apache-2.0
Built with ❤️ for the SurrealDB community