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
| Method | Description | Returns |
|---|---|---|
findOne | Find first matching record | T | null |
findMany | Find all matching records | T[] |
findUnique | Find by unique field | T | null |
create | Create a new record | T |
upsert | Create or update a record | T | null | T[] |
updateMany | Update matching records | T[] |
updateUnique | Update by unique field | T | null | boolean |
deleteMany | Delete matching records | number |
deleteUnique | Delete by unique field | boolean | T | null |
count | Count matching records | number |
exists | Check if any match | boolean |
$transaction | Atomic batch execution | Typed 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 | nullWith select — only selected fields
const user = await client.db.User.findOne({
where: { id: '123' },
select: { id: true, name: true },
});
// user: { id: CerialId; name: string } | nullWith include — model plus relations
const user = await client.db.User.findOne({
where: { id: '123' },
include: { profile: true },
});
// user: (User & { profile: Profile }) | nullCombined select + include
const user = await client.db.User.findOne({
where: { id: '123' },
select: { id: true },
include: { posts: true },
});
// user: ({ id: CerialId } & { posts: Post[] }) | nullWhen 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 inputWhen 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 } }); // RecordIdIf 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:
| Method | Returns | Description |
|---|---|---|
getMetadata() | ModelMetadata | Full model metadata (name, table, fields, relations) |
getName() | string | Model name (e.g., 'User') |
getTableName() | string | SurrealDB 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.