Cerial
CLI

Generated Output

Structure and contents of every file produced by the generate command.

After running bunx cerial generate, the output directory contains a complete TypeScript client. This page documents the structure and contents of every generated file.

Directory Structure

Models, objects, tuples, enums, and literals are each generated into their own directory so you can tell at a glance which is which:

db-client/
├── client.ts             # CerialClient class with connect/disconnect/migrate
├── models/
│   ├── user.ts           # User interface + all derived types
│   ├── profile.ts        # Profile interface + all derived types
│   ├── post.ts           # Post interface + all derived types
│   ├── tag.ts            # Tag interface + all derived types
│   └── index.ts          # Re-exports all model types
├── objects/
│   ├── address.ts        # Address object interface + types
│   └── index.ts          # Re-exports all object types
├── tuples/
│   ├── coordinate.ts     # Coordinate tuple literal + types
│   └── index.ts          # Re-exports all tuple types
├── internal/
│   ├── model-registry.ts # Runtime metadata (fields, relations, decorators)
│   ├── migrations.ts     # DEFINE TABLE/FIELD/INDEX statements
│   └── index.ts          # Internal exports
└── index.ts              # Main entry: CerialClient + all types

client.ts

The main client class that manages connections and provides typed model access.

import { CerialClient } from './db-client';

const client = new CerialClient();

// Connect to SurrealDB
await client.connect({
  url: 'http://localhost:8000',
  namespace: 'main',
  database: 'main',
  auth: { username: 'root', password: 'root' },
});

// Access models through the typed db proxy
const users = await client.db.User.findMany();
const post = await client.db.Post.create({
  data: { title: 'Hello', authorId: users[0].id },
});

// Run migrations explicitly (optional, they run lazily by default)
await client.migrate();

// Disconnect when done
await client.disconnect();

The db property is a JavaScript Proxy that intercepts property access and routes it to the appropriate query builder. client.db.User returns a model accessor with methods like findOne, findMany, create, updateUnique, deleteUnique, and more. See Connection for full client lifecycle details.

models/*.ts

One file is generated per model defined in your schema. Each file contains the full set of types needed for type-safe queries.

Types Per Model

TypeDescription
UserBase interface. The shape of a User record returned from queries.
UserInputInput interface. Used internally for raw data handling.
UserCreateFields accepted by create(). Required fields, optional fields, relation connects.
UserNestedCreateFields for creating records in nested operations.
UserUpdateFields accepted by update(). All optional, with set/increment/disconnect operators.
UserWhereFilter conditions. Field-level operators (eq, gt, contains, etc.).
UserFindUniqueWhereUnique lookup, by id or any @unique field.
UserSelectField selection. Boolean per field, object sub-selects for embedded objects.
UserIncludeRelation inclusion. Which relations to load with optional nested select/where/include.
UserOrderBySort order. 'asc' or 'desc' per field.
User$RelationsRelation metadata type. Lists all relation fields and their types.
GetUserPayload<S, I>Return type resolver. Infers the exact return shape from Select and Include options.

Example

Given this schema:

model User {
  id Record @id
  name String
  email String @unique
  bio String?
  posts Relation[] @model(Post)
  postIds Record[]
}

The generated base interface:

export interface User {
  id: CerialId;
  name: string;
  email: string;
  bio?: string | null;
  posts?: Post[];
  postIds: CerialId[];
}

The generated create type:

export interface UserCreate {
  id?: RecordIdInput;
  name: string;
  email: string;
  bio?: string | null;
  posts?: {
    connect?: RecordIdInput | RecordIdInput[];
  };
}

The generated where type:

export interface UserWhere {
  AND?: UserWhere[];
  OR?: UserWhere[];
  NOT?: UserWhere;
  id?: RecordIdInput | { eq?: RecordIdInput; not?: RecordIdInput; in?: RecordIdInput[] };
  name?: string | { eq?: string; not?: string; contains?: string; startsWith?: string /* ... */ };
  email?: string | { eq?: string; not?: string; contains?: string /* ... */ };
  bio?: string | null | { eq?: string | null; not?: string | null; isNone?: boolean /* ... */ };
}

See Generated Types for the complete reference of all generated types.

objects/*.ts

One file is generated per object defined in your schema. Objects produce a smaller set of types since they're embedded inline within models, not standalone tables.

Types Per Object

TypeDescription
AddressBase interface. The shape of the embedded object.
AddressInputInput interface for creating/updating the object.
AddressWhereFilter conditions for nested object fields.
AddressSelectSub-field selection for the object.
AddressOrderBySort order for object fields.

Objects don't generate GetPayload, Include, Create, Update, NestedCreate, FindUniqueWhere, or $Relations types. They're embedded data, not queryable tables.

Example

Given this schema:

object Address {
  street String
  city String
  state String
  zipCode String?
}

The generated interface:

export interface Address {
  street: string;
  city: string;
  state: string;
  zipCode?: string | null;
}

The generated select type:

export interface AddressSelect {
  street?: boolean;
  city?: boolean;
  state?: boolean;
  zipCode?: boolean;
}

tuples/*.ts

One file is generated per tuple defined in your schema.

Types Per Tuple

TypeDescription
CoordinateOutput type. TypeScript tuple literal.
CoordinateInputInput type. Accepts array or named object form.
CoordinateWhereFilter conditions for tuple fields.
CoordinateUpdatePer-element update type (object form for selective element update).
CoordinateSelectSub-field selection (only generated if the tuple contains object elements).
CoordinateUnsetPer-element unset (only generated if the tuple has optional elements).

Tuples don't generate Create, Include, OrderBy, GetPayload, or $Relations types. Like objects, tuples are embedded inline and not standalone tables.

CoordinateSelect is only generated for tuples that contain object elements at any nesting depth. Primitive-only tuples use boolean select instead.

internal/model-registry.ts

The model registry provides runtime metadata used by the query builder to construct correct SurrealQL queries. It contains information about every model, including:

  • Field metadata: name, type, optionality, whether it's an array
  • Relation metadata: target model, target table name, direction (forward/reverse), the Record field it references
  • Decorator metadata: @id, @unique, @now, @createdAt, @updatedAt, @default() values, @onDelete() behavior

The query builder reads this registry to:

  • Know which fields are relations vs. scalar fields
  • Determine how to join related records
  • Apply default values at the correct time
  • Cascade deletes through relation chains
// The registry is used internally, you don't interact with it directly.
// But understanding it helps when debugging query behavior.
import { modelRegistry } from './db-client/internal';

const userMeta = modelRegistry.get('User');
// { fields: [...], relations: [...], decorators: [...] }

internal/migrations.ts

Contains the SurrealQL migration statements generated from your schema. These are executed when you call client.migrate() or automatically before the first query.

Example generated migrations:

-- Table definitions
DEFINE TABLE user SCHEMAFULL;
DEFINE TABLE post SCHEMAFULL;

-- User fields
DEFINE FIELD name ON TABLE user TYPE string;
DEFINE FIELD email ON TABLE user TYPE string ASSERT string::is::email($value);
DEFINE FIELD bio ON TABLE user TYPE option<string | null>;
DEFINE FIELD isActive ON TABLE user TYPE bool DEFAULT true;
DEFINE FIELD createdAt ON TABLE user TYPE datetime DEFAULT time::now();
DEFINE FIELD updatedAt ON TABLE user TYPE datetime DEFAULT ALWAYS time::now();
DEFINE FIELD postIds ON TABLE user TYPE option<array<record<post>>>;
DEFINE FIELD postIds[*] ON TABLE user TYPE record<post>;

-- User indexes
DEFINE INDEX user_email_unique ON TABLE user FIELDS email UNIQUE;

-- Post fields
DEFINE FIELD title ON TABLE post TYPE string;
DEFINE FIELD content ON TABLE post TYPE option<string | null>;
DEFINE FIELD authorId ON TABLE post TYPE record<user>;

Key details about generated migrations:

  • id fields are not included. SurrealDB auto-manages the id field.
  • Optional fields use option<T | null> to accept both NONE and null values.
  • Array fields define both the array type and the element type (field + field[*]).
  • Record fields use record<tablename> to enforce referential integrity.
  • @unique fields generate a DEFINE INDEX ... UNIQUE statement.
  • @default() values are included in the DEFINE FIELD statement.
  • @createdAt fields use DEFAULT time::now().
  • @updatedAt fields use DEFAULT ALWAYS time::now().
  • @now fields use COMPUTED time::now() (computed at query time, not stored).

index.ts

The main entry point that re-exports everything you need:

// Everything is available from the top-level import
import {
  CerialClient,
  // Model types
  User,
  UserCreate,
  UserUpdate,
  UserWhere,
  UserSelect,
  UserInclude,
  // Object types
  Address,
  AddressWhere,
  AddressSelect,
  // Utility types
  CerialId,
  RecordIdInput,
} from './db-client';

This is typically the only import you need in your application code.

Regenerating

The generated output is meant to be regenerated whenever your schema changes. You should:

  1. Add the output directory to .gitignore if you generate at build time, or commit it if you want the types available without a build step.
  2. Run generation in CI to ensure the client is always in sync with the schema.
  3. Use watch mode during development for instant regeneration.
# Regenerate after schema changes
bunx cerial generate

# Or use watch mode during development
bunx cerial generate --watch

Automatic Cleanup

When you regenerate, stale files from previous generations are automatically removed. If you rename a model from User to Account, the old models/user.ts is deleted and models/account.ts is created. Empty directories left behind are also cleaned up.

For a guaranteed clean slate (e.g., after major schema restructuring), use the --clean flag to wipe the entire output directory before generating:

bunx cerial generate --clean

Do not manually edit generated files. Your changes will be lost on the next regeneration.

On this page