# Unreal ORM - Full Documentation > This file aggregates all documentation for Unreal ORM. ## The UnrealORM CLI is `@unreal-orm/cli`, usually run through `bunx`, `npx`, or `pnpm dlx` as `unreal`. Source: advanced/cli-usage.mdx The UnrealORM CLI is `@unreal-orm/cli`, usually run through `bunx`, `npx`, or `pnpm dlx` as `unreal`. ## Install ```bash bun add -D @unreal-orm/cli ``` ## Commands | Command | Purpose | |---|---| | `unreal init` | Scaffold `unreal.config.json`, `unreal/surreal.ts`, and sample models. | | `unreal push` | Apply TypeScript models to the database. | | `unreal pull` | Introspect the database and generate TypeScript models. | | `unreal diff` | Compare local models with the database schema. | | `unreal mermaid` | Generate a Mermaid ERD from code, the database, or a `.surql` file. | | `unreal view` | Browse records interactively. | | `unreal docs` | Open the docs or generate context files. | | `unreal github` | Open the repository on GitHub. | ## Connection resolution Every database command resolves the connection in this order: 1. **CLI flags** — `--url`, `-u`/`--username`, `-p`/`--password`, `-n`/`--namespace`, `-d`/`--database`, `--auth-level`, `--embedded` 2. **`unreal/surreal.ts`** — auto-loaded from the configured `unreal/` folder 3. **Interactive prompt** — if no flags and no `surreal.ts` are found ## Fully automated push ```bash bunx unreal push \ --url ws://localhost:8000 \ -u root -p secret \ -n production -d myapp \ --auth-level root \ -y ``` ## Connection from a file ```bash # Use the connection defined in unreal/surreal.ts bunx unreal push ``` ## Diff options ```bash # Show detailed field-level changes bunx unreal diff --detailed # Use embedded in-memory SurrealDB bunx unreal diff --embedded mem:// ``` ## Embedded mode For quick tests without a server: ```bash bunx unreal push --embedded mem:// -y ``` Use `--embedded` with a file path for disk persistence: ```bash bunx unreal push --embedded file:/tmp/mydb -y ``` ## Project layout expected by the CLI ``` my-project/ ├── unreal.config.json ├── unreal/ │ ├── surreal.ts │ └── tables/ └── package.json ``` `unreal.config.json` only contains the path to the `unreal/` folder: ```json { "$schema": "./node_modules/unreal-orm/schema.json", "path": "./unreal" } ``` For per-command details, see the generated [CLI Reference](../cli/). --- ## SurrealDB can store file references through `DEFINE BUCKET` and `DEFINE FIELD ... TYPE file`. This is an experimental feature in SurrealDB — start the server with `--allow-experimental files`. Source: advanced/file-storage.mdx SurrealDB can store file references through `DEFINE BUCKET` and `DEFINE FIELD ... TYPE file`. This is an experimental feature in SurrealDB — start the server with `--allow-experimental files`. ## Define a bucket ```ts import { Table, Field, Bucket, applySchema } from 'unreal-orm'; import { surql } from 'surrealdb'; const AvatarsBucket = Bucket.define({ name: 'avatars', backend: 'memory', }); class User extends Table.normal({ name: 'user', schemafull: true, fields: { name: Field.string(), avatar: Field.file(), }, }) {} await applySchema(db, [User, AvatarsBucket]); ``` `backend` can be `memory`, `file:/path`, or omitted for the global bucket. ## Storing and reading files A `file` field accepts a file URL string in the form `f"bucket:key"`: ```ts const user = await User.create(db, { name: 'Alice', avatar: 'f"avatars:alice.png"', }); ``` When read from the database, `avatar` is a `FileRef` with `.bucket` and `.key` properties. ## Permissions on buckets ```ts const UploadsBucket = Bucket.define({ name: 'uploads', backend: 'file:/var/data/uploads', permissions: surql`WHERE $auth.role = 'admin' OR $action = 'get'`, }); ``` `$action` is the file operation (`get`, `put`, `head`, `delete`). --- ## SurrealDB supports full-text search through `DEFINE INDEX ... SEARCH`. UnrealORM exposes this via `Index.define` with `search: true`. Source: advanced/full-text-search.mdx SurrealDB supports full-text search through `DEFINE INDEX ... SEARCH`. UnrealORM exposes this via `Index.define` with `search: true`. ## Define a search index ```ts import { Table, Field, Index } from 'unreal-orm'; class Post extends Table.normal({ name: 'post', schemafull: true, fields: { title: Field.string(), content: Field.string(), }, }) {} const PostSearchIndex = Index.define(() => Post, { name: 'post_search_idx', fields: ['title', 'content'], search: true, analyzer: 'english', bm25: true, highlights: true, }); ``` ## Query the index Use SurrealQL's full-text operators in a `surql` template: ```ts const posts = await Post.select(db, { where: surql`title @@ 'surreal' OR content @@ 'surreal'`, }); ``` ## Limitations UnrealORM does not generate full-text analyzers. Define them with raw SurrealQL if you need custom tokenizers: ```ts await db.query(surql`DEFINE ANALYZER english TOKENIZERS blank,class FILTERS lowercase,snowball(english)`); ``` See the [SurrealDB full-text search documentation](https://surrealdb.com/docs/learn/data-models/full-text-search/overview) for more details on operators and analyzers. --- ## SurrealDB can store and query GeoJSON-like geometry. UnrealORM provides `Field.geometry(...)` with a typed `GeometryType`. Source: advanced/geospatial.mdx SurrealDB can store and query GeoJSON-like geometry. UnrealORM provides `Field.geometry(...)` with a typed `GeometryType`. ## Geometry types ```ts import { Table, Field } from 'unreal-orm'; class Place extends Table.normal({ name: 'place', schemafull: true, fields: { name: Field.string(), location: Field.geometry('point'), boundary: Field.geometry('polygon'), route: Field.geometry('linestring'), }, }) {} ``` Supported types: `point`, `linestring`, `polygon`, `multipoint`, `multilinestring`, `multipolygon`, `collection`, and `feature`. ## Inserting geometry Use `surrealdb` geometry helpers or raw values: ```ts await Place.create(db, { name: 'Office', location: { type: 'Point', coordinates: [-0.1276, 51.5074] }, }); ``` ## Spatial queries Use `surql` for distance, intersection, and containment checks: ```ts const nearby = await Place.select(db, { where: surql`geo::distance(location, { type: 'Point', coordinates: [-0.1276, 51.5074] }) < 10_000`, }); ``` The `where` builder also exposes `intersects` when you are comparing geometry columns: ```ts Place.select(db, { where: (f) => f.boundary.intersects(area), }); ``` For advanced spatial operations, write the expression directly in `surql`. --- ## UnrealORM exposes the `Unreal.ast` namespace for comparing your TypeScript models against the live database schema or another schema definition. Source: advanced/migrations.mdx UnrealORM exposes the `Unreal.ast` namespace for comparing your TypeScript models against the live database schema or another schema definition. ## Extract an AST from models ```ts import { Unreal } from 'unreal-orm'; import { User, Post } from './tables'; const localAst = Unreal.ast.extractSchema([User, Post]); ``` ## Parse SurrealQL definitions ```ts const tableAst = Unreal.ast.parseTable('DEFINE TABLE user SCHEMAFULL'); const fieldAst = Unreal.ast.parseField('DEFINE FIELD email ON user TYPE string ASSERT string::is::email($value)'); const indexAst = Unreal.ast.parseIndex('DEFINE INDEX user_email_idx ON user FIELDS email UNIQUE'); ``` ## Compare schemas ```ts const localAst = Unreal.ast.extractSchema([User, Post]); const remoteAst = await db.query<[SchemaAST]>(surql`INFO FOR DB`); // simplified const changes = Unreal.ast.compare(localAst, remoteAst); ``` `compare` returns a list of `SchemaChange` objects describing added, removed, and modified tables, fields, and indexes. ## Generate migration DDL ```ts const migration = Unreal.ast.generateMigration(changes); await db.query(surql`${migration}`); ``` ## Using the CLI ```bash # See a diff between local models and the database bunx unreal diff # Apply local schema to the database bunx unreal push # Pull an existing database schema into TypeScript bunx unreal pull ``` Common options: ```bash bunx unreal diff \ --url ws://localhost:8000 \ -u root -p root \ -n test -d test \ --auth-level root ``` Use `--detailed` for field-level changes. --- ## SurrealDB has a built-in permissions system. UnrealORM exposes it through `permissions` on tables and fields. Values are either `true` (always allowed), `false` (never allowed), or a `surql` expression that evaluates to a boolean. Source: advanced/permissions.mdx SurrealDB has a built-in permissions system. UnrealORM exposes it through `permissions` on tables and fields. Values are either `true` (always allowed), `false` (never allowed), or a `surql` expression that evaluates to a boolean. ## Row-level security ```ts import { Table, Field } from 'unreal-orm'; import { surql } from 'surrealdb'; class Post extends Table.normal({ name: 'post', schemafull: true, fields: { title: Field.string(), content: Field.string(), author: Field.record(() => User), }, permissions: { select: true, create: surql`$auth.id != NONE`, update: surql`author = $auth.id`, delete: surql`author = $auth.id`, }, }) {} ``` ## Field-level permissions Hide or protect individual fields: ```ts class User extends Table.normal({ name: 'user', fields: { name: Field.string(), email: Field.string({ permissions: { select: surql`id = $auth.id`, update: surql`id = $auth.id`, }, }), }, }) {} ``` ## The $auth variable `$auth` is the currently authenticated record. Its exact shape depends on how you sign in. With scope authentication it is the record from the scope's `user` table: ```ts await db.connect('ws://localhost:8000', { namespace: 'app', database: 'app', authentication: { access: 'user', variables: { email: 'alice@example.com', password: 'password', }, }, }); ``` > **Note**: Pass authentication details directly to `.connect()` so the SDK can automatically re-authenticate on reconnect. Using separate `.signin()` calls means tokens will not be refreshed automatically. After sign in, `$auth.id` is the `RecordId` of the authenticated user. ## Testing permissions Use the in-memory engine and set `$auth` through query bindings for fast unit tests: ```ts await expect( db.query(surql`DELETE $id`, { id: post.id, auth: { id: new RecordId('user', 'bob') } }) ).rejects.toThrow(); ``` For real integration tests, sign in as different users and run the ORM calls against a clean `mem://` database. --- ## UnrealORM intentionally does not wrap all of SurrealDB. When a feature is not supported, use the `surrealdb` SDK directly and pass the same `db` instance to the ORM. Source: advanced/sdk-interop.mdx UnrealORM intentionally does not wrap all of SurrealDB. When a feature is not supported, use the `surrealdb` SDK directly and pass the same `db` instance to the ORM. ## When to drop to raw SurrealQL | Feature | UnrealORM support | Recommended approach | |---|---|---| | Events (`DEFINE EVENT`) | Not supported | Use `db.query` with a `surql` template | | Triggers | Not supported | Use `db.query` with a `surql` template | | Custom functions (`DEFINE FUNCTION`) | Not supported | Use `db.query` with a `surql` template | | Users and scopes (`DEFINE USER`, `DEFINE ACCESS`) | Not supported | Use `db.query(...)` or `.connect()` with `authentication` | | Live queries (`LIVE SELECT`) | Not supported | Use `db.live(...)` from the SDK | | Vector indexes | Not supported | Use raw `DEFINE INDEX ... MTREE` or `HNSW` | | Time-series tables | Not supported | Use raw `DEFINE TABLE ... TYPE TIMESERIES` | | Complex graph queries | Partial | Use `surql` templates inside `typed()` or `where` | ## Example: live queries ```ts import { surql } from 'surrealdb'; const { unsubscribe } = await db.live(surql`LIVE SELECT * FROM post`, (action, result) => { console.log(action, result); }); ``` ## Example: custom functions ```ts await db.query(surql` DEFINE FUNCTION fn::score($base::number, $multiplier::number) { RETURN $base * $multiplier; } `); ``` Then use the function in a `surql` expression within an UnrealORM query: ```ts const posts = await Post.select(db, { select: { title: true, score: typed(surql`fn::score(views, 2)`), }, }); ``` ## Transactions Use the `surrealdb` SDK transaction API. UnrealORM methods accept any SurrealDB-like `db`, including a transaction object. ```ts await db.transaction(async (tx) => { const user = await User.create(tx, { name: 'Alice' }); await Post.create(tx, { title: 'Hello', author: user.id }); }); ``` ## Keep the ORM as the happy path For day-to-day table, field, index, and CRUD work, use UnrealORM. For experimental or unsupported features, write the raw SurrealQL once and keep the ORM for the rest of the app. --- ## SurrealDB can run fully in-memory, which makes UnrealORM models easy to test without Docker. Source: advanced/testing.mdx SurrealDB can run fully in-memory, which makes UnrealORM models easy to test without Docker. ## Test setup ```ts import { Surreal } from 'surrealdb'; import { createNodeEngines } from '@surrealdb/node'; import { applySchema } from 'unreal-orm'; import { User, Post } from './tables'; async function setupTestDb() { const db = new Surreal({ engines: { ...createNodeEngines() } }); await db.connect('mem://', { namespace: 'test', database: 'test', }); await applySchema(db, [User, Post]); return db; } ``` ## Example tests ```ts import { test, expect, beforeAll } from 'vitest'; import { RecordId } from 'surrealdb'; import { User } from './tables'; let db: Surreal; beforeAll(async () => { db = await setupTestDb(); }); test('creates a user', async () => { const user = await User.create(db, { name: 'Alice', email: 'alice@example.com' }); expect(user.name).toBe('Alice'); expect(user.id).toBeInstanceOf(RecordId); }); test('enforces email assertion', async () => { await expect( User.create(db, { name: 'Bad', email: 'not-an-email' }) ).rejects.toThrow(); }); ``` ## Isolation For parallel test runners, use a unique namespace per worker: ```ts await db.connect('mem://', { namespace: `test-${process.pid}`, database: 'test', }); ``` Or create a new `Surreal` instance per test and reconnect each time. ## Snapshotting the schema You can snapshot the schema DDL to catch accidental changes: ```ts import { Unreal } from 'unreal-orm'; const ddl = Unreal.generateFullSchemaQl([User, Post]); expect(ddl).toMatchSnapshot(); ``` --- ## # Added Source: changelog/0.4.4.mdx ## Added - Support for SurrealDB's `FLEXIBLE` option on object and custom fields via `{ flexible: true }`. - Integration tests for flexible object and custom fields. **Example usage:** ```ts import { Table, Field } from "unreal-orm"; class FlexibleModel extends Table.define({ name: "flexible_model", fields: { // This field allows storing any object shape, not just the declared one meta: Field.object({ foo: Field.string() }, { flexible: true }), // This custom field is also flexible data: Field.custom("object", { flexible: true }), // This field is NOT flexible, only allows { bar: string } regular: Field.object({ bar: Field.string() }), }, schemafull: true, }) {} ``` **Rendered SurrealQL for flexible fields:** ```sql DEFINE FIELD meta ON TABLE flexible_model FLEXIBLE TYPE object; DEFINE FIELD data ON TABLE flexible_model FLEXIBLE TYPE object; DEFINE FIELD regular ON TABLE flexible_model TYPE object; ``` ## Changed - Migrated all documentation to a new Starlight-powered docs site for improved navigation and onboarding. - Updated all links and references in READMEs and documentation to point to the new docs site. - Streamlined "Contributing" sections to unify guidance for contributors. - Schema generator now emits correct SurrealQL for flexible fields. - Extended TypeScript types to support the flexible option safely. - Refactored test suite for improved clarity and consistency. - README improvements: new shields, motivation, "Why UnrealORM?" section, quick links, and improved layout. - Updated package description for consistency with documentation. ## Removed - Legacy markdown documentation files, replaced by the new docs site. - Redundant and outdated documentation entry points. ## Fixed - Homepage "Get Started" link now points to the correct getting-started route. - Fixed `module` field in `package.json` to ensure compatibility with consumers. --- Other minor changes and improvements not individually listed. --- ## import { Aside } from '@astrojs/starlight/components'; Source: changelog/0.5.0.mdx import { Aside } from '@astrojs/starlight/components'; This release introduces powerful new features, significant architectural improvements, and a more streamlined developer experience. Key highlights include a new `merge` method for partial updates, support for a wide array of advanced data types, and a decoupled API for defining indexes. ## ✨ Features - **Partial Updates with `merge`**: Introduced `instance.merge(db, data)` and `Model.merge(db, data)` for performing partial record updates, aligning with SurrealDB's `MERGE` statement. This is now the recommended way to update specific fields without fetching and sending the entire record. - **Expanded Data Type Support**: Added comprehensive, type-safe support for most of SurrealDB's advanced data types, including: - `Field.any()` - `Field.decimal()` - `Field.float()` - `Field.int()` - `Field.bytes()` - `Field.duration()` - `Field.uuid()` - `Field.geometry()` (with type-safe definitions for specific geometry types) - **Flexible Fields**: Added a `flexible: true` option to `Field.object()` and `Field.custom()` to support SurrealDB's `FLEXIBLE` keyword, allowing for dynamic, schema-less fields within structured models. - **Decoupled Index Definitions**: Indexes are now defined separately from tables using a new `Index.define()` API. This improves separation of concerns and simplifies the table definition API. ## 🚀 Improvements & Refactors - **Unified Schema API**: The `applySchema` and `generateFullSchemaQl` functions now accept a single array of definable items (e.g., `[User, UserEmailIndex]`), simplifying the schema management process. - **Clearer Table Definition**: `Table.define` has been replaced with `Table.normal()` and `Table.relation()` to make the distinction between standard and edge tables explicit and type-safe. - **Instance-level `delete`**: Added an `instance.delete(db)` method for more intuitive record deletion. ## 🛠️ Fixes - **Schema Generation**: Fixed an issue where the schema generator would emit redundant `WHERE` clauses for raw permission strings. - **Query Engine**: Correctly implemented support for `vars` bindings in queries and enforced that `orderBy` clauses use a valid `order` direction. - **Field Definitions**: Prevented the schema generator from emitting duplicate `field[*]` definitions for array fields. - **Package Compatibility**: Set the `module` field in `package.json` to `dist/index.js` to ensure correct module resolution in various environments. ## 📚 Documentation - **JSDoc Coverage**: Added comprehensive JSDoc comments with examples to all core functions, types, and classes, providing rich IntelliSense in supported editors. - **New Guides & Cookbook**: Added a detailed tutorial, a cookbook with practical recipes (e.g., pagination, soft-delete), and updated the README with a quick-start guide. - **Starlight Migration**: All documentation has been migrated to a modern, searchable Starlight-powered site. ## 💥 Breaking Changes - The `indexes` property has been removed from `Table.define` options. Indexes must now be created separately using the new `Index.define()` function. - `Table.define` is deprecated. Use `Table.normal()` for standard tables and `Table.relation()` for edge tables instead. --- ## import { Aside } from '@astrojs/starlight/components'; Source: changelog/0.5.1.mdx import { Aside } from '@astrojs/starlight/components'; This release includes an important bug fix for schema generation, ensuring consistent behavior when using the `OVERWRITE` and `IF NOT EXISTS` methods. ## 🛠️ Fixes ### Schema Generation - **Fixed OVERWRITE method application**: The `OVERWRITE` and `IF NOT EXISTS` methods were only being applied to `DEFINE TABLE` statements, but not to `DEFINE FIELD` or `DEFINE INDEX` statements. This inconsistency has been resolved by ensuring these methods are now consistently applied to all schema definition statements (tables, fields, and indexes). This fix ensures that when using schema generation with the `OVERWRITE` method, all schema elements will be properly overwritten as expected, providing consistent behavior across the entire schema. ```typescript // Now correctly applies OVERWRITE to table, fields, and indexes await applySchema(db, [User, UserEmailIndex], "OVERWRITE"); ``` ## 🧪 Tests - Added comprehensive tests to verify that the `OVERWRITE` and `IF NOT EXISTS` methods are correctly applied to all schema definition types. --- ## import { Aside } from '@astrojs/starlight/components'; Source: changelog/0.5.2.mdx import { Aside } from '@astrojs/starlight/components'; This release fixes a critical issue with relation table DDL generation and ensures proper SurrealDB table type specifications are applied during schema creation. ## 🛠️ Fixes ### Schema Generation - **Fixed table TYPE clause in DDL generation**: Restored the missing `TYPE` clause in `DEFINE TABLE` statements. The DDL generator now correctly outputs: - `DEFINE TABLE tablename TYPE RELATION` for relation tables created with `Table.relation()` - `DEFINE TABLE tablename TYPE NORMAL` for normal tables created with `Table.normal()` Previously, relation tables were being created as normal tables, which prevented proper SurrealDB edge/graph functionality. ```typescript // Now correctly generates TYPE RELATION DDL class Likes extends Table.relation({ name: 'likes', schemafull: true, fields: { in: Field.record(() => User), out: Field.record(() => Post), } }) {} // Generates: DEFINE TABLE likes TYPE RELATION SCHEMAFULL; ``` ### Relation Table Creation - **Enhanced relation table record creation**: Updated the `create` method to use SurrealDB's `insertRelation` method specifically for relation tables, ensuring proper edge record creation and validation. - **Improved type safety**: Replaced generic `any[]` types with more specific `{ [key: string]: unknown }[]` for better type safety in relation table creation. ## 🧪 Tests - Updated all DDL generation tests to expect correct `TYPE` clauses in generated schema - Added `OVERWRITE` method to relation table schema applications to prevent conflicts during testing - Enhanced test coverage for relation table functionality --- ## import { Aside } from '@astrojs/starlight/components'; Source: changelog/0.6.0.mdx import { Aside } from '@astrojs/starlight/components'; This release enhances the query builder with comprehensive SurrealDB clause support, improves query syntax ordering, and adds extensive test coverage for advanced query features. ## ✨ Features ### Query Builder Enhancements - **Complete SurrealDB clause support**: Added support for all modern SurrealDB query clauses: - `WITH INDEX` and `WITH NOINDEX` for index control - `SPLIT` clause for field splitting operations - `TIMEOUT` clause for query timeout specification - `PARALLEL` clause for parallel query execution - `TEMPFILES` clause for temporary file usage - `EXPLAIN` clause for query plan analysis - **Query syntax ordering**: Reordered `SelectQueryOptions` interface properties to match SurrealDB's official syntax order, improving developer experience and consistency. - **Enhanced ORDER BY support**: Added support for `COLLATE` and `NUMERIC` options in ORDER BY clauses with correct syntax positioning. ## 🛠️ Fixes ### Query Builder - **Fixed 'only' clause positioning**: Corrected a bug where the `ONLY` clause was incorrectly positioned in the FROM clause and incorrectly parsed, ensuring proper single record query behavior. - **Fixed SurrealQL clause ordering**: Corrected the order of `COLLATE` and `NUMERIC` modifiers in ORDER BY clauses to appear before the sort direction (ASC/DESC), matching SurrealDB syntax requirements. - **Improved type safety**: Enhanced type assertions and boolean coercion in query result processing to prevent type-related issues. - **Query builder refactoring**: Extracted query building logic into focused helper functions for better maintainability while preserving all functionality: - `buildSelectFromClause()` for SELECT/FROM logic and RecordId binding - `buildOrderByClause()` for ORDER BY with collation/numeric options - `buildQuery()` for complete query assembly in correct SQL order - `executeAndProcessQuery()` for query execution and result processing ## 🧪 Tests - **Comprehensive test coverage**: Added extensive test coverage for all new query options including WITH, SPLIT, TIMEOUT, PARALLEL, TEMPFILES, and EXPLAIN clauses. - **Query debugging tests**: Added isolated test cases to identify and document query execution issues, particularly with the PARALLEL clause. - **Enhanced query validation**: Improved test assertions and error handling for complex query combinations. ## 📚 Documentation - **Enhanced JSDoc**: Updated function documentation to reflect new query capabilities and correct clause ordering. - **Query examples**: Added comprehensive examples demonstrating all supported SurrealDB query clauses and their proper usage. --- ## import { Aside } from '@astrojs/starlight/components'; Source: changelog/1.0.0-alpha.1.mdx import { Aside } from '@astrojs/starlight/components'; This major release migrates Unreal ORM to SurrealDB JS SDK 2.0 alpha, adds comprehensive transaction support, and modernizes the API with enhanced type safety and query building capabilities. ## ✨ Features ### SurrealDB JS SDK 2.0 Alpha Migration - **Upgrade to SurrealDB SDK 2.0.0-alpha.14** (and @surrealdb/node 2.3.4 for internal tests) - **Add BoundQuery and Expr type support** for field options (assert, default, value, permissions) - **Implement surql template literals** for type-safe SurrealQL expressions - **Add SurrealLike type** for SurrealDB object compatibility (Surreal, Transaction, Session) ### Client-side Transactions Support > **Note:** Client-side transactions are only supported in SurrealDB v3 (alpha). - **Implement SurrealLike parameter** for all CRUD methods (create, select, update, delete) - **Create comprehensive transaction tests** with feature flag checking - **Add feature flag checking** for transaction compatibility ### API Modernization - **Refactor update methods** with explicit modes: content, merge, replace, patch - **Add JsonPatchOperation support** for patch mode updates - **Update build configuration** to exclude tests from package output ### Development Tooling - **Switch package manager** from pnpm back to bun and update workspace configuration - **Update test files** with new update method syntax and surql templates ## 🛠️ Breaking Changes ### Field Options Type Changes - **Field options now use BoundQuery/Expr instead of strings** **Before:** ```ts Field.string({ assert: "$value CONTAINS '@'", default: "'unknown@example.com'", }); ``` **After:** ```ts import { surql } from "surrealdb"; Field.string({ assert: surql`$value CONTAINS "@"`, default: surql`"unknown@example.com"`, }); ``` ### Update Method Signature Changes - **Update method signature now requires mode parameter and options object** **Before:** ```ts await user.update(db, { name: "Jane" }); await user.merge(db, { name: "Jane" }); ``` **After:** ```ts await user.update(db, { data: { name: "Jane" }, mode: "merge" }); await user.update(db, { data: { name: "Jane" }, mode: "content" }); ``` ### Merge Method Removal - **Merge method removed, use update with mode: "merge" instead** **Before:** ```ts await user.merge(db, { name: "Jane" }); ``` **After:** ```ts await user.update(db, { data: { name: "Jane" }, mode: "merge" }); ``` --- --- ## import { Aside } from "@astrojs/starlight/components"; Source: changelog/1.0.0-alpha.10.mdx import { Aside } from "@astrojs/starlight/components"; This release adds new field type definition methods and fixes schema generation compatibility with SurrealDB v3. ## ✨ Features ### `Field.literal()` - SurrealQL Literal Types Define fields using SurrealQL's native literal type syntax directly in your schema: ```typescript class Post extends Table.normal({ status: Field.literal("draft"), // or "published" config: Field.literal({ mode: "strict", enabled: true }), }); ``` This generates proper SurrealQL field definitions like `TYPE "draft"` and `TYPE { mode: "strict", enabled: true }`. ### `Field.union()` - Union Type Definitions Define fields with multiple possible types: ```typescript class Post extends Table.normal({ value: Field.union([Field.string(), Field.int()]), status: Field.union([Field.literal("draft"), Field.literal("published")]), }); ``` Generates: `TYPE string | int` and `TYPE "draft" | "published"`. ### AI Usage Guide New documentation guide for integrating AI assistance into your development workflow with Unreal ORM. ### Automated AI Context Files The docs now automatically generate `llms.txt` and `llms-full.txt` context files for improved AI tool compatibility. ## 🔧 Improvements ### SurrealDB Dependency Updates - Upgraded `surrealdb` from `^2.0.0` to `^2.0.3` - Upgraded `@surrealdb/node` from `^3.0.1` to `^3.0.3` ## 🐛 Bug Fixes ### `FLEXIBLE` Keyword Placement Fixed schema generation to place the `FLEXIBLE` keyword before the `TYPE` clause. SurrealDB v3 has stricter parsing rules, and this ordering is required: ```sql -- Before (broken in SurrealDB v3): DEFINE FIELD data ON TABLE posts TYPE object FLEXIBLE; -- After (correct): DEFINE FIELD data ON TABLE posts FLEXIBLE TYPE object; ``` --- ## import { Aside } from "@astrojs/starlight/components"; Source: changelog/1.0.0-alpha.11.mdx import { Aside } from "@astrojs/starlight/components"; This release introduces **explicit database connections**, a **type-safe query builder**, **vector index support**, **file storage buckets**, and a complete **documentation redesign**. ## ⚠️ Breaking Changes ### Explicit `db` Argument Required Global database configuration has been removed. All model CRUD methods (`create`, `insert`, `select`, `count`, `update`, `updateMany`, `delete`, `deleteMany`) and instance methods now require an explicit `db` argument as the first parameter. **Before:** ```typescript import { configure } from "unreal-orm"; configure({ getDatabase: () => db }); // db was resolved implicitly from global config const users = await User.select({ where: surql`email = 'bob@bob.com'` }); ``` **After:** ```typescript // db must be passed explicitly as the first argument const users = await User.select(db, { where: surql`email = 'bob@bob.com'` }); ``` The `configure()` function (previously exported from `unreal-orm`) has been removed. A `validateDb()` utility is now used internally to provide clear error messages when `db` is missing. ## ✨ Features ### Type-Safe Query Builder A new field proxy query builder provides typed operators, SurrealDB built-in functions, and graph traversal syntax directly on field references. The proxy is passed as a callback parameter to `where` clauses on `select`, `count`, `updateMany`, and `deleteMany`: ```typescript const results = await Post.select(db, { where: (f) => f.title.contains("hello"), select: { title: true, commentCount: typed(surql`count(<-comment)`), }, }); ``` The proxy exposes typed namespaces for `string::*`, `math::*`, `array::*`, `time::*`, `meta::*`, `record::*`, `type::*`, `is::*`, and other SurrealDB function families, with full TypeScript autocompletion and return type inference. Graph traversal operators (`->`, `<-`, `<->`) are available directly on field refs. Standalone helper functions `and(...)`, `or(...)`, `not(...)`, `eq()`, `gt()`, `gte()`, `lt()`, `lte()`, `countIn()`, `countOut()`, and more are also exported for composing complex conditions. ### `upsert` Method A new `UPSERT` CRUD method creates or updates a record based on unique index lookup, avoiding table scans: ```typescript const user = await User.upsert(db, { data: { email: "bob@bob.com", name: "Bob Bobson" }, }); ``` ### New CRUD Helpers: `relate`, `count`, `updateMany`, `deleteMany` New static methods added to model classes: - **`relate`** — Creates graph edges with `RELATE` syntax, with optional `OR UPDATE` and custom edge IDs - **`count`** — Counts records with optional `where` filter and `by` grouping - **`updateMany`** — Bulk updates all records matching a `where` clause - **`deleteMany`** — Bulk deletes all records matching a `where` clause ```typescript // Create a graph edge const follow = await Follow.relate(db, { from: user.id, to: post.id, data: { since: new Date() }, }); // Count active users const activeCount = await User.count(db, { where: (f) => f.is_active.eq(true), }); // Bulk update await User.updateMany(db, { where: (f) => f.is_active.eq(false), data: { is_active: true }, mode: 'merge', }); // Bulk delete await User.deleteMany(db, { where: (f) => f.is_active.eq(false), }); ``` ### Range APIs (`RangeId` and `Range`) New builder APIs for SurrealDB record ID and value ranges, with support for `NONE` and `UNBOUNDED` sentinels: ```typescript import { RangeId, NONE, UNBOUNDED } from "unreal-orm"; const range = RangeId("player").from(["mage", NONE]).to(["mage", UNBOUNDED]); const results = await Player.select(db, { from: range }); ``` ### `set` Update Mode A new `set` update mode supports computed field expressions via the query builder. Available on `update` (with `data` and `mode: 'set'`) and `updateMany` (which accepts a callback for both `where` and `data`): ```typescript // Single record update with SET mode await Post.update(db, postId, { mode: 'set', data: { views: surql`views + 1` }, }); // Bulk update with field proxy callback await Post.updateMany(db, { mode: 'set', where: (f) => f.status.eq('published'), data: (f) => ({ views: f.views.add(1) }), }); ``` ### Callback-Based `where` Clauses `select`, `count`, `updateMany`, and `deleteMany` now accept callback-based `where` clauses for type-safe condition building. Use the standalone `and(...)`, `or(...)`, and `not(...)` functions to combine conditions: ```typescript import { and } from "unreal-orm"; const active = await User.select(db, { where: (f) => and(f.is_active.eq(true), f.age.gte(18)), }); ``` ### Index Enhancements New index options added: - `COUNT` — maintain a count of indexed entries - `CONCURRENTLY` — build index without blocking writes - `DEFER` — defer index creation to next transaction - `FULLTEXT` / `BM25` / `HIGHLIGHTS` — full-text search index configuration ### Select Options New select query options: - `EXPLAIN` — query execution plan - `SPLIT` — split results on a field - `WITH` — index hint - `TEMPFILES` — use temporary files for large result sets ### Vector Index Support (MTREE, HNSW, DISKANN) Full vector index support across the ORM and CLI, including parsing, generation, extraction, comparison, introspection, and codegen: ```typescript import { Index, Field } from "unreal-orm"; class Post extends Table.normal({ embedding: Field.array(Field.float()), // ... }); Index.define(() => Post, { name: "idx_embedding", fields: ["embedding"], vector: { type: "MTREE", dimension: 128, distance: "EUCLIDEAN", }, }); ``` Supports all three vector index types with configurable dimension, distance metric, and element type. The CLI `pull` command now introspects vector indexes, and `diff` shows detailed change descriptions (e.g., "dimension 128 → 256", "type MTREE → HNSW"). ### File Storage Buckets New `Bucket.define()` API for SurrealDB v3 file storage buckets (experimental): ```typescript import { Bucket } from "unreal-orm"; const AvatarBucket = Bucket.define({ name: "avatars", backend: "memory", }); const UploadBucket = Bucket.define({ name: "uploads", backend: "file:/var/data/uploads", permissions: surql`WHERE $auth.role = 'admin' OR $action = 'get'`, readonly: true, }); await Unreal.applySchema(db, [User, AvatarBucket, UploadBucket]); ``` Supports memory and file backends, read-only mode, permission clauses with `$action`/`$file`/`$target` variables, and global buckets. Requires the `files` experimental capability in SurrealDB v3. ## 🔧 Improvements ### Type Safety Overhaul - Enforced `RecordId` type for `CreateData.id` across `create`, `upsert`, and `relate` - Removed all `db as unknown as Surreal` casts in favor of `SurrealLike.query()` with typed `surql` calls - Removed `Record` destructuring casts in favor of direct typed destructuring - Removed `as TableData` casts via proper `surql<[TableData[]]>` result typing - Fixed `id.table.name` usage instead of unsafe `(id as { tb?: string }).tb` cast - Fixed discriminated union narrowing for update modes ### Field Assert Refactoring `resolveAssert` now accepts an `AssertCallback` and uses `createValueProxy` for a typed `$value` proxy in field assertions. Removed redundant `prop as string` casts in field-proxy. ### Documentation Redesign - Complete restructure into **getting-started**, **concepts**, **querying**, **advanced**, and **guides** sections - New hand-drawn design system with custom CSS, fonts, paper textures, and dark mode palette - New pages: installation, quickstart, what-is-surrealdb, fields-and-types, indexes, models-and-tables, schema-application, surrealql-mapping, selecting, inserting, updating, deleting, where-builder, relations, permissions, migrations, sdk-interop, testing, cli-usage, full-text-search, geospatial, file-storage - Fixed all SurrealDB SDK connection examples to use single `.connect()` call pattern - Upgraded Astro, Starlight, and docs dependencies to latest - Bumped TypeScript to 7.0.2 and Biome to 2.5.6 ### Dependency Updates - Bumped `surrealdb` peer and dev dependency to `^2.0.8` ## 🐛 Bug Fixes ### CLI - Removed duplicate `generateMigrationSurql` and `compareSchemas` from CLI — now re-exports from `unreal-orm` - Removed dead code from `generateMigration` (unused functions and variables) - Deleted empty introspection types file - Removed redundant `db.use()` call (handled by `db.connect()`) - Fixed VIEW table generation: removed incorrect `TYPE NORMAL` / `TYPE ANY` clauses - Hardened push transaction: ensured semicolons between statements in `BEGIN...COMMIT` block - Fixed inconsistent `SchemaChange` import (direct import from `unreal-orm` instead of inline) - Fixed `idx_email.fields` fallback for `string[] | undefined` type mismatch - Enhanced `compareSchemas` with vector index type suffix and detailed vector parameter change descriptions --- ## import { Aside } from '@astrojs/starlight/components'; Source: changelog/1.0.0-alpha.2.mdx import { Aside } from '@astrojs/starlight/components'; This alpha release introduces support for SurrealDB's experimental `REFERENCE` fields and pre-computed table views, along with DDL generation updates. ## ✨ Features ### Reference Fields (Experimental) - **Added `reference` option to `RecordFieldOptions`**: You can now mark a record link as a `REFERENCE`. - **Support for `ON DELETE` actions**: Configure behavior when the referenced record is deleted (`IGNORE`, `UNSET`, `CASCADE`, `REJECT`). ```typescript import { Field } from "unreal-orm"; const Comment = Field.record( () => User, { // Simple reference reference: true, // Or with specific options reference: { onDelete: "CASCADE" } } ); ``` ### Table Views - **Added `Table.view()` method**: Define pre-computed table views using `DEFINE TABLE ... AS SELECT ...`. - **Support for `AS` clause**: Views are defined by a query that is executed to populate the table. ```typescript import { Table } from "unreal-orm"; import { surql } from "surrealdb"; class AdultUsers extends Table.view({ name: "adult_users", as: surql`SELECT * FROM user WHERE age >= 18`, }) {} ``` You can also provide a TypeScript type to `Table.view` to infer the shape of the view: ```typescript type AdultUser = { name: string; age: number }; class AdultUsers extends Table.view({ name: "adult_users", as: "SELECT name, age FROM user WHERE age >= 18", }) {} ``` ## 🛠️ Internals ### DDL Generation - **Updated DDL generators**: - `generateFieldsDdl`: Adds `REFERENCE` and `ON DELETE` clauses. - `generateTableDdl`: Adds `TYPE VIEW` (handled as normal tables with `AS` clause) and the `AS` query string. ### Testing - **Enabled experimental capabilities**: Updated test database setup to allow experimental features for testing `REFERENCE` fields. --- ## import { Aside } from '@astrojs/starlight/components'; Source: changelog/1.0.0-alpha.3.mdx import { Aside } from '@astrojs/starlight/components'; This alpha release marks the **first public release of `@unreal-orm/cli`** — a complete CLI toolkit for schema management. It also introduces implicit database support and significant DX improvements. ## 🚀 Introducing @unreal-orm/cli The new CLI package provides powerful tools for managing your SurrealDB schema: ```bash # Quick start bunx @unreal-orm/cli init # Or with other package managers npx @unreal-orm/cli init ``` ### Available Commands | Command | Description | |---------|-------------| | `unreal init` | Initialize project with connection and sample tables | | `unreal pull` | Generate TypeScript models from database schema | | `unreal push` | Apply TypeScript schema to database | | `unreal diff` | Compare code vs database schema | | `unreal mermaid` | Generate ERD diagrams | | `unreal view` | Interactive TUI for browsing/editing records | | `unreal docs` | Open documentation | | `unreal github` | Open GitHub repository | ## ✨ Features ### Interactive Database Viewer New `unreal view` command provides a TUI for browsing and editing database records directly from the terminal. ```bash unreal view ``` **Capabilities:** - **Table list** with concurrent count fetching - **Records view** with pagination and row selection - **Record detail view** with batch editing - **Multi-line text editor** with cursor movement and optimized rendering - Configurable `--timeout` and `--concurrency` options **Keyboard shortcuts:** - `↑/↓` or `j/k` — Navigate - `Enter` — Select/Edit - `e` — Edit field - `+` — Add field, `-` — Remove field - `s` — Save changes - `d` — Delete record - `b` or `Esc` — Go back - `q` — Quit ### Implicit Database Support All CRUD methods now support implicit database connections, reducing boilerplate: ```typescript // Before: explicit db required const users = await User.select(db, { limit: 10 }); const user = await User.create(db, { name: "John" }); // After: implicit db (uses configured default) const users = await User.select({ limit: 10 }); const user = await User.create({ name: "John" }); ``` ### Enhanced Init Experience The `unreal init` command is now the primary entry point with improved DX: - **CLI as dev dependency** — Installed automatically for local usage - **Auto-inject surreal.ts import** — Optionally adds import to your app entry point - **Package manager detection** — Supports npm, yarn, pnpm, and bun ### Auto-Generated CLI Documentation CLI reference documentation is now automatically generated from Commander.js definitions: - New **CLI Reference** section in docs sidebar - Individual pages for each command with options tables - Stays in sync with actual CLI implementation ## 🔧 Improvements ### Schema Architecture Refactor - **Centralized AST logic** — Schema AST, parser, and generator moved from `unreal-cli` to `unreal-orm` - **AST-based DDL generation** — Replaced legacy DDL generation with unified AST implementation - **Shared utilities** — CLI now uses schema utilities exported from ORM package ### Build & Module Resolution - **ESM fixes** — Resolved runtime ESM errors using `bun build` - **Consistent release process** — Added `publish:flow` script to both packages ### Documentation - **Galaxy theme** — Added starlight-theme-galaxy plugin for improved docs styling - **Updated migration guide** — Comprehensive CLI tools documentation - **Fixed banner overflow** — Custom CSS fix for theme styling issue ## 💥 Breaking Changes ### `$dynamic` Property Removed Extra fields (fields not defined in your schema) are now assigned directly to model instances. ```typescript // Before console.log(user.$dynamic.someExtraField); // After console.log(user.someExtraField); ``` ### Enhanced `from()` Method The `from` method now supports `surql` template literals and raw queries: ```typescript import { surql } from "surrealdb"; // Record ID (still works) const user = await User.from(db, "user:123"); // SurrealQL query (new) const users = await User.from(db, surql`SELECT * FROM user WHERE age > 18`); // Raw query string (new) const users = await User.from(db, { raw: "SELECT * FROM user WHERE active = true" }); ``` ## 📦 Package Updates | Package | Version | |---------|---------| | `unreal-orm` | 1.0.0-alpha.3 → 1.0.0-alpha.5 | | `@unreal-orm/cli` | 1.0.0-alpha.3 → 1.0.0-alpha.5 | ### 1.0.0-alpha.5 (Patch) - **fix(cli)**: Update `init` command to install packages with `@latest` tag - **docs**: update docs to use `@latest` tag ### 1.0.0-alpha.4 (Patch) - **chore**: Switch publish tag from `alpha` to `latest` - **fix(orm)**: Switch build from `bun build` to `unbuild` to fix Node.js ESM resolution errors when using `bunx @unreal-orm/cli` ## 🔗 Links - [Migration Guide](/guides/migrating-to-alpha/) - [CLI Reference](/cli/) --- ## import { Aside } from '@astrojs/starlight/components'; Source: changelog/1.0.0-alpha.6.mdx import { Aside } from '@astrojs/starlight/components'; This release introduces powerful type-safe selection capabilities to the ORM and significantly improves the CLI's schema parsing robustness. ## ✨ Features ### Type-Safe Select (UnrealORM) The ORM now supports full type inference for specific field selections, including nested objects and computed fields. ```typescript import { typed } from 'unreal-orm'; import { surql } from 'surrealdb'; // 1. Nested object selection with type inference const posts = await Post.select({ select: { title: true, author: { name: true, email: true }, // Auto-expands record links metadata: { category: true } // Deep object selection } }); // Result type: { title: string; author: { name: string; email: string }; metadata: { category: string } }[] // 2. Computed fields with typed() helper const stats = await Post.select({ select: { title: true, commentCount: typed(surql`count(<-comment)`) } }); // Result type: { title: string; commentCount: number }[] // 3. Type-safe OMIT const users = await User.select({ omit: { password: true, secret: true } }); // Result type: Omit[] // 4. SELECT VALUE const names = await User.select({ value: 'name' }); // Result type: string[] ``` ### Improved Schema Parsing (UnrealCLI) The CLI's schema parser has been enhanced to better handle real-world schemas and edge cases: - **READONLY Support**: Correctly extracts `READONLY` attributes from field definitions. - **Robust Error Handling**: Added try-catch blocks with user-friendly warnings instead of crashing on parse errors. - **Unsupported Feature Reporting**: Explicitly warns about unsupported features like events, functions, and params instead of silently ignoring them. - **Mermaid Improvements**: Warnings are now displayed in the `mermaid` command output when parsing `.surql` files. - **Configuration Fix**: The `-y` flag now correctly defaults to using the config file unless explicit DB credentials are provided. - **Cleanup**: Removed the `/* Schema not inferred */` comment from empty `Field.object({})` definitions. ## 📦 Package Updates | Package | Version | |---------|---------| | `unreal-orm` | 1.0.0-alpha.5 → 1.0.0-alpha.6 | | `@unreal-orm/cli` | 1.0.0-alpha.5 → 1.0.0-alpha.6 | --- ## import { Aside } from '@astrojs/starlight/components'; Source: changelog/1.0.0-alpha.7.mdx import { Aside } from '@astrojs/starlight/components'; This release introduces the new `insert()` method with full SurrealDB INSERT statement support, and improves CLI reliability. ## ✨ Features ### INSERT Statement Support A new type-safe `insert()` method that provides full access to SurrealDB's INSERT statement capabilities: ```typescript import { surql } from 'surrealdb'; // Single insert (explicit db) const user = await User.insert(db, { data: { name: 'John', email: 'john@example.com' }, }); // Bulk insert (implicit db) const users = await User.insert({ data: [ { name: 'John', email: 'john@example.com' }, { name: 'Jane', email: 'jane@example.com' }, ], }); // With custom ID (must be RecordId type) const user = await User.insert(db, { data: { id: new RecordId('user', 'john'), name: 'John' }, }); // INSERT IGNORE - silently skip duplicates await User.insert(db, { data: { id: existingId, name: 'John' }, ignore: true, }); // ON DUPLICATE KEY UPDATE with native SurrealQL await User.insert(db, { data: { id: existingId, name: 'John', visits: 0 }, onDuplicate: surql`visits += 1, lastSeen = time::now()`, }); // Custom RETURN clause with native SurrealQL const result = await User.insert(db, { data: { name: 'John', email: 'john@example.com' }, return: surql`id, name, email`, }); // Insert relation (auto-detected for relation tables) await Follows.insert(db, { data: { in: userId, out: targetId, createdAt: new Date() }, }); ``` ## 🔧 Improvements ### CLI Improvements - **`--no-count` Option**: Added `--no-count` flag to the `view` command to skip fetching table record counts, improving performance for large databases. - **Version Detection**: Improved CLI version detection to work correctly when installed globally from npm. - **Version Check**: Changed default npm dist-tag from `alpha` to `latest` for update checks. ## 🐛 Bug Fixes - Fixed CLI package.json not being included in published package, which caused version detection to fail. ## 📦 Package Updates | Package | Version | |---------|---------| | `unreal-orm` | 1.0.0-alpha.6 → 1.0.0-alpha.7 | | `@unreal-orm/cli` | 1.0.0-alpha.6 → 1.0.0-alpha.7 | --- ## import { Aside } from '@astrojs/starlight/components'; Source: changelog/1.0.0-alpha.8.mdx import { Aside } from '@astrojs/starlight/components'; This release introduces a unified `--log-level` flag for the CLI, upgrades SurrealDB dependencies to `surrealdb@2.0.0` and `@surrealdb/node@3.0.1`, fixes query binding collisions, and deprecates the `PARALLEL` clause. ## ✨ Features ### CLI: `--log-level` Flag A new `--log-level` option is available on every command, giving you full control over CLI output verbosity: ```bash # Suppress all output except errors (great for CI/automation) unreal push --log-level silent --embedded memory -y # Default behavior — spinners, headers, success messages unreal pull --log-level normal # Timestamped step-by-step debug logging unreal diff --log-level debug ``` | Level | Behavior | |-------|----------| | `silent` | Only errors and warnings (via stderr) | | `normal` | Default — spinners, headers, success messages | | `debug` | Timestamped `[DEBUG +Nms]` logs at each step | The `--log-level` option appears in every command's `--help` output and is processed automatically before command execution. ### Query `DEBUG` Option Added a `DEBUG` option to `select()`, `count()`, and `insert()` queries that logs the generated SurrealQL and bindings: ```typescript const users = await User.select(db, { where: eq("city", "NY"), DEBUG: true, // Logs query string and bindings to console }); ``` ### `SurrealLike` Type: Transaction Support Updated the `SurrealLike` type to make `connect` and `close` optional, ensuring transaction objects can be used wherever a database instance is expected. ## 🔧 Improvements ### CLI Improvements - **Debug Instrumentation**: Added `debug()` calls throughout `push`, `pull`, `diff`, `mermaid`, and `connect` commands for step-by-step visibility when using `--log-level debug`. - **Silent Mode**: In silent mode, the update check is skipped entirely, spinners are replaced with no-op stubs, and `printer.log()` is suppressed. - **Process Exit Fix**: Added `process.exit(0)` to the `postAction` hook to prevent the CLI from hanging when `@surrealdb/node` native engine holds event loop handles after `db.close()`. - **Test Output Suppression**: All E2E test shell invocations now use Bun's `.quiet()` method, keeping test runner output clean while still capturing stdout/stderr for assertions. ### Query Binding Key Uniqueness Replaced `Date.now()` with an incrementing counter for query binding keys (`record_0`, `table_1`, `limit_2`, etc.). This prevents key collisions when multiple bindings are generated within the same millisecond. ### Test Improvements - Reduced redundant test setup logging with a `hasLoggedSetup` guard in `dbTestUtils.ts`. - Removed stray `console.log` in the CRUD integration tests. - Added `DELETE person` cleanup in transaction tests to avoid stale data. ## 💥 Breaking Changes ### Array Length Semantics In SurrealDB v3, `array` now means the array must contain **exactly 5 elements**, not "up to 5" as in v2. To reflect this, `Field.array()` now accepts a `length` option (replacing the deprecated `max`): ```typescript // New: use `length` (recommended) tags: Field.array(Field.string(), { length: 5 }) // Deprecated: `max` still works but is misleading for v3 tags: Field.array(Field.string(), { max: 5 }) ``` Both generate the same `array` DDL — the database version determines interpretation. ## ⚠️ Deprecations ### `PARALLEL` Clause The `parallel` option on `select()` and `count()` queries is now deprecated. SurrealDB 2.2+ no longer supports the `PARALLEL` clause — the option is retained for backwards compatibility but is silently ignored. Related debug tests have been removed. ## 📦 Dependency Updates | Package | Change | |---------|--------| | `surrealdb` | `^2.0.0-alpha.14` → `^2.0.0` | | `@surrealdb/node` | `2.3.4` → `^3.0.1` | ## 🏗️ Infrastructure - **Package Manager**: Migrated workspace from Bun to pnpm (`pnpm-workspace.yaml`) to resolve monorepo dependency resolution issues. - **Workspace Config**: Switched from wildcard workspace globs to explicit package paths. ## 📦 Package Updates | Package | Version | |---------|---------| | `unreal-orm` | 1.0.0-alpha.7 → 1.0.0-alpha.8 | | `@unreal-orm/cli` | 1.0.0-alpha.7 → 1.0.0-alpha.8 | --- ## import { Aside } from '@astrojs/starlight/components'; Source: changelog/1.0.0-alpha.9.mdx import { Aside } from '@astrojs/starlight/components'; This release focuses heavily on **TypeScript type safety and inference improvements**, specifically targeting nested projections and query result mapping. ## ✨ Features ### Documentation Improvements & Advanced Guides Completely revamped the documentation structure and added a suite of deep-dive guides for production-grade development: - **[Mastering Graph Relations](/guides/graph-relations/):** Advanced many-to-many patterns, edge properties, and high-performance `FROM` clause path traversal. - **[Security & Permissions](/advanced/permissions/):** Row-level security (RLS) patterns using `$auth` for table and field-level protection. - **[Testing Strategies](/advanced/testing/):** Ultra-fast unit testing workflows using the SurrealDB `mem://` engine and `@surrealdb/node`. - **[Common Patterns](/guides/patterns/):** Architectural recipes for singletons, tagging systems, recursive hierarchies, and polymorphic links. - **[Migrations & Schema Sync](/guides/migrations/):** Best practices for the CLI `pull`/`push`/`diff` workflow and CI/CD integration. ### CLI Unified Log Levels Introduced a centralized `--log-level` flag (`silent`, `normal`, `debug`) across all CLI commands. ### Interactive CLI Prompts The CLI now provides interactive prompts for database connections if flags are omitted, making it more convenient for manual usage. ### Deeply Nested Type Inference The type signatures for `Table.select()` and the `FieldSelect` utility have been entirely rewritten to support infinite-depth schema awareness. When projecting complex structures, TypeScript will now provide full autocompletion and strict validation for: - Nested `Field.object()` properties - Linked tables via `Field.record()` - Array projections like `Field.array(Field.object({...}))` **Example:** ```typescript const posts = await Post.select(db, { select: { title: true, // Primitive author: { name: true, email: true }, // Infers User schema across Record link metadata: { category: true, history: { date: true } // Infers Array Element schema } } }); // `posts` is strictly typed as: // Array<{ title: string, author: { name: string, email: string }, metadata: { category: string, history: Array<{ date: string }> } }> ``` If you specify an unknown field inside a nested object, the TypeScript compiler will now correctly reject it: ```typescript const posts = await Post.select(db, { select: { author: { invalidField: true } // ❌ TypeScript Error! } }); ``` ## 🔧 Improvements ### Strict `only: true` Return Types The `.select()` method now provides precise return type narrowing when using the `only: true` query option. When specified alongside field projections (`select:`) or exclusions (`omit:`), TypeScript will correctly infer the result as a single object (or `undefined`) instead of an array of objects. ```typescript const singlePost = await Post.select(db, { select: { title: true, views: true }, from: post.id, only: true, // Narrows the return type }); // Type: { title: string; views: number } | undefined ``` ### Wildcard Selection Overrides When using the `*` wildcard to select all fields on a table, you can now explicitly declare subselections without breaking type inference. Explicitly declared keys will cleanly override the base wildcard types. ```typescript const results = await Post.select(db, { select: { "*": true, // Fetch all fields author: { name: true } // But strictly project the 'author' relation down to just `name` } }); // Result merges inferShape but narrows `author: { name: string }` ``` ### Enhanced SurrealDB Feature Support The [Capabilities Matrix](/getting-started/capabilities/) has been updated to reflect full support for: - **Table Views (`AS SELECT`)**: Define projected views directly in your schema. - **Changefeeds**: Configure durations and original data inclusion. - **Record References**: Support for `ON DELETE` actions (CASCADE, REJECT, etc.) in `Field.record`. ### Explicit `id` Projection The `FieldSelect` utility now officially permits manually specifying `id: true` alongside standard schema fields, and correctly resolves the output type to the table's `RecordId`. ```typescript const results = await Post.select(db, { select: { id: true, title: true } }); // Result strictly includes `id: RecordId<"post">` ``` ## 🐛 Bug Fixes - **CLI:** `unreal init` will now correctly install `@types/node` (or `@types/bun` if using Bun) as a dev dependency to prevent TypeScript unresolved module errors on fresh setups. - **CLI Examples:** Fixed an outdated query signature in the generated example `Post` class (`order` changed to `orderBy`). --- ## Open the UnrealORM documentation Source: cli/docs.md Open the UnrealORM documentation ## Usage ```bash unreal docs ``` --- ## Open the UnrealORM GitHub repository Source: cli/github.md Open the UnrealORM GitHub repository ## Usage ```bash unreal github ``` --- ## The UnrealORM CLI provides commands for managing your SurrealDB schema and database. Source: cli/index.md The UnrealORM CLI provides commands for managing your SurrealDB schema and database. ## Installation ```bash # npm npm install -g unreal-orm # pnpm pnpm add -g unreal-orm # bun bun add -g unreal-orm ``` ## Commands | Command | Description | |---------|-------------| | [`unreal docs`](/cli/docs/) | Open the UnrealORM documentation | | [`unreal github`](/cli/github/) | Open the UnrealORM GitHub repository | | [`unreal init`](/cli/init/) | Initialize UnrealORM in your project | | [`unreal view`](/cli/view/) | Interactive TUI for browsing database tables and records | ## Global Options All commands support the following global options: ```bash unreal --version # Show version number unreal --help # Show help ``` --- ## Initialize UnrealORM in your project Source: cli/init.md Initialize UnrealORM in your project ## Usage ```bash unreal init [options] ``` ## Options | Option | Description | Default | |--------|-------------|---------| | `--url ` | SurrealDB URL | - | | `-u, --username ` | Database username | - | | `-p, --password ` | Database password | - | | `-n, --namespace ` | Namespace | - | | `-d, --database ` | Database | - | | `-e, --embedded ` | Embedded mode (memory or file path) | - | | `--sample` | Generate sample schema tables | - | | `--from-db` | Import schema from existing database | - | | `--from-surql ` | Import schema from .surql file | - | | `--install` | Install dependencies automatically | - | | `--no-install` | Skip dependency installation | - | | `--pm ` | Package manager (npm, yarn, pnpm, bun) | - | --- ## Interactive TUI for browsing database tables and records Source: cli/view.md Interactive TUI for browsing database tables and records ## Usage ```bash unreal view [options] ``` ## Options | Option | Description | Default | |--------|-------------|---------| | `--url ` | Database URL | - | | `-u, --username ` | Database username | - | | `-p, --password ` | Database password | - | | `-n, --namespace ` | Database namespace | - | | `-d, --database ` | Database name | - | | `--auth-level ` | Authentication level (root, namespace, database) | - | | `-e, --embedded ` | Use embedded mode (memory or file path) | - | | `--page-size ` | Records per page (5-100, default: auto) | - | | `--timeout ` | Query timeout in seconds (default: 3) | - | | `--concurrency ` | Max concurrent count queries (default: 5) | - | | `--no-count` | Skip fetching table record counts | - | --- ## `Field` is a factory object with one method per SurrealDB type. Every method returns a `FieldDefinition` that a `Table` uses to generate `DEFINE FIELD` statements. Source: concepts/fields-and-types.mdx `Field` is a factory object with one method per SurrealDB type. Every method returns a `FieldDefinition` that a `Table` uses to generate `DEFINE FIELD` statements. ## Primitive fields ```ts import { Field } from 'unreal-orm'; const userFields = { name: Field.string(), age: Field.int(), rating: Field.float(), balance: Field.decimal(), active: Field.bool(), createdAt: Field.datetime(), ttl: Field.duration(), avatar: Field.bytes(), token: Field.uuid(), }; ``` ## Links and files ```ts const userFields = { // Record link to user table author: Field.record(() => User), // File pointer into a bucket (requires --allow-experimental files) avatar: Field.file(), }; ``` ## Collections and nested data ```ts const userFields = { tags: Field.array(Field.string()), roles: Field.set(Field.string()), meta: Field.object({ visits: Field.int(), lastVisit: Field.datetime(), }), }; ``` `Field.array` and `Field.set` accept a length limit: ```ts Field.array(Field.string(), { length: 5 }) // exactly 5 elements ``` > Note: `max` is deprecated. Use `length` for the exact element count. ## Optional fields SurrealDB fields are required by default. Wrap a field with `Field.option(...)` to allow `NONE`: ```ts bio: Field.option(Field.string()) ``` ## Special field types ```ts // A fixed literal value, e.g. status must be "active" or "banned" status: Field.literal("active" as const) // A union of types value: Field.union([Field.int(), Field.string()]) // A custom SurrealQL type string not yet covered by a builder custom: Field.custom('string') // Any value; useful for schemaless parts metadata: Field.any() ``` ## Geometry ```ts location: Field.geometry('point') // or 'linestring', 'polygon', 'multipoint', 'multilinestring', 'multipolygon', 'collection', 'feature' ``` ## Field options All builders accept `FieldOptions`: ```ts Field.string({ default: surql`''`, // default value for new records value: surql`time::now()`, // computed on every create/update assert: surql`string::len($value) > 0`, // validation expression readonly: true, // prevents manual updates permissions: { select: surql`id = $auth.id`, update: surql`id = $auth.id`, }, comment: 'User display name', }) ``` ## Record links with references By default `Field.record` stores a `RecordId` without referential integrity. You can enable the experimental `REFERENCE` feature: ```ts author: Field.record(() => User, { reference: { onDelete: 'cascade', // or 'ignore' }, }) ``` This requires SurrealDB to be started with `--allow-experimental record_references`. --- ## Indexes are defined separately from tables and passed to `applySchema` alongside the model classes. Use `Index.define` and pass a thunk that returns the target model; this avoids circular import problems. Source: concepts/indexes.mdx Indexes are defined separately from tables and passed to `applySchema` alongside the model classes. Use `Index.define` and pass a thunk that returns the target model; this avoids circular import problems. ```ts import { Index } from 'unreal-orm'; import { User } from './User'; const UserEmailIndex = Index.define(() => User, { name: 'user_email_idx', fields: ['email'], unique: true, }); ``` ## Index options ```ts Index.define(() => User, { name: 'user_email_idx', fields: ['email'], unique: true, concurrently: true, // CREATE INDEX ... CONCURRENTLY defer: false, // defer index updates to a background queue comment: 'Unique email index', }) ``` | Option | Purpose | |---|---| | `name` | Index name in the database. | | `fields` | Array of field names to index. | | `unique` | Enforce uniqueness across the indexed columns. | | `count` | Build a `COUNT` index for fast `count()` queries. | | `search` | Build a full-text `SEARCH` index. | | `analyzer` | Analyzer to use with a search index. | | `bm25` | Enable BM25 ranking on a search index. | | `highlights` | Enable keyword highlighting on a search index. | | `concurrently` | Create the index without blocking writes. | | `defer` | Use a background queue for index updates. | | `comment` | Optional index comment. | ## Unique and composite indexes ```ts const UserNameIndex = Index.define(() => User, { name: 'user_name_idx', fields: ['firstName', 'lastName'], unique: true, }); ``` ## Full-text search ```ts const PostContentIndex = Index.define(() => Post, { name: 'post_content_idx', fields: ['title', 'content'], search: true, analyzer: 'english', bm25: true, }); ``` Use the index in a query: ```ts const results = await Post.select(db, { where: surql`title @@ 'database' OR content @@ 'database'`, }); ``` See the [Full-Text Search](../advanced/full-text-search/) guide for a deeper dive. ## Vector indexes Vector indexes enable efficient k-nearest-neighbor (kNN) similarity search on high-dimensional vector embeddings. UnrealORM supports three algorithms: | Algorithm | Best for | Storage | |---|---|---| | `MTREE` | Smaller datasets, exact distance | In-memory | | `HNSW` | Low-latency ANN, graph fits in memory | In-memory graph + persistence | | `DISKANN` | Very large corpora, RAM-limited (SurrealDB 3.1+) | Key-value-backed graph + bounded cache | ### HNSW index ```ts const EmbeddingIndex = Index.define(() => Document, { name: 'doc_embedding_hnsw', fields: ['embedding'], vector: { type: 'HNSW', dimension: 768, distance: 'COSINE', elementType: 'F32', efc: 200, // EF construction (default: 150) m: 16, // Max connections per element (default: 12) m0: 32, // Max connections in lowest layer (default: 24) }, }); ``` ### MTREE index ```ts const EmbeddingIndex = Index.define(() => Document, { name: 'doc_embedding_mtree', fields: ['embedding'], vector: { type: 'MTREE', dimension: 768, distance: 'COSINE', elementType: 'F32', }, }); ``` ### DISKANN index ```ts const EmbeddingIndex = Index.define(() => Document, { name: 'doc_embedding_diskann', fields: ['embedding'], vector: { type: 'DISKANN', dimension: 768, distance: 'EUCLIDEAN', elementType: 'F32', degree: 64, // Target max graph degree (default: 64) lBuild: 100, // Construction search-list size (default: 100) alpha: 1.2, // Pruning parameter (default: 1.2) hashedVector: true, // Hash-stabilised vector-document keys }, }); ``` ### Vector options reference | Option | Applies to | Description | |---|---|---| | `type` | All | Algorithm: `'MTREE'`, `'HNSW'`, or `'DISKANN'` | | `dimension` | All | Vector dimension (number of elements) | | `elementType` | All | Vector type: `'F64'`, `'F32'`, `'F16'`, `'I64'`, `'I32'`, `'I16'`, `'I8'`, `'U8'` | | `distance` | All | Distance metric: `'EUCLIDEAN'`, `'COSINE'`, `'MANHATTAN'`, `'INNER_PRODUCT'`, `'COSINE_NORMALIZED'`, `'HAMMING'` | | `efc` | HNSW | EF construction (default: 150) | | `m` | HNSW | Max connections per element (default: 12) | | `m0` | HNSW | Max connections in lowest layer (default: 24) | | `lm` | HNSW | Level generation multiplier (auto-computed by default) | | `degree` | DISKANN | Target max graph degree (default: 64) | | `lBuild` | DISKANN | Construction search-list size (default: 100) | | `alpha` | DISKANN | Pruning parameter (default: 1.2) | | `hashedVector` | DISKANN | Enable hash-stabilised vector-document keys | --- ## Everything in UnrealORM starts with `Table`. The factory returns a base class that you extend. Each model knows its table name, fields, and SurrealDB type. Source: concepts/models-and-tables.mdx Everything in UnrealORM starts with `Table`. The factory returns a base class that you extend. Each model knows its table name, fields, and SurrealDB type. ## Table types `Table` has three factories: - `Table.normal(...)` — a standard `DEFINE TABLE`. - `Table.relation(...)` — a `DEFINE TABLE ... TYPE RELATION`, used for graph edges. - `Table.view(...)` — a `DEFINE TABLE ... AS SELECT` pre-computed view. ```ts import { Table, Field } from 'unreal-orm'; import { surql } from 'surrealdb'; class User extends Table.normal({ name: 'user', schemafull: true, fields: { name: Field.string(), }, }) {} class Likes extends Table.relation({ name: 'likes', schemafull: true, fields: { in: Field.record(() => User), out: Field.record(() => Post), createdAt: Field.datetime({ default: surql`time::now()` }), }, }) {} class AdultUsers extends Table.view<{ name: string; age: number }>({ name: 'adult_users', as: surql`SELECT name, age FROM user WHERE age >= 18`, }) {} ``` ## Table options | Option | Purpose | |---|---| | `name` | The table name in SurrealDB. | | `schemafull` | If `true`, only declared fields are allowed. | | `fields` | The field definitions (see [Fields and Types](../fields-and-types/)). | | `permissions` | Row-level `SELECT`/`CREATE`/`UPDATE`/`DELETE` clauses. | | `changefeed` | Enable `DEFINE TABLE ... CHANGEFEED`. | | `drop` | Generate `DEFINE TABLE ... DROP`. | | `comment` | Optional table comment. | ## Custom methods Because `Table.normal` returns a class, you can add instance and static methods directly in the class body. ```ts class User extends Table.normal({ ... }) { getDisplayName() { return `${this.name} <${this.email}>`; } static async findByEmail(db: SurrealLike, email: string) { return User.select(db, { where: surql`email = ${email}`, limit: 1 }); } } ``` ## Indexes Indexes are defined separately and linked via a thunk to avoid circular imports. ```ts import { Index } from 'unreal-orm'; const UserEmailIndex = Index.define(() => User, { name: 'user_email_idx', fields: ['email'], unique: true, }); ``` Pass both models and indexes to `applySchema`. ## Views Views are read-only tables produced by a `SELECT` statement. They have no fields of their own; the generic type argument describes the returned shape. ```ts class AdultUsers extends Table.view<{ name: string }>({ name: 'adult_users', as: surql`SELECT name FROM user WHERE age >= 18`, }) {} ``` --- ## After you define tables, fields, indexes, and buckets, you turn them into SurrealQL and apply them to a database. Source: concepts/schema-application.mdx After you define tables, fields, indexes, and buckets, you turn them into SurrealQL and apply them to a database. ## `applySchema` ```ts import { applySchema } from 'unreal-orm'; import { User, UserEmailIndex, Post, Follow } from './tables'; await applySchema(db, [User, Post, Follow, UserEmailIndex]); ``` `applySchema` walks the definitions and emits `DEFINE TABLE`, `DEFINE FIELD`, `DEFINE INDEX`, and `DEFINE BUCKET` statements in dependency order. The `db` argument is any SurrealDB-like object, such as a `Surreal` client or a transaction. ## Application methods Both `applySchema` and the `generate*` functions accept an optional `method` argument: - `'error'` (default) — throw if the table already exists. - `'IF NOT EXISTS'` — emit `DEFINE ... IF NOT EXISTS`. - `'OVERWRITE'` — emit `DEFINE ... OVERWRITE`. ```ts await applySchema(db, [User], 'IF NOT EXISTS'); ``` ## Generating DDL without applying it ```ts import { generateFullSchemaQl, generateTableSchemaQl } from 'unreal-orm'; // All definables const fullDdl = generateFullSchemaQl([User, UserEmailIndex]); // One table const tableDdl = generateTableSchemaQl(User); ``` This is useful for inspecting the generated SurrealQL, writing manual migrations, or running schema changes through a CI gate. ## The `Unreal` namespace `Unreal` bundles the most common schema utilities into one import. ```ts import { Unreal } from 'unreal-orm'; await Unreal.applySchema(db, [User, Post]); const ddl = Unreal.generateSchema([User, Post]); ``` For programmatic schema work the `Unreal.ast` object exposes parsing, extraction, comparison, and migration helpers. ```ts import { Unreal } from 'unreal-orm'; const ast = Unreal.ast.extractSchema([User, Post]); const changes = Unreal.ast.compareSchemas(localAst, remoteAst); ``` See [Migrations](../advanced/migrations/) for the full AST workflow. --- ## If you already know SurrealDB, this table shows where each concept lives in UnrealORM. Source: concepts/surrealql-mapping.mdx If you already know SurrealDB, this table shows where each concept lives in UnrealORM. ## Schema definitions | SurrealDB statement | UnrealORM API | |---|---| | `DEFINE TABLE user SCHEMAFULL` | `Table.normal({ name: 'user', schemafull: true, ... })` | | `DEFINE TABLE follows TYPE RELATION` | `Table.relation({ name: 'follow', ... })` | | `DEFINE TABLE adult_users AS SELECT ...` | `Table.view({ name: 'adult_users', as: ... })` | | `DEFINE FIELD name ON user TYPE string` | `name: Field.string()` | | `DEFINE FIELD tags ON post TYPE array` | `tags: Field.array(Field.string())` | | `DEFINE FIELD author ON post TYPE record` | `author: Field.record(() => User)` | | `DEFINE INDEX user_email ON user FIELDS email UNIQUE` | `Index.define(() => User, { ... })` | | `DEFINE BUCKET avatars AS MEMORY` | `Bucket.define({ name: 'avatars', backend: 'memory' })` | ## Data operations | SurrealDB operation | UnrealORM method | |---|---| | `CREATE user CONTENT { ... }` | `User.create(db, { ... })` | | `INSERT INTO user [{ ... }]` | `User.insert({ data: [{ ... }] })` | | `SELECT ... FROM user WHERE ...` | `User.select(db, { where: ..., select: ... })` | | `UPDATE user CONTENT/MERGE/...` | `User.update(db, id, { data: ..., mode: '...' })` | | `DELETE user WHERE ...` | `User.delete(db, { where: ... })` | | `RELATE user:a->follows->user:b` | `Follow.relate(db, { from: idA, to: idB })` | ## Expressions Most clauses that accept a `WHERE` or `assert` value can be a raw `surql` template or a type-safe field-proxy expression. ```ts // Raw SurrealQL User.select(db, { where: surql`email = ${email}` }) // Type-safe field proxy User.select(db, { where: (f) => f.email.eq(email), }) ``` The field proxy and the `surql` template are not mutually exclusive. Use the proxy for common comparisons and `surql` for anything the proxy does not yet express. --- ## :::tip Source: contributing/design-principles.md :::tip Follow these principles to ensure your usage and contributions align with the project's goals. ::: ## Core Philosophy UnrealORM is designed to provide a type-safe interface to SurrealDB while staying as close as possible to SurrealDB's native capabilities. Our goal is to enhance the developer experience through TypeScript types and builder patterns without abstracting away from SurrealDB's powerful features. ## Key Principles ### 1. Native First - **DO** expose SurrealDB's native features directly - **DO** use SurrealQL expressions for computations and mutations - **DON'T** create abstractions that hide or replace SurrealDB's native capabilities - **DON'T** add computed fields or transformations at the ORM level Example: ```typescript import { surql } from "surrealdb"; // GOOD: Using SurrealQL's native time::now() function const User = Table.normal({ createdAt: Field.datetime({ default: surql`time::now()` }), }); // BAD: Adding ORM-level computation const User = Table.normal({ createdAt: Field.datetime({ defaultNow: true }), // Don't add this kind of abstraction }); ``` ### 2. Type Safety Without Overhead - **DO** provide TypeScript types for all SurrealDB features - **DO** use type inference to improve developer experience - **DON'T** add runtime type checking or validation - **DON'T** create complex type hierarchies that don't map to SurrealDB concepts Example: ```typescript // GOOD: Types that directly map to SurrealDB concepts interface RecordLinkOptions { table: typeof Table; reference?: boolean; onDelete?: "cascade" | "restrict" | "no action"; } // BAD: Complex abstractions that don't map to SurrealDB interface ComputedFieldOptions { compute: (record: any) => any; // Don't add client-side computation } ``` ### 3. Query Building - **DO** allow direct use of SurrealQL in queries - **DO** provide type-safe parameters for queries - **DON'T** create a query builder that abstracts away SurrealQL - **DON'T** add ORM-specific query operations Example: ```typescript import { surql, gte } from "surrealdb"; // GOOD: Direct use of SurrealQL with type-safe parameters const adults = await User.select(db, { where: surql`age >= ${18}`, // or using expressions where: gte("age", 18), order: [{ field: "age", direction: "DESC" }], }); // BAD: ORM-specific query abstractions const adults = await User.where().ageGreaterThan(18).orderByAgeDesc().find(); ``` ### 4. Schema Definition - **DO** provide a direct mapping to SurrealDB's schema capabilities - **DO** expose all SurrealQL field types and options - **DON'T** add ORM-specific field types - **DON'T** create schema features that can't be represented in SurrealDB Example: ```typescript import { surql } from "surrealdb"; // GOOD: Direct mapping to SurrealDB field types and options const Product = Table.normal({ name: Field.string({ assert: surql`string::len($value) > 0`, value: surql`string::trim($value)`, }), price: Field.number({ assert: surql`$value >= 0`, }), }); // BAD: ORM-specific validations or transformations const Product = Table.normal({ name: Field.string({ transform: (value) => value.trim(), // Don't add client-side transforms validate: (value) => value.length > 0, // Don't add client-side validation }), }); ``` ### 5. Record Links and References - **DO** use SurrealDB's native record linking capabilities - **DO** support SurrealDB's reference tracking feature --- ## :::tip Source: contributing/guide.md :::tip Before contributing, please read our [Design Principles](/contributing/design-principles) to understand the project's approach and goals. ::: ## Code of Conduct Please be respectful and considerate of others when contributing to this project. We are committed to providing a welcoming and inclusive environment for everyone. ## Project Philosophy unreal-orm is designed to provide a type-safe interface to SurrealDB while staying as close as possible to SurrealDB's native capabilities. Key principles include: - **Native First**: Expose SurrealDB features directly, don't abstract them away - **Type Safety Without Overhead**: Use TypeScript for developer experience, not runtime checks - **Query Building**: Allow direct SurrealQL usage with type safety - **Schema Definition**: Direct mapping to SurrealDB's schema capabilities ## Development Environment Setup ### Prerequisites - [Node.js](https://nodejs.org/) (v18 or higher recommended) - [pnpm](https://pnpm.io/) (v8 or higher) - [Bun](https://bun.sh/) (for testing) - [SurrealDB](https://surrealdb.com/) (for integration tests) ### Installation 1. Fork and clone the repository ```bash git clone https://github.com/jimpex/unreal-orm.git cd unreal-orm ``` 2. Install dependencies ```bash pnpm install ``` 3. Build the project ```bash pnpm run build ``` 4. Run tests (bun required) ```bash pnpm run test ``` ## Project Structure unreal-orm is organized as a monorepo: ``` unreal-orm/ ├── apps/ # Applications │ └── docs/ # Documentation site (Astro + Starlight) ├── packages/ # Packages │ ├── unreal-orm/ # Main ORM library │ │ ├── src/ # Source code │ │ └── tests/ # Tests │ └── unreal-cli/ # CLI tools for schema management │ ├── src/ # CLI source code │ └── tests/ # CLI tests ``` ## Development Workflow 1. Create a new branch for your feature/fix ```bash git checkout -b feature/your-feature-name ``` 2. Make your changes and ensure tests pass ```bash pnpm run test ``` 3. Update documentation as needed 4. Commit your changes with a descriptive message ```bash git commit -m "feat: add new feature" ``` 5. Push to your fork and submit a pull request ## Pull Request Process 1. Ensure your PR addresses a specific issue or has a clear purpose 2. Update relevant documentation 3. Add or update tests as needed 4. Follow the coding guidelines 5. Make sure all tests pass 6. Request review from maintainers ### Commit Message Format We follow the [Conventional Commits](https://www.conventionalcommits.org/) specification: ``` (): [optional body] [optional footer(s)] ``` Common types: - `feat`: A new feature - `fix`: A bug fix - `docs`: Documentation changes - `style`: Code style changes (formatting, etc.) - `refactor`: Code changes that neither fix bugs nor add features - `test`: Adding or updating tests - `chore`: Changes to the build process or auxiliary tools ## Coding Guidelines ### Code Style - Use TypeScript for all source files - Follow the established code style (enforced by the project's linter) - Write clear, descriptive variable and function names - Use JSDoc comments for public APIs ### Type Safety - Prioritize type-safety and avoid `any` types when possible - Use TypeScript's type inference where appropriate - Document complex types with JSDoc comments ### API Design - Follow the project's [Design Principles](/contributing/design-principles) - Keep APIs consistent with existing patterns - Prioritize developer experience without abstractions ## Testing - All new features should include tests - Run tests with `pnpm run test` - Integration tests should use the embedded SurrealDB instance - Ensure tests are deterministic and don't rely on external services ## Documentation - Update documentation for any changed functionality - Document public APIs with JSDoc comments - Include examples for new features - Keep the [Capabilities](/getting-started/capabilities) document up to date --- Thank you for contributing to unreal-orm! --- ## :::tip Source: getting-started/capabilities.md :::tip This page summarizes which SurrealDB schema features are supported by unreal-orm. For hands-on examples see the [Quick Blog snippet](../readme/) and the [Tutorial](../guides/unreal-orm-tutorial/). ::: ## Quick Highlights | 💎 Feature | What it gives you | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | **Schema-only mode** | Generate SurrealQL with `generateFullSchemaQl()` without touching the database—perfect for migrations & CI. | | **Embedded testing** | Run SurrealDB fully in-process via `@surrealdb/node` engines (`mem://`) for ultra-fast unit tests. | | **Parameterized queries (`vars`)** | Prevent injection; supported in `select`, `count`, `query`, etc. | | **Type-safe `fetch` projection** | Automatic hydration of related records (`Field.record`) with full TypeScript types. | | **Circular dependency thunk** | Use `Field.record(() => OtherTable)` to avoid import cycles while keeping types. | | **Advanced data types** | Full, type-safe support for `bytes`, `duration`, `uuid`, `decimal`, and specific `geometry` types. | | **Custom Surreal types** | Define `Field.custom<'left' \| 'right'>('"left" \| "right"')` or any other database type for ultimate flexibility. | --- ## `DEFINE TABLE` Features | Feature | SurrealDB Syntax Example | `unreal-orm` Support | Notes | | ----------------------------------------- | ----------------------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------- | | Basic Table Definition | `DEFINE TABLE user;` | ✅ **Supported** | `class User extends Table.normal({ name: 'user', ... })` | | `IF NOT EXISTS` | `DEFINE TABLE user IF NOT EXISTS;` | ✅ **Supported** | `Table.normal({ ..., method: 'IF NOT EXISTS' })` | | `OVERWRITE` | `DEFINE TABLE user OVERWRITE;` | ✅ **Supported** | `Table.normal({ ..., method: 'OVERWRITE' })` | | `SCHEMAFULL` | `DEFINE TABLE user SCHEMAFULL;` | ✅ **Supported** | `Table.normal({ ..., schemafull: true })` is the standard. | | `SCHEMALESS` | `DEFINE TABLE user SCHEMALESS;` | ✅ **Supported** | `Table.normal({ ..., schemafull: false })` | | `TYPE NORMAL` | `DEFINE TABLE user TYPE NORMAL;` | ✅ **Supported** | This is the default for `Table.normal`. | | `TYPE ANY` | `DEFINE TABLE user TYPE ANY;` | ✅ **Supported** | Not directly supported, use `schemafull: false`. | | `TYPE RELATION IN ... OUT ...` | `DEFINE TABLE likes TYPE RELATION IN user OUT post;` | ✅ **Supported** | Use `Table.relation({ fields: { in: Field.record(...), out: Field.record(...) } })`. `ENFORCED` not supported. | | `ENFORCED` (for `TYPE RELATION`) | `DEFINE TABLE likes TYPE RELATION ... ENFORCED;` | ❌ **Not Supported** | Tied to full `TYPE RELATION` syntax. | | Table View (`AS SELECT ...`) | `DEFINE TABLE user_view AS SELECT ... FROM user;` | ✅ **Supported** | `class UserView extends Table.view({ name: 'user_view', as: surql'...' })`. | | `CHANGEFEED @duration [INCLUDE ORIGINAL]` | `DEFINE TABLE user CHANGEFEED 1h;` | ✅ **Supported** | `Table.normal({ ..., changefeed: { duration: '1h' } })`. | | `PERMISSIONS` (Table-level) | `DEFINE TABLE user PERMISSIONS FOR select WHERE ...;` | ✅ **Supported** | `Table.define({ ..., permissions: { select: '...' } })` | | `COMMENT @string` | `DEFINE TABLE user COMMENT 'User accounts';` | ✅ **Supported** | `Table.define({ ..., comment: '...' })` | | `DROP` (within `DEFINE TABLE`) | `DEFINE TABLE user DROP;` | ❌ **Not Supported** | `REMOVE TABLE` is a separate operation, not part of `Table.define`. | ## `DEFINE FIELD` Features | Feature | SurrealDB Syntax Example | `unreal-orm` Support | Notes | | --------------------------------------- | ---------------------------------------------------------------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------- | | Basic Field Definition | `DEFINE FIELD email ON user;` | ✅ **Supported** | Via `fields` object in `Table.define({ fields: { email: Field.string() } })`. | | `IF NOT EXISTS` (Field-level) | `DEFINE FIELD email ON user IF NOT EXISTS;` | ❌ **Not Supported** | ORM regenerates the whole schema. | | `OVERWRITE` (Field-level) | `DEFINE FIELD email ON user OVERWRITE;` | ❌ **Not Supported** | ORM regenerates the whole schema. | | `TYPE @type` (various types) | `DEFINE FIELD age ON user TYPE number;` | ⚠️ **Partially** | See "Data Type Support" section below for detailed coverage. | | `FLEXIBLE TYPE @type` | `DEFINE FIELD meta ON user FLEXIBLE TYPE object;` | ✅ **Supported** | Use `{ flexible: true }` with `Field.object()` or `Field.custom()`. Enables flexible schemas for object/custom fields. | | `DEFAULT @expression` | `DEFINE FIELD role ON user TYPE string DEFAULT 'guest';` | ✅ **Supported** | `Field.string({ default: surql`"guest"` })`. Handles primitives, `time::now()`, functions expressed as SurrealQL. | | `DEFAULT ALWAYS @expression` | `DEFINE FIELD updated_at ON user TYPE datetime DEFAULT ALWAYS time::now();` | ❌ **Not Supported** | Only standard `DEFAULT` is supported. | | `VALUE @expression` | `DEFINE FIELD created_at ON user VALUE time::now();` | ✅ **Supported** | `Field.string({ value: surql`time::now()` })` or e.g. `Field.string({ value: surql`string::lowercase($value)` })` | | `VALUE { @expression }` | `DEFINE FIELD last_accessed ON user VALUE { time::now() };` | ❌ **Not Supported** | Only standard VALUE is supported, not VALUE . | | `ASSERT @expression` | `DEFINE FIELD email ON user ASSERT string::is::email($value);` | ✅ **Supported** | `Field.string({ assert: surql`string::is::email($value)` })` | | `READONLY` | `DEFINE FIELD id ON user READONLY;` | ✅ **Supported** | `Field.string({ readonly: true })` | | `PERMISSIONS` (Field-level) | `DEFINE FIELD email ON user PERMISSIONS FOR select WHERE...;` | ✅ **Supported** | `Field.string({ permissions: { ... } })` | | `COMMENT @string` | `DEFINE FIELD email ON user COMMENT 'User email';` | ✅ **Supported** | `Field.string({ comment: '...' })` | | `REFERENCE` (with `ON DELETE` actions) | `DEFINE FIELD author ON post TYPE record REFERENCE ON DELETE CASCADE;` | ✅ **Supported** | `Field.record(() => User, { reference: { onDelete: 'CASCADE' } })`. (SurrealDB v2.2+) | | Define type for `id` field | `DEFINE FIELD id ON user TYPE string;` | ✅ **Supported** | `Table.define({ fields: { id: Field.custom({ type: 'string' }) } })` | | Define types for specific array indices | `DEFINE FIELD data[0] ON mytable TYPE string;` | ❌ **Not Supported** | `Field.array(Field.string())` defines type for all items. | ## `DEFINE INDEX` Features | Feature | SurrealDB Syntax Example | `unreal-orm` Support | Notes | | ------------------------------------ | ------------------------------------------------------- | -------------------- | ---------------------------------------------------------------------------------------- | | Basic Index Definition | `DEFINE INDEX user_email ON user COLUMNS email;` | ✅ **Supported** | `Table.define({ indexes: { user_email: { fields: ['email'] } } })` | | `IF NOT EXISTS` (Index-level) | `DEFINE INDEX user_email ON user IF NOT EXISTS ...;` | ❌ **Not Supported** | ORM regenerates the whole schema. | | `OVERWRITE` (Index-level) | `DEFINE INDEX user_email ON user OVERWRITE ...;` | ❌ **Not Supported** | ORM regenerates the whole schema. | | `UNIQUE` Index | `DEFINE INDEX user_email ON user COLUMNS email UNIQUE;` | ✅ **Supported** | `Index.define(() => User, { fields: ['email'], unique: true })` | | `SEARCH ANALYZER` (Full-Text Search) | `DEFINE INDEX ... SEARCH ANALYZER ... BM25 HIGHLIGHTS;` | ✅ **Supported** | Use the `analyzer` option in `Index.define`. Other keywords like `BM25` are not exposed. | | Vector Index (`MTREE`, `HNSW`) | `DEFINE INDEX ... MTREE DIMENSION ...;` | ❌ **Not Supported** | | | `COMMENT @string` | `DEFINE INDEX user_email ON user ... COMMENT '...';` | ✅ **Supported** | Use the `comment` option in `Index.define`. | | `CONCURRENTLY` | `DEFINE INDEX user_email ON user ... CONCURRENTLY;` | ❌ **Not Supported** | | ## Other `DEFINE` Statements `unreal-orm` primarily focuses on schema generation for tables, fields, and indexes. Other `DEFINE` statements are not yet supported by the ORM. | Feature | SurrealDB Syntax Example | `unreal-orm` Support | Notes | | ------------------ | -------------------------------- | -------------------- | ----------------------------------------- | | `DEFINE NAMESPACE` | `DEFINE NAMESPACE test;` | ❌ **Not Supported** | Operates within a given DB/NS connection. | | `DEFINE DATABASE` | `DEFINE DATABASE test;` | ❌ **Not Supported** | | | `DEFINE USER` | `DEFINE USER ...;` | ❌ **Not Supported** | | | `DEFINE TOKEN` | `DEFINE TOKEN ...;` | ❌ **Not Supported** | | | `DEFINE SCOPE` | `DEFINE SCOPE ...;` | ❌ **Not Supported** | | | `DEFINE ANALYZER` | `DEFINE ANALYZER ...;` | ❌ **Not Supported** | | | `DEFINE EVENT` | `DEFINE EVENT ... ON TABLE ...;` | ❌ **Not Supported** | | | `DEFINE FUNCTION` | `DEFINE FUNCTION fn::abc() ...;` | ❌ **Not Supported** | | | `DEFINE PARAM` | `DEFINE PARAM $myparam ...;` | ❌ **Not Supported** | | ## Data Type Support ### Field Definition Examples ```ts import { surql } from "surrealdb"; import { Table, Field } from "unreal-orm"; class User extends Table.normal({ name: "user", schemafull: true, fields: { name: Field.string({ assert: surql`$value.length > 2` }), age: Field.number({ assert: surql`$value >= 0`, default: surql`0` }), isActive: Field.bool({ default: surql`true` }), createdAt: Field.datetime({ default: surql`time::now()` }), profile: Field.object({ bio: Field.string(), website: Field.option(Field.string()), }), tags: Field.array(Field.string(), { max: 10 }), posts: Field.array(Field.record(() => Post)), nickname: Field.option(Field.string()), // Advanced Types balance: Field.decimal(), apiKey: Field.uuid(), lastLogin: Field.duration(), avatar: Field.bytes(), location: Field.geometry("point"), side: Field.custom<"left" | "right">('"left" | "right"'), }, }) {} // Define indexes separately const UserNameIndex = Index.define(() => User, { name: "user_name_idx", fields: ["name"], unique: true, }); ``` --- ## You need three things: a running SurrealDB instance, the `unreal-orm` package in your app, and the `@unreal-orm/cli` package for schema workflows. Source: getting-started/installation.mdx You need three things: a running SurrealDB instance, the `unreal-orm` package in your app, and the `@unreal-orm/cli` package for schema workflows. ## 1. Run SurrealDB For local development the easiest option is Docker: ```bash docker run --rm -p 8000:8000 surrealdb/surrealdb:latest start ``` For in-memory unit tests you can also use the embedded engine: ```bash bun add surrealdb @surrealdb/node ``` ```ts import { Surreal } from 'surrealdb'; import { createNodeEngines } from '@surrealdb/node'; const db = new Surreal({ engines: { ...createNodeEngines() } }); await db.connect('mem://'); ``` ## 2. Add UnrealORM to your project ```bash bun add surrealdb unreal-orm bun add -D @unreal-orm/cli ``` `unreal-orm` is the runtime library. `@unreal-orm/cli` provides the `unreal` command-line tool. ## 3. Initialize a project ```bash bunx unreal init ``` This creates: ``` my-project/ ├── unreal.config.json ├── unreal/ │ ├── surreal.ts # connection setup │ └── tables/ # your model files ``` `unreal.config.json` only points at the `unreal/` folder. Connection details live in `unreal/surreal.ts` so you can import them in your app and tests. ## 4. Configure the connection Edit `unreal/surreal.ts`: ```ts import { Surreal } from 'surrealdb'; const db = new Surreal(); export async function connect() { await db.connect('ws://localhost:8000', { namespace: 'test', database: 'test', authentication: { username: 'root', password: 'root' }, }); return db; } export default db; ``` > **Important**: Pass `namespace`, `database`, and `authentication` directly to `.connect()`. If you use separate `.use()` and `.signin()` calls, the SDK will not automatically re-authenticate or refresh tokens on reconnect. ## 5. Apply your first schema After you add model files under `unreal/tables/`, push them to the database: ```bash bunx unreal push ``` Use `bunx unreal pull` to generate TypeScript models from an existing database, and `bunx unreal diff` to compare the two. ## Next steps - [Quickstart](./quickstart/) — build a minimal blog schema. - [Capabilities](./capabilities/) — full feature matrix. --- ## This guide creates a tiny blog schema: `User`, `Post`, and a `Follow` relation. Source: getting-started/quickstart.mdx This guide creates a tiny blog schema: `User`, `Post`, and a `Follow` relation. ## 1. Define the tables `unreal/tables/User.ts` ```ts import { Table, Field, Index } from 'unreal-orm'; import { surql } from 'surrealdb'; export class User extends Table.normal({ name: 'user', schemafull: true, fields: { name: Field.string(), email: Field.string({ assert: surql`string::is::email($value)` }), createdAt: Field.datetime({ default: surql`time::now()`, readonly: true }), }, }) { getDisplayName() { return `${this.name} <${this.email}>`; } } export const UserEmailIndex = Index.define(() => User, { name: 'user_email_idx', fields: ['email'], unique: true, }); export const UserDefinitions = [User, UserEmailIndex]; ``` `unreal/tables/Post.ts` ```ts import { Table, Field } from 'unreal-orm'; import { surql } from 'surrealdb'; import { User } from './User'; export class Post extends Table.normal({ name: 'post', schemafull: true, fields: { title: Field.string(), content: Field.string(), author: Field.record(() => User), publishedAt: Field.datetime({ default: surql`time::now()` }), }, }) {} // Optionally make a field optional with Field.option() // summary: Field.option(Field.string()), ``` `unreal/tables/Follow.ts` ```ts import { Table, Field } from 'unreal-orm'; import { surql } from 'surrealdb'; import { User } from './User'; export class Follow extends Table.relation({ name: 'follow', schemafull: true, fields: { in: Field.record(() => User), out: Field.record(() => User), since: Field.datetime({ default: surql`time::now()` }), }, }) {} ``` `unreal/tables/index.ts` ```ts export { User, UserDefinitions } from './User'; export { Post } from './Post'; export { Follow } from './Follow'; ``` ## 2. Aggregate and apply the schema `unreal/surreal.ts` exposes a `connect` function. Apply the schema once on startup: ```ts import { applySchema } from 'unreal-orm'; import { connect } from './surreal'; import * as models from './tables'; const db = await connect(); await applySchema(db, [models.User, models.UserEmailIndex, models.Post, models.Follow]); ``` ## 3. Create and query records ```ts import { RecordId } from 'surrealdb'; import { db } from './surreal'; import { User, Post, Follow } from './tables'; const alice = await User.create(db, { name: 'Alice', email: 'alice@example.com' }); const bob = await User.create(db, { name: 'Bob', email: 'bob@example.com' }); const post = await Post.create(db, { title: 'Hello world', content: 'My first post', author: alice.id, }); await Follow.relate(db, { from: alice.id, to: bob.id }); const posts = await Post.select(db, { select: { title: true, author: { name: true, email: true } }, fetch: ['author'], }); ``` ## 4. Use the CLI ```bash # Push the schema bunx unreal push # Pull an existing database back into models bunx unreal pull # See what changed bunx unreal diff ``` ## What next? - [Models and Tables](../concepts/models-and-tables/) — how `Table.normal`, `Table.relation`, and `Table.view` work. - [Fields and Types](../concepts/fields-and-types/) — every field builder. --- ##

Source: getting-started/readme.md

Unreal ORM Logo

[![GitHub Stars](https://img.shields.io/github/stars/Jimpex/unreal-orm?style=social)](https://github.com/Jimpex/unreal-orm) [![npm version](https://badge.fury.io/js/unreal-orm.svg)](https://www.npmjs.com/package/unreal-orm) [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC) [![npm downloads](https://img.shields.io/npm/dm/unreal-orm)](https://www.npmjs.com/package/unreal-orm) ![TypeScript](https://img.shields.io/badge/TypeScript-blue?logo=typescript)

# UnrealORM A modern, type-safe ORM for SurrealDB. Native SurrealDB power, full TypeScript safety, zero abstraction—no decorators, no magic, just classes and functions. UnrealORM builds on top of the official `surrealdb` package, providing a TypeScript ORM experience while preserving full access to SurrealDB's native features. Define your schema once in code, and the ORM handles type inference, DDL generation, and schema synchronization. > **Note:** UnrealORM 1.0.0-alpha.x requires SurrealDB's 2.0 (alpha) JS SDK. If you're using 1.x of their SDK, install [unreal-orm@0.6.0](https://www.npmjs.com/package/unreal-orm/v/0.6.0) instead. To upgrade, see the [Migration Guide](https://unreal-orm.jimpex.dev/guides/migrating-to-alpha). ## Quick Start ```bash bunx @unreal-orm/cli init # Or with other package managers npx @unreal-orm/cli init pnpm dlx @unreal-orm/cli init yarn dlx @unreal-orm/cli init ``` This will: - Set up your project structure (`unreal/` folder) - Configure database connection (`surreal.ts`) - Install dependencies (`unreal-orm`, `surrealdb`, `@unreal-orm/cli`) - Optionally generate sample tables or import from existing database
Manual installation ```bash # Using bun bun add unreal-orm@latest surrealdb@latest bun add -D @unreal-orm/cli@latest # Using pnpm pnpm add unreal-orm@latest surrealdb@latest pnpm add -D @unreal-orm/cli@latest # Using npm npm install unreal-orm@latest surrealdb@latest npm install -D @unreal-orm/cli@latest # Using yarn yarn add unreal-orm@latest surrealdb@latest yarn add -D @unreal-orm/cli@latest ```
## Features - **Type-safe models** — Define tables as classes with full TypeScript inference for fields, queries, and results - **Schema sync** — Generate DDL from code with `applySchema()`, or generate code from database with `unreal pull` - **Relations** — Typed record links with automatic hydration via `fetch` - **Native SurrealQL & query builder** — Use `surql` templates and functional expressions directly, or filter with typed field proxies in `select`, `count`, `updateMany`, and `deleteMany` - **Indexes** — Define unique, composite, and search indexes with full type safety - **Custom methods** — Add instance and static methods to your models - **CLI tools** — `init`, `pull`, `push`, `diff`, `mermaid` for schema management ## Example ```ts import { Surreal, surql } from "surrealdb"; import { Table, Field, Index, Unreal } from "unreal-orm"; // Define a User model with validation and custom methods class User extends Table.normal({ name: "user", fields: { name: Field.string(), email: Field.string({ assert: surql`$value CONTAINS "@"` }), createdAt: Field.datetime({ default: surql`time::now()` }), }, }) { getDisplayName() { return `${this.name} <${this.email}>`; } } // Define a unique index const idx_user_email = Index.define(() => User, { name: "idx_user_email", fields: ["email"], unique: true, }); // Define a Post with a relation to User class Post extends Table.normal({ name: "post", fields: { title: Field.string(), content: Field.string(), author: Field.record(() => User), }, }) {} async function main() { const db = new Surreal(); await db.connect("ws://localhost:8000", { namespace: "test", database: "test", authentication: { username: "root", password: "root" }, }); // Apply schema to database await Unreal.applySchema(db, [User, idx_user_email, Post]); // Create records const user = await User.create(db, { name: "Alice", email: "alice@example.com", }); const post = await Post.create(db, { title: "Hello", content: "World", author: user.id, }); // Query with hydrated relations const result = await Post.select(db, { from: post.id, only: true, fetch: ["author"], }); console.log(result.author.getDisplayName()); // "Alice " // Update with explicit mode await user.update(db, { data: { name: "Alice Smith" }, mode: "merge" }); await db.close(); } ``` See the [Hands-on Tutorial](https://unreal-orm.jimpex.dev/guides/unreal-orm-tutorial) for a complete walkthrough building a blog API with users, posts, comments, and relations. ### Type-Safe Select Select specific fields with full type inference: ```ts import { typed } from "unreal-orm"; import { surql } from "surrealdb"; // Nested object fields - types inferred from objectSchema const posts = await Post.select(db, { select: { title: true, metadata: { category: true } }, }); // Type: { title: string; metadata: { category: string } }[] // Nested record fields - types inferred from linked table const posts = await Post.select(db, { select: { title: true, author: { name: true, email: true } }, }); // Type: { title: string; author: { name: string; email: string } }[] // Computed fields with typed() helper const posts = await Post.select(db, { select: { title: true, commentCount: typed(surql`count(<-comment)`) }, }); // Type: { title: string; commentCount: number }[] // Type-safe omit - exclude fields from result const users = await User.select(db, { omit: { password: true, secret: true }, }); // Type: Omit[] ``` ### Type-Safe Query Builder Use the callback-based `where` API for fully typed filters with IntelliSense: ```ts import { and, or, eq, gt } from "unreal-orm"; // SELECT with typed filters const posts = await Post.select(db, { where: (f) => f.views.gt(100), }); // Use SurrealDB string/array/date functions on columns const recent = await Post.select(db, { where: (f) => f.title.toLowerCase().contains("surreal"), }); // Native function namespaces cover the entire SurrealDB function surface const filtered = await Post.select(db, { where: (f) => and( f.title.string.starts_with("hello"), f.views.math.round().eq(100), ), }); // Compose logical expressions const featuredTech = await Post.select(db, { where: (f) => and( eq(f.metadata.category, "tech"), or(f.metadata.featured.eq(true), gt(f.views, 1000)), ), }); // COUNT, UPDATE, and DELETE also accept callbacks const popular = await Post.count(db, { where: (f) => f.views.gt(100), }); await Post.updateMany(db, { where: (f) => f.metadata.category.eq("tech"), data: { views: 0 }, mode: "merge", }); await Post.deleteMany(db, { where: (f) => f.tags.contains("deprecated"), }); ``` Available operators include `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `exact` (`==`), `isNone`, `isNull`, `isTrue`, `isFalse`, `inside`, `outside`, `intersects`, `matches` (full-text `@@`), `isIn`, `isNotIn`, `containsAny`, `containsAll`, `containsNone`, plus logical helpers `and`, `or`, `not` (all available as both column methods and standalone helpers). **Arithmetic operators** (`add`, `subtract`, `multiply`, `divide`, `modulo`) return `ColumnRef` for chaining: ```ts // WHERE views + 10 > 100 const trending = await Post.select(db, { where: (f) => f.views.add(10).gt(100), }); ``` **Graph traversal** (`out`, `in`, `both`) for `->`, `<-`, `<->` operators: ```ts // WHERE author->follow->user.name = "Alice" const posts = await Post.select(db, { where: (f) => f.author.out("follow").out("user").name.eq("Alice"), }); ``` Columns expose **all** native SurrealDB function namespaces: `string`, `math`, `array`, `time`, `is`, `meta`, `parse`, `bytes`, `crypto`, `duration`, `encoding`, `geo`, `http`, `object`, `rand`, `record`, `search`, `session`, `type`, `vector`. Any built-in function can be called, e.g. `f.title.string.lowercase()`, `f.views.math.round(2)`, `f.password.crypto.sha256()`, `f.location.geo.distance(other)`. Nested functions with `::` use bracket notation: `f.title.string['similarity::jaro']()`. Runtime availability depends on the connected SurrealDB version; full-text `matches()` requires a `SEARCH` index, `search::*` and `vector::*` require SurrealDB 2.x+, and regex operators such as `~` and `!~` were removed in SurrealDB 3.x. ## CLI The CLI helps manage schema synchronization between your code and database: ```bash unreal init # Initialize project with connection and sample tables unreal pull # Generate TypeScript models from database schema unreal push # Apply TypeScript schema to database unreal diff # Compare code vs database schema unreal mermaid # Generate ERD diagram unreal view # Interactive TUI for browsing/editing records unreal docs # Open the UnrealORM documentation unreal github # Open the UnrealORM GitHub repository ``` After `init`, the CLI is installed as a dev dependency and can be run via `bunx unreal` or `npx unreal`. ## Documentation - [Getting Started](https://unreal-orm.jimpex.dev/getting-started/readme/) — Installation and setup - [Hands-on Tutorial](https://unreal-orm.jimpex.dev/guides/unreal-orm-tutorial) — Build a blog API step-by-step - [Capabilities](https://unreal-orm.jimpex.dev/getting-started/capabilities/) — Supported SurrealDB features - [API Reference](https://unreal-orm.jimpex.dev/api/) — Full API documentation - [Migration Guide](https://unreal-orm.jimpex.dev/guides/migrating-to-alpha) — Upgrading from 0.x ## Community - 💬 [GitHub Discussions](https://github.com/Jimpex/unreal-orm/discussions) — Questions & ideas - 🐛 [Issues](https://github.com/Jimpex/unreal-orm/issues) — Bug reports - 🤝 [Contributing](https://unreal-orm.jimpex.dev/contributing/guide/) — How to contribute - ⭐ [Star on GitHub](https://github.com/jimpex/unreal-orm) — Show support - ☕ [Ko-fi](https://ko-fi.com/jimpex) — Buy me a coffee ## Author UnrealORM is created and maintained by [Jimpex](https://jimpex.dev/). ## License [ISC License](LICENSE) --- ## UnrealORM is a type-safe TypeScript ORM for [SurrealDB](https://surrealdb.com). Before you write your first model, it helps to know a few SurrealDB ideas that the ORM is built on. Source: getting-started/what-is-surrealdb.mdx UnrealORM is a type-safe TypeScript ORM for [SurrealDB](https://surrealdb.com). Before you write your first model, it helps to know a few SurrealDB ideas that the ORM is built on. ## Multi-model, not multi-database SurrealDB stores records as documents, but also treats relationships as first-class graph edges. You can query across documents and graph links in a single statement. UnrealORM maps this to normal TypeScript classes and relation classes. ## Schemafull and schemaless SurrealDB tables can be: - **Schemaless** — records can have any fields. - **Schemafull** — only fields defined in `DEFINE FIELD` are allowed. UnrealORM defaults to schemafull tables because its main value is type-safe schema definitions. ## Record IDs Every record has a unique `id` such as `user:ic7c1frczl1tw552yl4u` or `user:john`. In TypeScript these are represented as `RecordId<'user'>` instances from the `surrealdb` SDK. ```ts import { RecordId } from 'surrealdb'; const id: RecordId<'user'> = new RecordId('user', 'john'); ``` ## Record links and graph edges - A **record link** is a field that holds a `RecordId` pointing to another table, e.g. `record`. - A **graph edge** (or relation) is a record in an edge table with mandatory `in` and `out` fields. It is created with `RELATE user:john->follows->user:jane`. UnrealORM models both with `Field.record(...)` and `Table.relation(...)`. ## SurrealQL SurrealDB is queried with SurrealQL, a SQL-like language that also supports graph traversal and nested documents. UnrealORM generates SurrealQL from your TypeScript definitions and lets you drop back to raw `surql` templates whenever you need to. ```ts import { surql } from 'surrealdb'; const query = surql`SELECT * FROM user WHERE email = $email`; ``` ## Why an ORM? SurrealQL is powerful but string-based. UnrealORM gives you: - Type-safe model definitions that generate `DEFINE TABLE`, `DEFINE FIELD`, and `DEFINE INDEX` statements. - Type-safe `SELECT`, `INSERT`, `UPDATE`, and `COUNT` helpers. - Type-safe field projections and query builders. - A clear path to raw SurrealQL when the ORM does not cover a feature. The next page walks through installing SurrealDB and the UnrealORM tooling. --- ## import { Tabs, TabItem } from '@astrojs/starlight/components'; Source: guides/ai-usage.mdx import { Tabs, TabItem } from '@astrojs/starlight/components'; Unreal ORM's **Native First** design means AI tools can understand and generate code for it with minimal friction. Because it stays close to SurrealQL and avoids complex abstractions, models trained on SurrealDB documentation work well out of the box. This guide covers how to get the most out of AI when working with Unreal ORM. ## LLM Context Files We publish two files specifically designed for AI tools: - **[`/llms.txt`](/llms.txt)** — A concise index of all documentation guides and API references, ideal for AI crawlers and documentation discovery tools. - **[`/llms-full.txt`](/llms-full.txt)** — The entire documentation in a single Markdown file. Attach this to your AI chat or upload it to your IDE to give the model complete, up-to-date knowledge of the ORM. Using `llms-full.txt` is particularly effective when starting a new project or asking the AI to design a schema, as it prevents the model from hallucinating method names borrowed from other ORMs like Prisma or Drizzle. --- ## Setting Up Your AI Editor **Add Unreal ORM docs**: Go to "Add new Doc" and provide `https://unreal-orm.jimpex.dev/llms.txt`. Once indexed, use `@unreal-orm` in any prompt to ground suggestions in the actual API. **Add project rules** via `.cursorrules` at the project root or `.cursor/rules/*.md`: ``` - Define all tables using Table.normal or Table.relation. Never write raw SurrealQL strings for schema definitions. - Use type-safe select and create methods. Avoid the `any` type for query results. - Always use the surql template literal for custom filters and WHERE clauses to ensure parameter binding. ``` **Context Pinning**: Unreal ORM schemas often span multiple files (e.g., a `Table.normal` in one file and its `Table.relation` in another). Pin both files to the Cascade context so the agent maintains an accurate picture of your graph structure. **Cascade Hooks**: Set up a `pre_write_code` hook to run `unreal diff` before any schema file is written. If the proposed change would break the current database state, the hook returns the error directly to Cascade for self-correction. Add global rules via `global_rules.md`: ``` - Always use Table.normal or Table.relation for schema definitions. - Use the surql tag for all custom query logic. ``` Add `https://unreal-orm.jimpex.dev/llms.txt` as a documentation source in Copilot settings, or include `llms-full.txt` as a context file in your workspace. When using Copilot Chat, reference your schema files directly with `#file` to help it suggest correct field names and query options. --- ## Schema-First Workflow The most effective pattern when using AI with Unreal ORM is to **lock in the schema before writing queries**. 1. **Give the AI your requirements** and ask it to generate `Table.normal` and `Table.relation` definitions. If you have an existing SurrealDB database, run `unreal pull` first and share the output. 2. **Review the schema** before moving on. Ask the AI to check for missing indexes or incorrect relation directions. 3. **Generate implementation code** referencing the finalized schema. Prompt specifically: _"Use type-safe `select` and `create` methods. Use the `surql` template literal for any custom filtering."_ This sequence reduces architectural drift — the AI is less likely to invent fields or methods if it's working from an explicit definition. --- ## Providing Schema as Context Always share your table definitions when asking the AI for help with queries. This is the single biggest factor in output quality: ```typescript // Share this with the AI when asking for query help const User = Table.normal({ name: 'user', fields: { username: Field.string(), email: Field.string({ assert: surql`string::is::email($value)`, }), }, }); const Writes = Table.relation({ name: 'writes', in: User, out: Post, fields: { timestamp: Field.datetime({ default: surql`time::now()` }), }, }); ``` Field constraints (like `assert`) are especially helpful — they tell the AI what values are valid, making it less likely to generate logic that violates your business rules. --- ## The `surql` Tag Always instruct the AI to use the `surql` template literal for custom query logic. This is important for two reasons: 1. **Security** — variables are passed as bound parameters, not interpolated into the query string. 2. **Type inference** — the query builder maintains return type inference through `surql` expressions. If the AI generates raw string queries, correct it: ```typescript // ❌ What AI tools sometimes suggest const query = `SELECT * FROM user WHERE email = '${email}'`; // ✅ What you should prompt for instead const users = await User.select(db, { where: surql`email = ${email}`, }); ``` --- ## Using the CLI in Your AI Workflow The `unreal-cli` tools are useful checkpoints when working with an AI agent: | Command | When to use it | |---|---| | `unreal pull` | Before starting a session on an existing project — generates ORM definitions from the live database | | `unreal diff` | After the AI proposes a schema change — shows what would actually change in the database | | `unreal push` | After reviewing the diff and confirming the migration is safe | A useful prompt after the AI updates your schema: > _"Run `unreal diff` and show me what would change in the database."_ This gives you a concrete SQL diff to review before committing anything. --- ## Tips for Avoiding Hallucinations - **Use `llms-full.txt`** when the AI invents methods (e.g., a `.where()` from Drizzle). Attach the file and ask it to _"verify the available query options according to the provided documentation"_. - **Pin the version**: Tell the AI which version of Unreal ORM you're on. It may otherwise suggest APIs that don't exist yet or have been changed. - **Commit frequently**: After the AI produces a working schema or query, commit it. This creates a clean baseline you can always return to if the conversation drifts. --- ## Need a quick pattern without reading the whole tutorial? Grab a ready-made recipe below 👇 Source: guides/cookbook.mdx Need a quick pattern without reading the whole tutorial? Grab a ready-made recipe below 👇 > All snippets assume `db` is an active `Surreal` connection and models are already defined. ## Pagination with `start`, `limit`, and `orderBy` ```ts // Fetch the next page of posts ordered by newest first const pageSize = 10; const posts = await Post.select(db, { orderBy: [{ field: 'createdAt', order: 'desc' }], start: 20, // skip first 2 pages limit: pageSize, }); ``` ## Soft Delete (`isDeleted` flag) ```ts class Post extends Table.normal({ name: 'post', fields: { title: Field.string(), content: Field.string(), isDeleted: Field.bool({ default: surql`false` }), }, }) { // Convenience helpers async softDelete(db: Surreal) { return this.update(db, { data: { isDeleted: true }, mode: 'merge', }); } static async allActive(db: Surreal) { return this.select(db, { where: surql`isDeleted = false` }); } } ``` ## Enum Helper ```ts // Authorised values enforced in SurrealDB export const Roles = ['admin', 'editor', 'viewer'] as const; export type Role = (typeof Roles)[number]; class User extends Table.normal({ name: 'user', fields: { role: Field.custom('string', { assert: surql`$value INSIDE ["admin", "editor", "viewer"]`, }), }, }) {} ``` ## Patch Update with `merge` ```ts // Partially update fields without replacing the whole record await Post.update(db, 'post:123', { data: { content: 'Updated body' }, mode: 'merge', }); ``` ## Parameterised Full-Text Search ```ts await Post.select(db, { where: surql`title ~~ $q OR content ~~ $q`, vars: { q: '*orm*' }, }); ``` ## Type-Safe Select (Field Projection) Select specific fields with full type inference: ```ts import { typed } from 'unreal-orm'; // Select specific fields - return type is inferred const posts = await Post.select(db, { select: { title: true, author: { name: true, email: true } }, }); // Type: { title: string; author: { name: string; email: string } }[] // Computed fields with typed() helper const posts = await Post.select(db, { select: { title: true, commentCount: typed(surql`count(<-comment)`) }, }); // Type: { title: string; commentCount: number }[] // Nested relation fetch by overriding the wildcard default const latest = await Post.select(db, { select: { '*': true, author: { name: true, email: true } } }); // Type: { title: string; content: string; views: number; metadata: ...; author: { name: string; email: string } }[] // Native ID string parsing const onlyIDs = await Post.select(db, { select: { id: true, title: true } }); // Type: { id: RecordId<"post">; title: string }[] // Type-safe omit - exclude fields from result const users = await User.select(db, { omit: { password: true }, }); // Type: Omit[] ``` ## DRY & Reusable Definitions Keep your models clean by abstracting common field patterns and logic. ### 🧱 Field Helper Functions Instead of repeating complex validation, wrap field definitions in functions. ```ts // Reusable email field with consistent validation export const EmailField = (options = {}) => Field.string({ assert: surql`string::is::email($value)`, comment: 'User email address', ...options }); // Usage in models class User extends Table.normal({ name: 'user', fields: { email: EmailField({ unique: true }), } }) {} ``` ### 🏗️ Base Models & Shared Methods You can use TypeScript inheritance to share logic across models. ```ts class BaseModel extends Table.normal({ ... }) { // Shared logic for all models extending this class async archive(db: Surreal) { return this.update(db, { data: { archived: true }, mode: 'merge' }); } } class Post extends BaseModel { // Post inherits .archive() } ``` ### 📋 Shared Fields (Spread Pattern) Need the same timestamps on every table? Use the spread operator. ```ts const TIMESTAMPS = { createdAt: Field.datetime({ default: surql`time::now()`, readonly: true }), updatedAt: Field.datetime({ value: surql`time::now()` }), }; class Post extends Table.normal({ name: 'post', fields: { title: Field.string(), ...TIMESTAMPS } }) {} ``` --- ## 🚀 Deep Dives Looking for more than just a snippet? Check out our in-depth guides: - [**Mastering Graph Relations**](./graph-relations/) — Many-to-many and edge properties. - [**Security & Permissions**](../advanced/permissions/) — Row-level security patterns. - [**Testing Strategies**](../advanced/testing/) — Fast in-memory unit testing. - [**Common Patterns**](./patterns/) — Architectural patterns like singletons and hierarchies. - [**Migrations & Schema Sync**](./migrations/) — CLI usage and CI/CD integration. 📖 Continue exploring the [Tutorial](./unreal-orm-tutorial/) or deep-dive into the [API Reference](../api/). --- ## SurrealDB is a Multi-Model database, meaning it thrives as both a Document and a **Graph** database. While simple `Field.record()` links are great for one-to-many relations, **Relation Tables** (Edges) allow for powerful many-to-many structures with their own properties. Source: guides/graph-relations.mdx SurrealDB is a Multi-Model database, meaning it thrives as both a Document and a **Graph** database. While simple `Field.record()` links are great for one-to-many relations, **Relation Tables** (Edges) allow for powerful many-to-many structures with their own properties. ## 🔗 Relation Tables (Edges) In Unreal ORM, relation tables are defined using `Table.relation()`. They require two special fields: `in` and `out`. ### Defining an Edge Suppose we have `User` and `Project` models. We want to track which users are "members" of which projects, and include their "role" on that specific project. ```ts import { Table, Field } from 'unreal-orm'; import { surql } from 'surrealdb'; class MemberOf extends Table.relation({ name: 'member_of', schemafull: true, fields: { // The source (User) in: Field.record(() => User), // The target (Project) out: Field.record(() => Project), // Edge property role: Field.string({ default: surql`"contributor"` }), joinedAt: Field.datetime({ default: surql`time::now()` }), }, }) {} ``` ### Creating an Edge Use the dedicated `.relate()` method on relation tables to create edges: ```ts const membership = await MemberOf.relate(db, { from: userId, to: projectId, data: { role: 'admin' }, }); ``` --- ## 🚀 Traversing Relations One of the main benefits of using edges is the ability to traverse them easily in both directions. ### Using `fetch` for hydration You can hydrate the `in` or `out` records automatically during a select: ```ts const memberships = await MemberOf.select(db, { where: surql`out = ${projectId}`, fetch: ['in'], // Hydrate the User record }); console.log(memberships[0].in.name); // "Alice" ``` ### 🚀 Path Traversal in `FROM` SurrealDB allows you to query directly from a relationship path. This is often **faster and more efficient** than using a `WHERE` clause because it uses graph indices to jump directly between records. In Unreal ORM, you can modify the `from` option in `select` to use a graph path. ```ts // Find all projects that Alice is a member of // This traverses: User (Alice) -> MemberOf Edge -> Project const aliceProjects = await Project.select(db, { from: surql`user:alice->member_of->project`, }); ``` --- ## 🔝 Advanced Projections with `typed()` When selecting related data, you can use the `typed()` helper to ensure the results are correctly typed in TypeScript. ### Projecting Relation Data You can project data across relations directly in the `select` block. ```ts import { typed } from 'unreal-orm'; const projects = await Project.select(db, { select: { name: true, // Project the count of members using a graph expression memberCount: typed(surql`count(<-member_of)`), // Fetch the names of all members memberNames: typed(surql`<-member_of<-user.name`), }, }); // projects[0].memberCount is typed as number // projects[0].memberNames is typed as string[] ``` --- ## 🔗 Official Resources For a deep dive into how SurrealDB handles graph data, check out the official documentation: - [**SurrealDB Graph Models**](https://surrealdb.com/docs/learn/data-models/graph/overview) — Details on IDs, Edges, and Graph Traversal. --- ## 💡 Many-to-Many vs. Record Links | Feature | `Field.record` (Link) | `Table.relation` (Edge) | | :--- | :--- | :--- | | **Cardinality** | One-to-One / One-to-Many | Many-to-Many | | **Properties** | No (Link only) | ✅ Yes (Properties on the Edge) | | **Query Speed** | Fast (Direct lookup) | Fast (Index-backed graph traversal) | | **Complexity** | Simple | More setup (Extra table) | **Recommendation:** Use `record` links for simple ownership (e.g., `Post` has one `Author`). Use `relation` tables when the relationship itself has data (e.g., `User` follows `User` with a `since` timestamp). --- ## import { Aside } from '@astrojs/starlight/components'; Source: guides/migrating-to-1.0.0-alpha.mdx import { Aside } from '@astrojs/starlight/components'; This guide helps you upgrade your UnrealORM codebase to 1.0.0 alpha. The 1.0.0 release includes major improvements including transaction support, SurrealDB JS SDK 2.0 integration, and enhanced update APIs. ## Breaking Changes ### 1. `$dynamic` Property Removed Extra fields (fields not defined in your schema) are now assigned directly to model instances instead of being stored in the `$dynamic` property. **Before (pre-1.0):** ```ts const user = await User.select(db, { only: true, limit: 1 }); // Extra fields were in $dynamic console.log(user.$dynamic.someExtraField); ``` **After (1.0.0 alpha):** ```ts const user = await User.select(db, { only: true, limit: 1 }); // Extra fields are now directly on the instance console.log(user.someExtraField); ``` ### 2. Update Method Signature **Before (pre-1.0):** ```ts // Instance methods await user.update(db, { name: "Jane" }); await user.merge(db, { name: "Jane" }); // Static methods await User.update(db, "user:123", { name: "Jane" }); await User.merge(db, "user:123", { name: "Jane" }); ``` **After (1.0.0 alpha):** ```ts // Instance methods - now requires explicit mode await user.update(db, { data: { name: "Jane" }, mode: "merge" }); await user.update(db, { data: { name: "Jane" }, mode: "content" }); // Static methods - now requires explicit mode await User.update(db, "user:123", { data: { name: "Jane" }, mode: "merge", }); ``` ### 3. Removed `merge` Method The `merge` method has been removed. Use `update` with `mode: 'merge'` instead. **Before:** ```ts await user.merge(db, { name: "Jane" }); ``` **After:** ```ts await user.update(db, { data: { name: "Jane" }, mode: "merge" }); ``` ### 4. `from` Method Enhanced The `from` method now supports `surql` template literals and raw queries in addition to record IDs. **Before (pre-1.0):** ```ts // Only record IDs were supported const user = await User.from(db, "user:123"); ``` **After (1.0.0 alpha):** ```ts import { surql } from "surrealdb"; // Record ID (still works) const user = await User.from(db, "user:123"); // SurrealQL query const users = await User.from(db, surql`SELECT * FROM user WHERE age > 18`); // Raw query string const users = await User.from(db, { raw: "SELECT * FROM user WHERE active = true" }); ``` ### 5. SurrealDB 2.0 Dependency UnrealORM 1.0.0 alpha requires SurrealDB 2.0. Update your dependencies: **package.json:** ```json { "dependencies": { "surrealdb": "^2.0.0", "@surrealdb/node": "^3.0.1" // if you run embedded, such as for internal testing } } ``` ### 6. Field Options Type Changes Field options now use `BoundQuery` and `Expr` types instead of strings for better type safety. **Before:** ```ts Field.string({ assert: "$value CONTAINS '@'", default: "'unknown@example.com'", }); ``` **After:** ```ts import { surql } from "surrealdb"; Field.string({ assert: surql`$value CONTAINS "@"`, default: surql`"unknown@example.com"`, }); ``` ## New: CLI Tools UnrealORM 1.0.0 alpha introduces a new CLI package for schema management: ```bash # Initialize a new project (recommended starting point) bunx @unreal-orm/cli init # Or with other package managers npx @unreal-orm/cli init pnpm dlx @unreal-orm/cli init yarn dlx @unreal-orm/cli init ``` The `init` command will: - Configure your database connection - Set up the project structure (`unreal/` folder) - Install dependencies (`unreal-orm`, `surrealdb`, `@unreal-orm/cli`) - Optionally generate sample tables or import from existing database ### Available Commands | Command | Description | | --------- | ------------------------------------------------ | | `init` | Initialize UnrealORM in your project | | `pull` | Introspect database and generate TypeScript schema | | `push` | Apply TypeScript schema to database | | `diff` | Compare code schema with database schema | | `mermaid` | Generate ERD diagrams from your schema | | `view` | Interactive TUI for browsing and editing records | ### Quick Start ```bash # Initialize a new project bunx @unreal-orm/cli init # After init, use the CLI directly unreal pull # Generate TypeScript from database unreal push # Apply schema to database unreal diff # Compare code vs database ``` ### Smart Merge The `pull` command now features intelligent merging that preserves your customizations: - **Adds** new fields/indexes with `// Added from database` comments - **Comments out** removed fields/indexes for review (never deletes your code) - **Preserves** your custom methods, comments, and formatting See the [CLI README](https://github.com/Jimpex/unreal-orm/tree/main/packages/unreal-cli) for full documentation. ## Removed: Implicit Database Support UnrealORM 1.0.0 alpha no longer supports implicit database connections. All CRUD methods now require an explicit `db` argument. This prevents unexpected behavior and keeps the type system simpler. ### Configuration Run `unreal init` to set up your project: ```bash bunx @unreal-orm/cli init ``` This creates: - `unreal/surreal.ts` - Database connection and `getDatabase()` helper - `unreal.config.json` - Project configuration Import `getDatabase()` from `unreal/surreal.ts` and pass `db` explicitly: ```ts import { getDatabase } from "./unreal/surreal"; import { User } from "./unreal/tables/User"; const db = await getDatabase(); const users = await User.select(db, { limit: 10 }); ``` ### Usage All CRUD methods require an explicit `db` argument: ```ts const users = await User.select(db, { limit: 10 }); const user = await User.create(db, { name: "John" }); await user.update(db, { data: { name: "Jane" }, mode: "merge" }); await user.delete(db); ``` ### Supported Methods | Method | Requires `db` | | ------ | ------------- | | `Model.select(db, options)` | ✓ | | `Model.create(db, data)` | ✓ | | `Model.update(db, id, options)` | ✓ | | `Model.delete(db, id)` | ✓ | | `instance.update(db, options)` | ✓ | | `instance.delete(db)` | ✓ | ## Step-by-Step Migration ### 1. Update Dependencies The easiest way is to run the init command which handles everything: ```bash bunx @unreal-orm/cli init ``` Or manually update: ```bash # Update SurrealDB to 2.0 bun add surrealdb@2.0.0 bun add @surrealdb/node@3.0.1 # if using embedded mode # Update UnrealORM and install CLI bun add unreal-orm@latest bun add -D @unreal-orm/cli@latest ``` ### 2. Update Update Method Calls Find all instances of `.update()` and `.merge()` calls and update them: ```bash # Search for update calls (manual review required) grep -r "\.update(" src/ grep -r "\.merge(" src/ ``` **Migration patterns:** ```ts // Pattern 1: Simple updates // Before await user.update(db, { name: "Jane" }); // After await user.update(db, { data: { name: "Jane" }, mode: "merge" }); // Pattern 2: Merge calls // Before await user.merge(db, { name: "Jane" }); // After await user.update(db, { data: { name: "Jane" }, mode: "merge" }); // Pattern 3: Static updates // Before await User.update(db, "user:123", { name: "Jane" }); // After await User.update(db, "user:123", { data: { name: "Jane" }, mode: "content", }); ``` ### 3. Update Field Definitions Update field options to use `surql` templates: ```ts import { surql } from "surrealdb"; // Before class User extends Table.normal({ name: "user", fields: { email: Field.string({ assert: "$value CONTAINS '@'", default: "'unknown@example.com'", }), active: Field.bool({ default: "true" }), }, }) {} // After class User extends Table.normal({ name: "user", fields: { email: Field.string({ assert: surql`$value CONTAINS "@"`, default: surql`"unknown@example.com"`, }), active: Field.bool({ default: surql`true` }), }, }) {} ``` ## Transaction Migration ### Basic Transaction Usage ```ts import { Surreal } from "surrealdb"; const db = new Surreal(); await db.connect("memory"); // Start a transaction const tx = await db.beginTransaction(); try { // All operations now use the transaction instead of db const user = await User.create(tx, { name: "Alice" }); const post = await Post.create(tx, { title: "Hello", author: user.id }); await user.update(tx, { data: { name: "Alice Smith" }, mode: "merge" }); // Commit the transaction await tx.commit(); } catch (error) { // Rollback on error await tx.cancel(); throw error; } ``` ### Feature Flag Checking Check if transactions are supported in your SurrealDB version: ```ts import { Features } from "surrealdb"; if (db.isFeatureSupported(Features.Transactions)) { // Transactional operations const tx = await db.beginTransaction(); // ... use transaction } else { // Fallback to non-transactional operations console.warn("Transactions not supported, using regular operations"); } ``` ## Update API Migration ### Update Modes Explained | Mode | Description | Use Case | | --------- | --------------------------------- | --------------------------- | | `content` | Full document replacement | Complete record updates | | `merge` | Partial field updates | Incremental changes | | `replace` | Full document replacement (alias) | Same as content | | `patch` | JSON Patch operations | Complex field-level changes | ### Migration Examples ```ts // 1. Simple field updates (most common) // Before await user.update(db, { name: "Jane" }); await user.merge(db, { email: "jane@example.com" }); // After await user.update(db, { data: { name: "Jane" }, mode: "merge" }); await user.update(db, { data: { email: "jane@example.com" }, mode: "merge" }); // 2. Complete record replacement // Before (update with all required fields) await user.update(db, { name: "Jane", email: "jane@example.com", age: 30 }); // After await user.update(db, { data: { name: "Jane", email: "jane@example.com", age: 30 }, mode: "content", }); // 3. JSON Patch operations (new in 1.0.0) const user = await User.update(db, "user:123", { data: [ { op: "replace", path: "/name", value: "Jane" }, { op: "add", path: "/age", value: 30 }, ], mode: "patch", }); ``` ## SurrealDB 2.0 Integration ### New Imports ```ts import { surql, BoundQuery, Expr } from "surrealdb"; ``` ### Query Building Changes ```ts // Before (string-based queries) const users = await User.select(db, { where: "age > 18 AND status = 'active'", }); // After (syntax highlighted and automatic variable binding) const users = await User.select(db, { // using surql template where: surql`age > 18 AND status = 'active'`, // or using Expr api where: and(eq("active", true), gte("age", age)), // or both! where: surql`${eq("active", true)} AND age >= ${age}`, }); ``` ### Field Option Updates ```ts // Before Field.string({ permissions: { select: "WHERE $auth.id = owner", }, }); // After Field.string({ permissions: { select: surql`WHERE $auth.id = owner`, }, }); ``` ## Troubleshooting ### Common Issues #### 1. TypeScript errors with update methods **Error:** `Argument of type '{ name: string }' is not assignable to parameter of type 'UpdateOptions<...>'` **Solution:** Wrap data in `data` property and specify `mode`: ```ts // Wrong await user.update(db, { name: "Jane" }); // Correct await user.update(db, { data: { name: "Jane" }, mode: "merge" }); ``` #### 2. Field option type errors **Error:** `Type 'string' is not assignable to type 'BoundQuery | Expr'` **Solution:** Use `surql` template literals: ```ts import { surql } from "surrealdb"; // Wrong Field.string({ default: "default_value" }); // Correct Field.string({ default: surql`default_value` }); ``` #### 3. Transaction not supported **Error:** `beginTransaction is not a function` **Solution:** Check SurrealDB version and feature support: ```ts import { Features } from "surrealdb"; if (!db.isFeatureSupported(Features.Transactions)) { console.log("Transactions require SurrealDB v3 (alpha)"); // Use regular db operations instead } ``` ### Getting Help - **GitHub Issues**: [Report bugs](https://github.com/Jimpex/unreal-orm/issues) - **GitHub Discussions**: [Ask questions](https://github.com/Jimpex/unreal-orm/discussions) - **Documentation**: [Full docs](https://unreal-orm.jimpex.dev) ### Migration Checklist - [ ] Update SurrealDB to 2.0 - [ ] Update UnrealORM to 1.0.0 alpha - [ ] Run `bunx @unreal-orm/cli init` or install CLI manually - [ ] Replace `.$dynamic.field` with `.field` (direct property access) - [ ] Replace all `.merge()` calls with `.update({ mode: 'merge' })` - [ ] Update all `.update()` calls to use new signature - [ ] Update `.from()` calls if using raw queries (now supports `surql` and `{ raw: ... }`) - [ ] Convert field options to use `surql` templates - [ ] Add transaction support where needed - [ ] Test all CRUD operations - [ ] Verify type safety with TypeScript compiler - [ ] Use `unreal diff` to verify schema sync --- --- ## Managing your database schema is a core part of the Unreal ORM workflow. The CLI provides tools to keep your TypeScript models and SurrealDB schema in sync. Source: guides/migrations.mdx Managing your database schema is a core part of the Unreal ORM workflow. The CLI provides tools to keep your TypeScript models and SurrealDB schema in sync. ## 🔄 The Sync Workflow The primary tools for schema management are `unreal pull`, `unreal push`, and `unreal diff`. ### 1. Pulling existing schema Use `unreal pull` to generate ORM models from an existing SurrealDB instance. **Pro-tip:** Flags are completely optional! If you just run `unreal pull`, the CLI will interactively prompt you for connection details or load them from your `unreal/surreal.ts`. ```bash # Automated/CI usage: unreal pull --url ws://localhost:8000 -u root -p root -n test -d test # Interactive/Convenient usage: unreal pull ``` ### 2. Pushing your models Once you've defined or modified your `Table.normal` classes, use `unreal push` to apply the generated SurrealQL to your database. ```bash bun unreal push ``` *(This uses your `unreal/surreal.ts` for connection details)* ### 3. Reviewing changes with `diff` Before applying changes, use `unreal diff` to see what has changed between your current models and the live database. ```bash bun unreal diff ``` --- ## 🛠️ Automation & CI/CD Integrating schema validation into your CI/CD pipeline ensures that your code never drifts from its intended schema. ### Schema Validation in CI You can use `Unreal.ast.compareSchemas` or the CLI in a dry-run mode to fail a pull-request if the models don't match the expected schema. ```yaml # GitHub Action Example - name: Verify Schema Sync run: | # Generate schema SQL and compare with a snapshot or live DB bun unreal diff --exit-code-on-change ``` ### Schema Generation as an Artifact You can generate the full SurrealQL schema without connecting to a database by using `Unreal.generateFullSchemaQl()`. This is useful for generating a `schema.surql` file for your repository. ```ts import { Unreal } from 'unreal-orm'; import { User, Post } from './models'; const sql = Unreal.generateFullSchemaQl([User, Post]); // Write sql to a file... ``` --- ## 💡 Best Practices 1. **Model-First approach**: Let your TypeScript code be the "source of truth". 2. **Version Control your Schema**: Always check in your `unreal.config.json` and consider checking in a generated `schema.surql` for documentation. 3. **Use SCHEMAFULL**: Always set `schemafull: true` on your tables to ensure SurrealDB enforces your type definitions. --- ## This guide covers common structural patterns you might encounter while building applications with SurrealDB and Unreal ORM. Source: guides/patterns.mdx This guide covers common structural patterns you might encounter while building applications with SurrealDB and Unreal ORM. ## 🏢 Singleton / Global Settings Use a singleton table when you need global configuration that only has one record (e.g., app settings, maintenance flags). ```ts import { Table, Field, Index } from 'unreal-orm'; import { surql } from 'surrealdb'; class AppSettings extends Table.normal({ name: 'app_settings', schemafull: true, fields: { siteName: Field.string(), maintenanceMode: Field.bool({ default: surql`false` }), }, }) { static async getInstance(db: Surreal) { // Always use a fixed ID for the singleton return this.select(db, { from: 'app_settings:main', only: true }); } } ``` --- ## 🏷️ Tagging Systems For tagging, you can either use an array of strings (simple) or a many-to-many relationship (advanced). ### Simple: Array of Strings Best for simple filtering where you don't need to store extra data about the tags. ```ts class BlogPost extends Table.normal({ name: 'post', schemafull: true, fields: { title: Field.string(), tags: Field.array(Field.string()), }, }) {} // Querying by tag const devPosts = await BlogPost.select(db, { where: surql`tags CONTAINS "development"` }); ``` --- ## 🌳 Recursive / Hierarchical Data The parent-child pattern is common for categories, folders, or comment threads. ```ts class Category extends Table.normal({ name: 'category', schemafull: true, fields: { name: Field.string(), parent: Field.option(Field.record(() => Category)), }, }) { static async getRoot(db: Surreal) { return this.select(db, { where: surql`parent = NONE` }); } async getChildren(db: Surreal) { return Category.select(db, { where: surql`parent = ${this.id}` }); } } ``` --- ## 🎭 Polymorphic Links A polymorphic link is a field that can point to multiple different types of tables. ```ts class ActivityFeed extends Table.normal({ name: 'activity', schemafull: true, fields: { action: Field.string(), // Link to either a User or an Organization actor: Field.custom('record'), }, }) {} ``` --- ## 🔑 Multi-Column Unique Index Enforcing uniqueness across multiple fields (e.g., a user can only have one "primary" email). ```ts const idx_user_primary = Index.define(() => User, { name: 'idx_user_primary', fields: ['userId', 'isPrimary'], unique: true, }); ``` --- ## We will be building a small **blog API** with users, posts, and comments using **UnrealORM** and **SurrealDB**. Source: guides/unreal-orm-tutorial.mdx We will be building a small **blog API** with users, posts, and comments using **UnrealORM** and **SurrealDB**. This tutorial focuses on UnrealORM features and how to use the ORM effectively. We expect it to take around **20-25 minutes** if you follow along. --- ## Setup UnrealORM is designed for **SurrealDB** and can run on **Node.js** or **Bun**. For this tutorial, we'll use Bun with the in-memory SurrealDB database. ### Install Dependencies ```bash bun init -y bun add surrealdb@latest unreal-orm@latest @surrealdb/node@latest bun add -D typescript @types/bun ``` > **Note:** `@surrealdb/node` is required for running SurrealDB embedded locally in a Node.js or Bun environment. This is great for prototyping, development, and testing. ### Project Setup Create `src/index.ts`: ```ts import { Surreal } from 'surrealdb'; import { createNodeEngines } from '@surrealdb/node'; const db = new Surreal({ engines: createNodeEngines() }); async function main() { await db.connect('mem://', { namespace: 'blog', database: 'tutorial', }); console.log('Connected to SurrealDB!'); } main().catch(console.error); ``` Run it to verify setup: ```bash bun run src/index.ts ``` You should see "Connected to SurrealDB!" output. --- ## Define Your First Model Let's create a `User` model with basic fields: ```ts // src/index.ts import { Surreal } from 'surrealdb'; import { createNodeEngines } from '@surrealdb/node'; import { Table, Field, Unreal } from 'unreal-orm'; import { surql } from 'surrealdb'; class User extends Table.normal({ name: 'user', schemafull: true, fields: { name: Field.string(), email: Field.string({ assert: surql`string::is::email($value)` }), bio: Field.option(Field.string()), }, }) {} async function main() { const db = new Surreal({ engines: createNodeEngines() }); await db.connect('mem://', { namespace: 'blog', database: 'tutorial', }); // Apply schema to database await Unreal.applySchema(db, [User]); // Create a user const user = await User.create(db, { name: 'Alice', email: 'alice@example.com', bio: 'Full-stack developer' }); console.log('Created user:', user); } main().catch(console.error); ``` Run this and you should see your first user created with type safety! --- ## Schema Generation UnrealORM automatically generates **SurrealQL DDL** statements for your models: > **Note on Required Fields:** In UnrealORM, fields are **required by default**. This means SurrealDB will reject any write operation where a required field is missing. To make a field optional, you must wrap it in `Field.option()`. ```sql -- The ORM generates and applies: DEFINE TABLE user SCHEMAFULL; 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; ``` The `Unreal.applySchema` function applies all these definitions to SurrealDB. ### Schema-only Mode Sometimes you want to **generate DDL without executing it** (e.g. for migration scripts or CI schema-drift checks): ```ts import { Unreal } from 'unreal-orm'; // Pass one or more model classes – returns a single SurrealQL script const ddl = Unreal.generateFullSchemaQl([User, Post]); console.log(ddl); // db.query(ddl) // you can run it manually later ``` Use this in pipelines to compare the generated DDL against committed files and fail the build if they differ. --- ## Core Operations, Methods, & Indexes Now let's explore the core features of UnrealORM: performing CRUD operations, adding custom business logic to models, and defining database indexes. ### 1. Core Operations (CRUD) UnrealORM provides a complete, type-safe API for creating, reading, updating, and deleting records. ```ts // --- 1. Create --- const user = await User.create(db, { name: 'Alice', email: 'alice@example.com', bio: 'Developer', }); // --- 2. Read --- // Find a single record by its ID const foundUser = await User.select(db, { from: user.id, only: true }); // --- 3. Update --- // Partial update (most common): merge specific fields const mergedUser = await foundUser.update(db, { data: { bio: 'Senior Developer' }, mode: 'merge', }); // Full document replacement: content mode (must provide ALL required fields) const updatedUser = await mergedUser.update(db, { data: { name: 'Alice Smith', email: 'alice.smith@example.com', bio: 'Lead Developer', }, mode: 'content', }); // --- 4. Delete --- // You can delete a record using the instance method await updatedUser.delete(db); // Or delete by ID using the static method // await User.delete(db, updatedUser.id); ``` ### 2. Custom Methods You can add custom business logic directly to your model classes. Instance methods have access to record data via `this`, while static methods are useful for creating custom queries. ```ts import { surql } from 'surrealdb'; class User extends Table.normal({ name: 'user', schemafull: true, fields: { name: Field.string(), email: Field.string({ assert: surql`string::is::email($value)` }), bio: Field.option(Field.string()), }, }) { // Instance Method getDisplayName() { return `${this.name} <${this.email}>`; } // Static Method static async findByEmail(db: Surreal, email: string) { const users = await this.select(db, { where: surql`email = $email`, vars: { email }, }); return users[0]; // Return the first match or undefined } } // --- Using Custom Methods --- const bob = await User.create(db, { name: 'Bob', email: 'bob@example.com' }); // Call the static finder const foundBob = await User.findByEmail(db, 'bob@example.com'); // Call the instance method console.log(foundBob?.getDisplayName()); // Outputs: "Bob " ``` ### 3. Defining Indexes Indexes are crucial for query performance and enforcing data integrity. Define them with `Index.define()` and pass them to `Unreal.applySchema` alongside your models. ```ts import { Index } from 'unreal-orm'; // Define a unique index on the email field const UserEmailIndex = Index.define(() => User, { name: 'user_email_unique', fields: ['email'], unique: true, // Enforce uniqueness }); // Apply schema for models AND indexes await Unreal.applySchema(db, [User, Post, UserEmailIndex]); // Now, SurrealDB will throw an error if you try to create // two users with the same email address. ``` --- ## Relations & Hydration Define relationships between models using `Field.record()` and fetch related data with the `fetch` option. > **Circular Dependencies?** > Use a thunk `() => OtherModel` inside `Field.record()` when two models reference each other. ```ts class Post extends Table.normal({ name: 'post', schemafull: true, fields: { title: Field.string(), content: Field.string(), author: Field.record(() => User), tags: Field.option(Field.array(Field.string())), published: Field.bool({ default: surql`false` }), }, }) {} async function testRelations() { const author = await User.create(db, { name: 'Charlie', email: 'charlie@example.com' }); const post = await Post.create(db, { title: 'Hydration is Awesome', content: '...', author: author.id, }); // Fetch the post and its author const result = await Post.select(db, { from: post.id, only: true, fetch: ['author'], }); // result.author is now a fully-typed User instance! console.log(`Post by ${result?.author.getDisplayName()}`); } ``` --- ## Validation & Error Handling UnrealORM provides both TypeScript and SurrealDB-level validation: ```ts async function testValidation() { try { // This will fail - missing required field 'name' await User.create(db, { email: 'incomplete@example.com' }); } catch (err) { console.log('Validation error:', err.message); } try { // This will fail - duplicate email (unique index) await User.create(db, { name: 'Another Bob', email: 'bob@example.com' // Already exists }); } catch (err) { console.log('Constraint error:', err.message); } } ``` SurrealDB native errors are passed through directly - no ORM-specific error wrapping. --- ## Edge Tables (Many-to-Many) For many-to-many relationships, use `Table.relation`. The dedicated `.relate()` method is the preferred way to create edges: ```ts class Comment extends Table.normal({ name: 'comment', schemafull: true, fields: { content: Field.string(), author: Field.record(() => User), post: Field.record(() => Post), }, }) {} // Edge table for likes class Liked extends Table.relation({ name: 'liked', schemafull: true, fields: { in: Field.record(() => User), out: Field.record(() => Post), timestamp: Field.datetime({ default: surql`time::now()` }), }, }) {} async function testEdges() { await Unreal.applySchema(db, [User, Post, Comment, Liked]); // Create like relationship using .relate() const like = await Liked.relate(db, { from: user.id, to: post.id, }); console.log('User liked post at:', like.timestamp); } ``` --- ## Complete Example Here's the full working blog API: ```ts // src/blog-api.ts import { Surreal } from 'surrealdb'; import { createNodeEngines } from '@surrealdb/node'; import { Table, Field, Index, Unreal } from 'unreal-orm'; import { surql } from 'surrealdb'; // Models class User extends Table.normal({ name: 'user', schemafull: true, fields: { name: Field.string(), email: Field.string({ assert: surql`string::is::email($value)` }), bio: Field.option(Field.string()), }, }) { getDisplayName() { return `${this.name} <${this.email}>`; } } class Post extends Table.normal({ name: 'post', schemafull: true, fields: { title: Field.string(), content: Field.string(), author: Field.record(() => User), published: Field.bool({ default: surql`false` }), }, }) {} // Define a unique index const UserEmailIndex = Index.define(() => User, { name: 'user_email_unique', fields: ['email'], unique: true, }); async function main() { const db = new Surreal({ engines: createNodeEngines() }); await db.connect('mem://', { namespace: 'blog', database: 'tutorial', }); // Apply schema for models and indexes await Unreal.applySchema(db, [User, Post, UserEmailIndex]); // Create and test const author = await User.create(db, { name: 'Tutorial Author', email: 'author@example.com', bio: 'Learning UnrealORM' }); const post = await Post.create(db, { title: 'Getting Started with UnrealORM', content: 'This ORM is amazing for SurrealDB!', author: author.id, published: true }); // Query with hydration const result = await Post.select(db, { from: post.id, only: true, fetch: ['author'] }); console.log(`Post "${result?.title}" by ${result?.author.getDisplayName()}`); await db.close(); } main().catch(console.error); ``` Run this and see your complete blog API in action! --- ## Key Takeaways **vs SurrealDB JS SDK:** | Feature | UnrealORM | SurrealDB SDK | |---------|-----------|---------------| | Type Safety | Full TypeScript | Manual typing | | Schema Generation | Automatic DDL | Manual SQL | | Relations & Hydration | Typed hydration | Manual joins | | Validation | TS + DB level | Manual checks | | Custom Methods | Class methods | Separate functions | **Best Practices:** - Use `schemafull: true` for production applications - Define custom methods directly in class bodies (no decorators) - Use `Unreal.applySchema()` in setup/migration scripts - Handle SurrealDB native errors directly - Use `fetch` parameter for efficient relation hydration - Pass `namespace`, `database`, and `authentication` in a single `.connect()` call --- ## Next Steps - **Advanced Relations**: Explore more complex many-to-many patterns in [Graph Relations](./graph-relations/) - **Permissions**: Add SurrealDB table-level permissions in [Security & Permissions](../advanced/permissions/) - **Migrations**: Version your schema changes in [Migrations & Schema Sync](./migrations/) - **Cookbook**: Quick recipes in the [Cookbook](./cookbook/) Check out the [API Reference](/docs/api/) for complete documentation! --- ## import { Card, CardGrid, LinkCard } from '@astrojs/starlight/components'; Source: index.mdx import { Card, CardGrid, LinkCard } from '@astrojs/starlight/components'; ## Key Features Stays close to SurrealDB's native capabilities, avoiding unnecessary abstractions that hide SurrealDB's powerful features. Full TypeScript type inference for your schema, queries, and results without runtime overhead. Generate valid SurrealDB schema definitions directly from your TypeScript code. Type-safe query building with typed field proxies, IntelliSense, and full SurrealQL function coverage. Typed record links with automatic hydration via `fetch`, plus edge tables for many-to-many relationships. Manage schema with `init`, `pull`, `push`, `diff`, `mermaid`, and `view` commands. Includes dedicated [llms.txt](/llms.txt) and [llms-full.txt](/llms-full.txt) context files for a superior experience with LLMs and AI tools. ## Quick Example ```ts import { Surreal, surql } from 'surrealdb'; import { Table, Field, Index, Unreal } from 'unreal-orm'; // Define a User model with validation and custom methods class User extends Table.normal({ name: 'user', schemafull: true, fields: { name: Field.string(), email: Field.string({ assert: surql`string::is::email($value)` }), createdAt: Field.datetime({ default: surql`time::now()` }), }, }) { getDisplayName() { return `${this.name} <${this.email}>`; } } // Define a unique index const userEmailIndex = Index.define(() => User, { name: 'user_email_unique', fields: ['email'], unique: true, }); // Define a Post with a relation to User class Post extends Table.normal({ name: 'post', schemafull: true, fields: { title: Field.string(), content: Field.string(), author: Field.record(() => User), }, }) {} async function main() { const db = new Surreal(); await db.connect('ws://localhost:8000', { namespace: 'test', database: 'test', authentication: { username: 'root', password: 'root' }, }); // Apply schema to database await Unreal.applySchema(db, [User, userEmailIndex, Post]); // Create records const user = await User.create(db, { name: 'Alice', email: 'alice@example.com', }); const post = await Post.create(db, { title: 'Hello World', content: 'My first post!', author: user.id, }); // Query with hydrated relations const result = await Post.select(db, { from: post.id, only: true, fetch: ['author'], }); console.log(result.author.getDisplayName()); // "Alice " } main(); ``` ## Quick Links --- ## Models expose both instance and static `delete` methods, plus a `deleteMany` helper. Source: querying/deleting.mdx Models expose both instance and static `delete` methods, plus a `deleteMany` helper. ## Delete by id ```ts await User.delete(db, userId); ``` ## Instance delete ```ts const user = await User.create(db, { ... }); await user.delete(db); ``` ## Delete by filter ```ts await User.delete(db, { where: (f) => f.inactive.isTrue(), }); ``` ## Delete many ```ts await User.deleteMany(db, { where: (f) => f.createdAt.lt(surql`time::now() - 1y`), }); ``` ## Soft deletes The ORM does not enforce soft deletes. Implement them with a flag and a static helper: ```ts class Post extends Table.normal({ name: 'post', fields: { isDeleted: Field.bool({ default: surql`false` }), }, }) { static async allActive(db) { return this.select(db, { where: (f) => f.isDeleted.isFalse() }); } } ``` --- ## UnrealORM exposes two static methods for adding data: `create` and `insert`. Source: querying/inserting.mdx UnrealORM exposes two static methods for adding data: `create` and `insert`. ## `create` `create` maps to SurrealDB `CREATE ... RETURN AFTER` and returns a hydrated model instance. ```ts const user = await User.create(db, { name: 'Alice', email: 'alice@example.com', }); console.log(user.id); // RecordId<'user'> ``` You can pass a custom id: ```ts await User.create(db, { id: new RecordId('user', 'alice'), name: 'Alice', }); ``` ## `insert` `insert` maps to `INSERT INTO` and is useful for bulk loading or when you need `IGNORE` or `ON DUPLICATE KEY UPDATE` behavior. ```ts const users = await User.insert(db, { data: [ { name: 'Alice', email: 'alice@example.com' }, { name: 'Bob', email: 'bob@example.com' }, ], }); ``` ### Ignore duplicates ```ts await User.insert(db, { data: { id: new RecordId('user', 'alice'), name: 'Alice' }, ignore: true, }); ``` ### On duplicate ```ts await User.insert(db, { data: { id: new RecordId('user', 'alice'), name: 'Alice' }, onDuplicate: { updatedAt: new Date() }, }); // Or raw SurrealQL await User.insert(db, { data: { id: new RecordId('user', 'alice'), name: 'Alice' }, onDuplicate: surql`visits += 1, lastSeen = time::now()`, }); ``` ### Relation inserts For relation tables `insert` can emit `INSERT RELATION`: ```ts await Follow.insert(db, { data: { id: new RecordId('follow', 'a-b'), in: userA.id, out: userB.id }, }); ``` ## Return clause Both methods support a `return` option: ```ts const names = await User.insert(db, { data: { name: 'Alice' }, return: { value: 'name' }, }); ``` --- ## SurrealDB is a graph database, so UnrealORM distinguishes between a record **link** and a graph **edge**. Source: querying/relations.mdx SurrealDB is a graph database, so UnrealORM distinguishes between a record **link** and a graph **edge**. ## Record links A `Field.record` stores a `RecordId` pointer. It does not enforce referential integrity by default. ```ts class Post extends Table.normal({ name: 'post', fields: { author: Field.record(() => User), }, }) {} ``` Create a post with a link: ```ts const post = await Post.create(db, { title: 'Hello', author: user.id, }); ``` Expand the link at query time: ```ts const posts = await Post.select(db, { select: { title: true, author: { name: true } }, fetch: ['author'], }); ``` ### References with referential integrity Enable the experimental `record_references` capability and use `reference`: ```ts author: Field.record(() => User, { reference: { onDelete: 'cascade' }, }) ``` ## Graph edges A relation table must declare `in` and `out` fields: ```ts class Follow extends Table.relation({ name: 'follow', schemafull: true, fields: { in: Field.record(() => User), out: Field.record(() => User), since: Field.datetime({ default: surql`time::now()` }), }, }) {} ``` Create an edge: ```ts const follow = await Follow.relate(db, { from: alice.id, to: bob.id, orUpdate: true, }); ``` ## Traversing edges in queries Use `surql` for graph paths inside `typed()` or `where`: ```ts // Followers of a user const followers = await User.select(db, { select: { name: true, followerNames: typed(surql`<-follow.in.name`), }, where: (f) => f.id.eq(alice.id), only: true, }); // Users that alice follows const following = await User.select(db, { select: { name: true, follows: typed(surql`->follow->user.name`), }, where: (f) => f.id.eq(alice.id), only: true, }); ``` For complex graph work you can always fall back to raw `surql` or combine the field proxy with `surql` templates. --- ## Every model has a static `select` method that generates a `SELECT` query. The `select` option is the key to type-safe projections. Source: querying/selecting.mdx Every model has a static `select` method that generates a `SELECT` query. The `select` option is the key to type-safe projections. ## All records ```ts const users = await User.select(db); ``` ## Projection ```ts const names = await User.select(db, { select: { name: true, email: true }, }); // Type: { name: string; email: string }[] ``` ## Nested records ```ts const posts = await Post.select(db, { select: { title: true, author: { name: true, email: true } }, fetch: ['author'], }); // author is expanded into { name: string; email: string } ``` ## Select all plus a relation ```ts const posts = await Post.select(db, { select: { '*': true, author: { name: true } }, fetch: ['author'], }); ``` ## Computed fields Use the `typed` helper to give a SurrealQL expression a TypeScript type: ```ts import { typed } from 'unreal-orm'; const posts = await Post.select(db, { select: { title: true, commentCount: typed(surql`count(<-comment)`), }, }); ``` ## Omit fields ```ts const users = await User.select(db, { omit: { password: true }, }); // Type: Omit[] ``` ## Filtering ```ts const users = await User.select(db, { where: (f) => f.age.gte(18).and(f.active.isTrue()), }); ``` Or raw SurrealQL: ```ts const users = await User.select(db, { where: surql`age >= 18 AND active = true`, }); ``` ## Ordering and pagination ```ts const posts = await Post.select(db, { orderBy: [{ field: 'createdAt', order: 'DESC' }], start: 20, limit: 10, }); ``` ## Other options ```ts Post.select(db, { from: new RecordId('post', 'abc'), // single record only: true, // SELECT ... FROM ONLY groupBy: ['author'], // GROUP BY split: ['tags'], // SPLIT AT timeout: '30s', explain: true, DEBUG: true, }); ``` See [Where Builder](./where-builder/) for every field-proxy operator. --- ## Updates in UnrealORM require an explicit `mode` so the generated SurrealQL is clear. Source: querying/updating.mdx Updates in UnrealORM require an explicit `mode` so the generated SurrealQL is clear. ## `content` — full replacement ```ts await User.update(db, userId, { data: { name: 'New Name' }, mode: 'content', }); ``` ## `merge` — partial update ```ts await User.update(db, userId, { data: { name: 'New Name' }, mode: 'merge', }); ``` ## `replace` — full record replacement ```ts await User.update(db, userId, { data: { name: 'New Name', email: 'new@example.com' }, mode: 'replace', }); ``` ## `set` — computed expressions ```ts await Post.update(db, postId, { mode: 'set', data: { views: surql`views + 1`, updatedAt: new Date(), }, }); ``` You can also use `ColumnRef` values from the field proxy. ## `patch` — JSON Patch ```ts await User.update(db, userId, { mode: 'patch', data: [ { op: 'replace', path: '/name', value: 'New Name' }, ], }); ``` ## Instance update Model instances also have an `update` method: ```ts const user = await User.select(db, { only: true, where: (f) => f.id.eq(userId) }); await user.update(db, { data: { name: 'New Name' }, mode: 'merge' }); ``` ## Updating many records ```ts await User.updateMany(db, { where: (f) => f.active.isFalse(), data: { active: true }, mode: 'merge', }); ``` ## Return values ```ts const updated = await User.update(db, userId, { data: { name: 'New Name' }, mode: 'merge', return: 'AFTER', }); ``` --- ## The `where` option on `select`, `count`, `updateMany`, and `deleteMany` can be a callback that receives a typed field proxy. This lets you write type-safe comparisons and let UnrealORM generate the SurrealQL. Source: querying/where-builder.mdx The `where` option on `select`, `count`, `updateMany`, and `deleteMany` can be a callback that receives a typed field proxy. This lets you write type-safe comparisons and let UnrealORM generate the SurrealQL. ## Comparison operators ```ts User.select(db, { where: (f) => f.age.gte(18).and(f.name.eq('Alice')), }); ``` | Operator | Purpose | |---|---| | `eq(value)` | `=` | | `ne(value)` | `!=` | | `gt(value)` | `>` | | `gte(value)` | `>=` | | `lt(value)` | `<` | | `lte(value)` | `<=` | | `exact(value)` | `==` (exact match) | | `isNone()` | `= NONE` | | `isNotNone()` | `!= NONE` | | `isNull()` | `= NULL` | | `isNotNull()` | `!= NULL` | | `isTrue()` | `= true` | | `isFalse()` | `= false` | ## Logical operators ```ts User.select(db, { where: (f) => f.age.gte(18) .and(f.active.isTrue()) .or(f.role.eq('admin')), }); ``` Use `and(...)` and `or(...)` from `unreal-orm` to combine separate proxies: ```ts import { and, or } from 'unreal-orm'; User.select(db, { where: (f) => and( f.age.gte(18), or(f.active.isTrue(), f.role.eq('admin')), ), }); ``` ## Collection operators ```ts User.select(db, { where: (f) => f.role.isIn(['admin', 'editor']), }); Post.select(db, { where: (f) => f.tags.containsAny(['news', 'tech']), }); ``` | Operator | Purpose | |---|---| | `inside(range)` | `INSIDE` | | `outside(range)` | `OUTSIDE` | | `intersects(geometry)` | `INTERSECTS` | | `matches(value)` | `~` (matches pattern) | | `isIn(values)` | `IN` | | `isNotIn(values)` | `NOT IN` | | `containsAny(values)` | `CONTAINSANY` | | `containsAll(values)` | `CONTAINSALL` | | `containsNone(values)` | `CONTAINSNONE` | ## Math and functions ```ts Post.select(db, { where: (f) => f.views.add(1).gt(100), }); ``` | Operator | Purpose | |---|---| | `add(v)` | `+` | | `subtract(v)` | `-` | | `multiply(v)` | `*` | | `divide(v)` | `/` | | `modulo(v)` | `%` | ## Graph helpers ```ts User.select(db, { where: (f) => f.out('follow').eq(bob.id), }); ``` Use `out(relation)`, `in_` (aliased as `in`), and `both(relation)` for graph traversal helpers. ## Aggregations ```ts import { count, sum } from 'unreal-orm'; Post.select(db, { select: { title: true, likeCount: typed(count('->like->user')), }, }); ``` For anything not covered by the proxy, use a `surql` template inside `where` or `typed`. ---