Cerial
Queries

Overview

Prisma-like query API with full TypeScript type inference, parameterized queries, dynamic return types, and lazy execution.

Cerial provides a Prisma-like query API with full TypeScript type inference. All queries are parameterized — values are bound via $varName, never inlined into query strings — ensuring injection safety by default.

You access the query API through the model proxy on your client instance:

const client = new CerialClient(/* ... */);
const user = await client.db.User.findOne({ where: { id: '123' } });

Methods Overview

MethodDescriptionReturns
findOneFind first matching recordT | null
findManyFind all matching recordsT[]
findUniqueFind by unique fieldT | null
createCreate a new recordT
upsertCreate or update a recordT | null | T[]
updateManyUpdate matching recordsT[]
updateUniqueUpdate by unique fieldT | null | boolean
deleteManyDelete matching recordsnumber
deleteUniqueDelete by unique fieldboolean | T | null
countCount matching recordsnumber
existsCheck if any matchboolean
$transactionAtomic batch executionTyped tuple

Dynamic Return Types

The actual TypeScript return type of read queries depends on the select and include options you pass. Cerial uses conditional types to infer the narrowest possible return type at compile time.

No select/include — full model type

const user = await client.db.User.findOne({
  where: { id: '123' },
});
// user: User | null

With select — only selected fields

const user = await client.db.User.findOne({
  where: { id: '123' },
  select: { id: true, name: true },
});
// user: { id: CerialId; name: string } | null

With include — model plus relations

const user = await client.db.User.findOne({
  where: { id: '123' },
  include: { profile: true },
});
// user: (User & { profile: Profile }) | null

Combined select + include

const user = await client.db.User.findOne({
  where: { id: '123' },
  select: { id: true },
  include: { posts: true },
});
// user: ({ id: CerialId } & { posts: Post[] }) | null

When both select and include are provided, the base fields are narrowed by select while the included relations are merged in via intersection.

CerialQueryPromise

All query methods return a CerialQueryPromise — a lazy thenable that only executes when you await it. This enables two powerful patterns:

Lazy execution — queries are not sent to the database until awaited:

// Query is built but NOT executed
const query = client.db.User.findMany({ where: { isActive: true } });

// Query executes here
const users = await query;

Transaction collection — unawaited queries can be collected into a $transaction for atomic batch execution:

const [users, posts] = await client.$transaction([
  client.db.User.findMany({ where: { isActive: true } }),
  client.db.Post.findMany({ where: { published: true } }),
]);

CerialQueryPromise is a thenable (has a .then() method), not an instanceof Promise. If you need to use it where a strict Promise is expected, wrap it: Promise.resolve(query).

Parameterized Queries

All values you pass in where, data, and other options are bound as parameters in the generated SurrealQL — they are never interpolated into query strings. This prevents injection attacks and ensures correct handling of all data types.

// Values are bound as $email parameter, never inlined
const user = await client.db.User.findOne({
  where: { email: userInput },
});

You never need to think about escaping or sanitization — Cerial handles it automatically.

CerialId

All id fields returned from queries are CerialId objects, not plain strings. CerialId provides structured access to the table name and record identifier:

const user = await client.db.User.findOne({
  where: { email: 'john@example.com' },
});

user.id;             // CerialId { table: 'user', id: '123' }
user.id.id;          // '123' (the raw identifier)
user.id.table;       // 'user' (the table name)
user.id.toString();  // 'user:123'
user.id.equals(other); // deep comparison with any input

When passing IDs as input, you can use any of the accepted RecordIdInput types:

// All of these work as input
await client.db.User.findUnique({ where: { id: '123' } });
await client.db.User.findUnique({ where: { id: user.id } }); // CerialId
await client.db.User.findUnique({ where: { id: someRecordId } }); // RecordId

If a model uses typed IDs (e.g., Record(int) @id), the .id property returns the typed value — number for int IDs, string for string IDs, etc. The RecordIdInput generic narrows the accepted input types accordingly.

Introspection Methods

Every model exposes metadata methods for runtime inspection:

MethodReturnsDescription
getMetadata()ModelMetadataFull model metadata (name, table, fields, relations)
getName()stringModel name (e.g., 'User')
getTableName()stringSurrealDB table name (e.g., 'user')
const metadata = client.db.User.getMetadata();
metadata.name;       // 'User'
metadata.tableName;  // 'user'
metadata.fields;     // Array of field definitions

const name = client.db.User.getName();       // 'User'
const table = client.db.User.getTableName(); // 'user'

Introspection methods are available on every model proxy, not just query results. Use them for dynamic logic like building admin panels or generating forms from schema metadata.

On this page