Skip to content

Query Builder

surql provides a composable, type-safe query builder for SurrealDB.

Basic Query

import { SurQLClient } from 'jsr:@oneiriq/surql'
import { RecordId } from 'surrealdb'

interface User {
  id: RecordId
  name: string
  email: string
  age: number
}

const client = new SurQLClient(config)

// Select all
const users = await client.query<User>('users').execute()

// Get first
const first = await client.query<User>('users').first()

Filtering

Object-style WHERE

const active = await client.query<User>('users')
  .where({ active: true, role: 'admin' })
  .execute()

Fluent WHERE with operators

import { Op } from 'jsr:@oneiriq/surql'

const adults = await client.query<User>('users')
  .where('age', Op.GREATER_THAN, 18)
  .where('name', Op.LIKE, '%alice%')
  .execute()

Convenience methods

const results = await client.query<User>('users')
  .whereEquals('status', 'active')
  .whereContains('tags', 'premium')
  .whereLike('email', '%@company.com')
  .execute()

Sorting

import { SortDirection } from 'jsr:@oneiriq/surql'

const sorted = await client.query<User>('users')
  .orderBy('created_at', SortDirection.DESC)
  .orderBy('name', SortDirection.ASC)
  .execute()

Pagination

// Limit and offset
const page = await client.query<User>('users')
  .limit(25)
  .offset(50)
  .execute()

// Page-based
const page2 = await client.query<User>('users')
  .page(2, 25)  // page 2, 25 per page
  .execute()

Field Selection

const partial = await client.query<User>('users')
  .select('name', 'email')
  .execute()

Aggregations

const stats = await client.query('orders')
  .groupBy('status')
  .count()
  .sum('total')
  .avg('total')
  .min('total')
  .max('total')
  .execute()

HAVING

const highValue = await client.query('orders')
  .groupBy('customer_id')
  .sum('total')
  .count()
  .having('SUM(total)', Op.GREATER_THAN, 1000)
  .having('COUNT(*)', Op.GREATER_THAN, 5)
  .execute()

Type Mapping

Transform raw SurrealDB types to your domain types:

interface UserRaw {
  id: RecordId
  name: string
  created_at: Date
}

interface UserDto {
  id: string
  name: string
  createdAt: string
}

const mapUser = (raw: UserRaw): UserDto => ({
  id: raw.id.toString(),
  name: raw.name,
  createdAt: raw.created_at.toISOString(),
})

const users = await client.query<UserRaw, UserDto>('users')
  .map(mapUser)
  .execute()

CRUD Operations

Create

const user = await client.create<User>('users', {
  name: 'Alice',
  email: 'alice@example.com',
  age: 30,
}).execute()

Create with custom ID

const user = await client.create<User>('users', {
  name: 'Bob',
  email: 'bob@example.com',
  age: 25,
}).withId('users:bob').execute()

Update (replace)

await client.update<User>('users', new RecordId('users', 'alice'), {
  name: 'Alice Updated',
  email: 'alice@example.com',
  age: 31,
}).execute()

Merge (partial update)

await client.merge<User>('users', new RecordId('users', 'alice'), {
  age: 32,
}).execute()

Upsert

await client.upsert<User>('users', {
  name: 'Carol',
  email: 'carol@example.com',
  age: 28,
}).execute()

JSON Patch (RFC 6902)

await client.patch<User>('users', new RecordId('users', 'alice'), [
  { op: 'replace', path: '/age', value: 33 },
  { op: 'add', path: '/verified', value: true },
]).execute()

Delete

await client.remove<User>('users', new RecordId('users', 'alice')).execute()

Raw Queries

Access the underlying connection for raw SurrealQL:

const db = await client.getConnection()
const result = await db.query('SELECT * FROM users WHERE age > $age', { age: 18 })

Error Handling

try {
  const users = await client.query<User>('users').execute()
} catch (error) {
  if (error instanceof SurQLError) {
    console.error('Query error:', error.message)
  }
}

Next Steps