Cerial

CerialId

The CerialId wrapper class for SurrealDB record IDs, the RecordIdInput union type, and the transformation flow between your code and the database.

SurrealDB uses a table:id format for record IDs (e.g., user:abc123). Rather than exposing raw strings, Cerial wraps all record IDs in CerialId objects on output and accepts a flexible RecordIdInput union on input.

Both types are generic. CerialId<T> preserves the ID's value type, so .id returns T instead of always returning string. When a model uses typed IDs (e.g., Record(int) @id), the generated types carry that type through automatically.

Properties and Methods

Property/MethodTypeDescription
.idTThe raw ID value (type depends on the record's ID type)
.tablestringThe table name (e.g., 'user')
.toString()stringFull record ID string, properly escaped (e.g., 'user:123')
.toRecordId()RecordIdConvert to a native SurrealDB RecordId instance
.equals(other)booleanDeep comparison with any value

The default type parameter is RecordIdValue, which covers all possible SurrealDB ID types: string, number, object, and more. When a model declares a specific ID type, T narrows accordingly.

Output Types Return CerialId

Every query that returns data produces CerialId objects for ID fields and record reference fields:

// String IDs (default Record @id)
const user = await db.User.findOne({ where: { id: '123' } });
console.log(user.id);            // CerialId<string>
console.log(user.id.id);         // '123' (string)
console.log(user.id.table);      // 'user'
console.log(user.id.toString()); // 'user:123'

// Integer IDs (Record(int) @id)
const product = await db.Product.findOne({ where: { id: 42 } });
console.log(product.id);            // CerialId<number>
console.log(product.id.id);         // 42 (number)
console.log(product.id.toString()); // 'product:42'

Record reference fields (defined as Record in your schema) also return CerialId. When the referenced model has a typed ID, the FK field carries that type:

// If Product has Record(int) @id, then Order.productId is CerialId<number>
const order = await db.Order.findOne({ where: { id: 'order1' } });
console.log(order.productId);            // CerialId<number>
console.log(order.productId.id);         // 42 (number)
console.log(order.productId.table);      // 'product'

RecordIdInput

Input types accept any of the following via the RecordIdInput<T> union:

type RecordIdInput<T extends RecordIdValue = RecordIdValue> =
  T | CerialId<T> | RecordId | StringRecordId;

When a model has a typed ID, T narrows the accepted values:

// Record(int) @id model: T = number
type ProductIdInput = number | CerialId<number> | RecordId | StringRecordId;

// Record(string, int) @id model: T = string | number
type FlexIdInput = string | number | CerialId<string | number> | RecordId | StringRecordId;

This gives you flexibility in how you pass IDs to queries.

Plain Value

Pass the ID value directly. Cerial resolves the table name from the schema.

// String IDs
await db.User.findOne({ where: { id: '123' } });

// Integer IDs
await db.Product.findOne({ where: { id: 42 } });

CerialId from a Previous Query

Pass a CerialId returned from a previous query directly:

const user = await db.User.findOne({ where: { id: '123' } });

// Use the CerialId directly in another query
await db.Post.findMany({ where: { authorId: user.id } });

CerialId in Relation Operations

const user = await db.User.findOne({ where: { id: '123' } });

await db.Post.create({
  data: {
    title: 'Hello World',
    authorId: user.id, // CerialId accepted here
  },
});

Native SurrealDB RecordId

If you're working with the SurrealDB SDK directly:

import { RecordId } from 'surrealdb';

await db.User.findOne({
  where: { id: new RecordId('user', '123') },
});

Comparing CerialIds

Always use the .equals() method to compare CerialId values. Do not use == or ===.

// Correct: compares by value (deep comparison)
user1.id.equals(user2.id); // true or false

// Wrong: compares object references, not values
user1.id === user2.id; // false (different object instances!)
user1.id == user2.id;  // false (same problem)

CerialId instances are objects. Two instances pointing to the same record are different object references, so === always returns false. Use .equals() for value comparison.

The .equals() method accepts unknown and performs a deep comparison of both the table and id properties. Two CerialId instances pointing to the same record are always equal, regardless of how they were created:

const a = await db.User.findOne({ where: { id: '123' } });
const b = await db.User.findOne({ where: { id: '123' } });

a.id.equals(b.id); // true

You can also compare against non-CerialId values. The method returns false for anything that isn't a matching CerialId:

user.id.equals('not a cerial id'); // false
user.id.equals(null);              // false
user.id.equals(42);                // false

Different ID Types

CerialId<T> preserves the ID's type through the generic parameter:

// CerialId<string> — default, from Record @id
const userId: CerialId<string> = user.id;
userId.id; // string

// CerialId<number> — from Record(int) @id
const productId: CerialId<number> = product.id;
productId.id; // number

// CerialId<string | number> — from Record(string, int) @id
const flexId: CerialId<string | number> = flexModel.id;
flexId.id; // string | number

Converting from RecordId

CerialId.fromRecordId() preserves the typed ID value from a native SurrealDB RecordId:

import { RecordId } from 'surrealdb';

const nativeId = new RecordId('product', 42);
const cerialId = CerialId.fromRecordId(nativeId);

cerialId.id;    // 42 (number preserved, not stringified)
cerialId.table; // 'product'

Transformation Flow

Cerial handles the conversion between RecordIdInput and CerialId automatically at the query layer. You don't need to think about this during normal usage, but it helps explain what happens under the hood.

Input (Your Code to SurrealDB)

When you pass a RecordIdInput value in a query, Cerial converts it into a native RecordId(table, id) that SurrealDB understands.

RecordIdInput<T>  →  transform  →  RecordId(table, id)  →  SurrealDB
  • Raw value (T) — wrapped as RecordId(tableName, value)
  • CerialId — extracted via .toRecordId()
  • RecordId — passed through directly
  • StringRecordId — parsed and converted

Output (SurrealDB to Your Code)

When SurrealDB returns query results, Cerial converts native RecordId instances into CerialId<T> objects, preserving the typed ID value.

SurrealDB  →  RecordId  →  transform  →  CerialId<T>  →  Your Code

This transformation is applied recursively to all fields in the result, so nested record references (e.g., inside included relations) are also converted.

On this page