https://github.com/surrealdb/surqlize
A type-safe TypeScript ORM for SurrealDB with full type inference, a fluent query builder, and native support for graph relationships
https://github.com/surrealdb/surqlize
orm orm-library surreal surrealdb surrealql typescript
Last synced: 4 months ago
JSON representation
A type-safe TypeScript ORM for SurrealDB with full type inference, a fluent query builder, and native support for graph relationships
- Host: GitHub
- URL: https://github.com/surrealdb/surqlize
- Owner: surrealdb
- License: apache-2.0
- Created: 2025-01-19T15:03:50.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2026-02-13T13:04:11.000Z (5 months ago)
- Last Synced: 2026-02-13T21:43:57.235Z (5 months ago)
- Topics: orm, orm-library, surreal, surrealdb, surrealql, typescript
- Language: TypeScript
- Homepage: https://surrealdb.com
- Size: 96.7 KB
- Stars: 18
- Watchers: 11
- Forks: 3
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
A type-safe TypeScript ORM for SurrealDB.
# Surqlize
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.
## Features
- **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
## Installation
```bash
bun add surqlize
# or
npm install surqlize
```
## Quick start
```typescript
import { 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;
// Result: Array<{ name: string; email: string }>
```
## Schema definition
### Tables
Define tables using the `table()` function with a rich type system:
```typescript
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`.
### Edges and graph relations
Define graph edges to model relationships between tables:
```typescript
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 edge
- `in`: RecordId of the source table (user)
- `out`: RecordId of the target table (post)
## CRUD Operations
### SELECT statements
```typescript
// 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,
})),
}));
```
#### Sorting with ORDER BY
```typescript
// 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");
```
#### Grouping with GROUP BY
```typescript
// Group by field(s)
const grouped = db.select("post")
.groupBy("author");
// Group all (for table-wide aggregates)
const totalCount = db.select("user")
.groupAll();
```
#### Loading relations with FETCH
```typescript
// Fetch linked records
const withAuthor = db.select("post")
.fetch("author");
// Fetch multiple relations
const deepFetch = db.select("post")
.fetch("author", "comments");
```
#### Splitting arrays with SPLIT
```typescript
// Split array field into multiple records
const splitTags = db.select("post")
.split("tags");
// Split multiple arrays
const multiSplit = db.select("post")
.split("tags", "categories");
```
#### Setting query timeout
```typescript
// Set timeout duration
const withTimeout = db.select("user")
.where(user => user.age.gt(18))
.timeout("5s");
```
#### Combining clauses
```typescript
// 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 statements
Create a new record with a specific id or a generated id.
```typescript
// 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 statements
Insert one or multiple records with support for bulk operations and conflict handling.
```typescript
// 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 }));
```
### UPSERT statements
Create a record if it doesn't exist, update records if matching records exist.
```typescript
// 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 statements
Update a record or multiple records in a table.
```typescript
// 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");
```
### RELATE statements
Create graph edges between records using defined edge schemas.
```typescript
// 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 statements
```typescript
// 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 }));
```
## Batch
Execute multiple queries as a single atomic operation in one round-trip. No intermediate results are available — all queries succeed or all fail together.
```typescript
// 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 tuple
```
You can also inspect the generated SurrealQL before executing:
```typescript
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;
```
## Transactions
Open a server-side transaction, execute queries one-by-one with intermediate results, and decide whether to commit or cancel based on the outcomes.
### Callback form (auto-commit/cancel)
The callback form automatically commits on success and cancels on error:
```typescript
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 automatically
```
### Manual form (explicit commit/cancel)
For full control, use the manual form:
```typescript
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`.
## Accessing Single Records
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)`:
```typescript
// .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();
```
## Filtering operations
All types support these comparison operators:
```typescript
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)
);
```
### Compound conditions
For complex conditions, use the standalone `and()` and `or()` combiners. These make precedence explicit and produce correctly parenthesized SurrealQL:
```typescript
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.
## Type-specific functions
### String functions
```typescript
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.
### Array functions
```typescript
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`).
### Number functions
```typescript
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`.
### Date functions
```typescript
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`).
### Option functions
When working with optional values (created with `t.option()`), you can use `map()` to transform the value if it exists:
```typescript
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()),
}));
```
### Record functions
When you have a record reference, you can perform nested queries:
```typescript
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;
// Result: Array<{
// title: string;
// author: { name: string; email: string } | undefined;
// }>
```
## Standalone functions
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).
```typescript
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`.
## Advanced Features
### Return Clauses
Control what gets returned from mutations:
```typescript
// 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 }));
```
### Query Timeouts
```typescript
const users = await db.select("user")
.where((u) => u.age.gt(18))
.timeout("5s");
await db.update("user", "alice")
.set({ age: 31 })
.timeout("10s");
```
### Operators
Use operators for atomic operations:
```typescript
// 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"] },
});
```
## Type inference
Extract TypeScript types from your queries using `t.infer<>`:
```typescript
// Infer query result type
const query = db.select("user").return((user) => ({
name: user.name,
age: user.age,
}));
type QueryResult = t.infer;
// QueryResult: Array<{ name: string; age: number }>
// Infer table type
const userTable = table("user", {
name: t.string(),
age: t.number(),
});
type User = t.infer;
// User: { id: RecordId<"user">; name: string; age: number }
// Infer individual type definitions
const emailType = t.string();
type Email = t.infer;
// Email: string
```
## Debugging Queries
Inspect generated SurrealQL:
```typescript
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 values
```
## Graph relationships
Surqlize provides type-safe graph traversal through the `lookup` system:
```typescript
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)
```
## Complex example
Here's a complete example showcasing multiple features:
```typescript
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;
// 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 RecordId
```
## Multi-session support
Surqlize 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.
### Multiple databases over one connection
```typescript
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 });
```
### Forking sessions
Use `forkSession()` to clone an existing session (inheriting its namespace, database, auth, and variables) and then diverge:
```typescript
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();
```
### Disposable sessions
Since `SurrealSession` implements `Symbol.asyncDispose`, sessions work with `await using` for automatic cleanup:
```typescript
{
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
}
```
## Comparison with other ORMs
| Feature | Surqlize | SurrealDB.js | Prisma | Drizzle | TypeORM |
|---------|----------|--------------|--------|---------|---------|
| SurrealDB support | ✅ | ✅ | ❌ | ❌ | ❌ |
| Schema definition | ✅ Code-first | ❌ | ✅ Schema file | ✅ Code-first | ⚠️ Decorators |
| Type inference | ✅ Full | ⚠️ Partial | ✅ With codegen | ✅ Full | ⚠️ Decorators |
| CRUD operations | ✅ All operations | ✅ | ✅ | ✅ | ✅ |
| Graph and edges | ✅ Native | ✅ Manual | ❌ | ❌ | ❌ |
| Query builder | ✅ Type-safe | ⚠️ Manual | ⚠️ Limited | ✅ Type-safe | ✅ Query builder |
| Database Functions | ✅ Integrated | ⚠️ Manual | ⚠️ Limited | ✅ SQL functions | ✅ Query functions |
| Nested Queries | ✅ Type-safe | ⚠️ Manual | ✅ 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
## Roadmap
This project is in active development. Planned features include:
- [x] **SurrealDB functions** - All 25 built-in function families (string, array, math, time, crypto, rand, and more)
- [x] **Advanced query clauses** - ORDER BY, GROUP BY, FETCH, SPLIT
- [x] **Transaction support** - Batch and interactive transactions
- [x] **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
## Development
```bash
# 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 changes
```
## Contributing
Contributions are welcome! This project is in an experimental stage, so expect breaking changes. If you'd like to contribute:
1. Open an issue to discuss your idea
2. Fork the repository
3. Create a feature branch
4. Submit a pull request
Please ensure your code passes the linting checks (`bun run qc`).
## License
Apache-2.0
---
**Built with ❤️ for the SurrealDB community**