An open API service indexing awesome lists of open source software.

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

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**