# Unreal ORM - Full Documentation > This file aggregates all documentation for Unreal ORM. ## # 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.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-1.0.0-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](/guides/security-permissions/):** Row-level security (RLS) patterns using `$auth` for table and field-level protection. - **[Testing Strategies](/guides/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 | - | --- ## :::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, }); ``` --- ##

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-1.0.0-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@alpha bun add -D @unreal-orm/cli@latest # Using pnpm pnpm add unreal-orm@latest surrealdb@alpha pnpm add -D @unreal-orm/cli@latest # Using npm npm install unreal-orm@latest surrealdb@alpha npm install -D @unreal-orm/cli@latest # Using yarn yarn add unreal-orm@latest surrealdb@alpha 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** β€” Use `surql` templates and functional expressions directly in queries and field definitions - **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, applySchema } 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"); await db.signin({ username: "root", password: "root" }); await db.use({ namespace: "test", database: "test" }); // Apply schema to database await 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[] ``` ## 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-1.0.0-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) --- ## 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**](./security-permissions/) β€” Row-level security patterns. - [**Testing Strategies**](./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'; class MemberOf extends Table.relation({ name: 'member_of', 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 To create a relationship, you create an instance of the edge model just like any other record. ```ts await MemberOf.create(db, { in: userId, out: projectId, 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/surrealdb/models/graph) β€” 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. ## New: Implicit Database Support UnrealORM 1.0.0 alpha introduces optional implicit database connections. Configure your database once, then call CRUD methods without passing `db` explicitly. ### Configuration Run `unreal init` to set up your project: ```bash bunx @unreal-orm/cli init ``` This creates: - `unreal/surreal.ts` - Database connection with `Unreal.configure()` call - `unreal.config.json` - Project configuration The generated `surreal.ts` automatically configures the ORM when imported. Just add this import to your app's entry point: ```ts // In your main.ts, index.ts, or app entry point import "./unreal/surreal"; ``` ### Usage All CRUD methods now support two calling patterns: ```ts // Pattern 1: Explicit db (always works) 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); // Pattern 2: Implicit db (uses configured default) const users = await User.select({ limit: 10 }); const user = await User.create({ name: "John" }); await user.update({ data: { name: "Jane" }, mode: "merge" }); await user.delete(); ``` ### Supported Methods | Method | Explicit | Implicit | | ------ | -------- | -------- | | `Model.select(db, options)` | βœ“ | `Model.select(options)` | | `Model.create(db, data)` | βœ“ | `Model.create(data)` | | `Model.update(db, id, options)` | βœ“ | `Model.update(id, options)` | | `Model.delete(db, id)` | βœ“ | `Model.delete(id)` | | `instance.update(db, options)` | βœ“ | `instance.update(options)` | | `instance.delete(db)` | βœ“ | `instance.delete()` | ## 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 - [ ] (Optional) Run `unreal init` to enable implicit database support --- --- ## 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 let you pick from your `unreal.config.ts`. ```bash # Automated/Pattern usage: bun unreal pull --url http://localhost:8000 --user root --pass root --ns test --db test # Interactive/Convenient usage: bun 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.config.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.ts` 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 class AppSettings extends Table.normal({ name: 'app_settings', 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', 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', 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', 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, }); ``` --- ## SurrealDB provides a robust permissions system that allows you to define who can access what at the Table or Field level. Unreal ORM exposes these features through the `permissions` option. Source: guides/security-permissions.mdx SurrealDB provides a robust permissions system that allows you to define who can access what at the Table or Field level. Unreal ORM exposes these features through the `permissions` option. ## πŸ›‘οΈ Table Permissions Table permissions control access to the entire record based on a SurrealQL `WHERE` clause. ### Common Patterns: Row-Level Security (RLS) The most common pattern is restricting access to the record owner. SurrealDB provides the `$auth` variable which contains the current session's record ID. ```ts class Post extends Table.normal({ name: 'post', fields: { title: Field.string(), author: Field.record(() => User), }, permissions: { // Anyone can see a post select: true, // Only the author can update or delete their own post update: surql`author = $auth.id`, delete: surql`author = $auth.id`, // Anyone logged in can create create: surql`$auth.id != NONE`, }, }) {} ``` --- ## πŸ”’ Field Permissions Field-level permissions allow you to hide or protect specific fields even if the user has access to the record. ### Hiding Private Data A classic example is hiding a user's `email` or `password_hash` from other users. ```ts class User extends Table.normal({ name: 'user', fields: { name: Field.string(), email: Field.string({ permissions: { // Only the user themselves can see their email select: surql`id = $auth.id`, // Only the user can update their email update: surql`id = $auth.id`, } }), }, }) {} ``` --- ## πŸ”‘ Permissions & $auth The `$auth` variable structure depends on your authentication level. Typically, if you are using SurrealDB's Scope authentication, it represents the record in the `user` (or equivalent) table. ### Permission Expressions Permissions can be set to: - `true`: Always allowed (standard SurrealDB `FULL` access). - `false`: Always denied (standard SurrealDB `NONE` access). - `surql'...'`: A custom SurrealQL expression that evaluates to a boolean. :::tip When using complex permissions, it is highly recommended to use the **`mem://` engine** for testing your security logic in unit tests. See the [Testing Guide](./testing/) for more. ::: --- ## One of the best developer experiences in SurrealDB is the ability to run the database fully in-memory (`mem://`). This makes it possible to write ultra-fast unit tests that verify your schema, assertions, and permissions without any external dependencies. Source: guides/testing.mdx One of the best developer experiences in SurrealDB is the ability to run the database fully in-memory (`mem://`). This makes it possible to write ultra-fast unit tests that verify your schema, assertions, and permissions without any external dependencies. ## πŸ§ͺ In-Memory Setup To use the in-memory engine, you need to use the `@surrealdb/node` engine (which is included in the `surrealdb` package as an optional dependency, but required for local engines). ```bash # Ensure you have the node engines available bun add @surrealdb/node ``` In your test setup, import the engine to register it with the SDK: ```ts import { Surreal } from 'surrealdb'; import { Unreal } from 'unreal-orm'; // Import the node engine to enable mem:// and file:// import { createNodeEngines } from '@surrealdb/node'; async function setupTestDb() { const db = new Surreal({ engines: { ...createNodeEngines(), }, }); // Connect to an isolated in-memory instance await db.connect('mem://'); await db.use({ namespace: 'test', database: 'test' }); // Configure the ORM to use this instance globally Unreal.configure({ database: db }); return db; } ``` :::info For a real-world example of how we test the ORM itself using this pattern, check out the [Unreal ORM Tests on GitHub](https://github.com/Jimpex/unreal-orm/tree/main/packages/unreal-orm/src/tests). ::: --- ## 🏎️ Running Unit Tests Using a framework like `vitest` or `bun test`, you can set up a clean database for every test file (or even every test). ### Example: Testing Assertions ```ts import { test, expect, beforeAll } from 'vitest'; import { User } from './models'; beforeAll(async () => { const db = await setupTestDb(); // Apply the schema once await Unreal.applySchema(db, [User]); }); test('should fail if email is invalid', async () => { const db = Unreal.getDatabase(); await expect(User.create(db, { name: 'Alice', email: 'invalid-email', // This should trigger Field assertion })).rejects.toThrow(); }); ``` --- ## πŸ›‘οΈ Testing Permissions Testing permissions is critical. You can simulate different users by "signing in" or using the `vars` option in queries. ```ts test('user should not be able to delete others posts', async () => { const db = Unreal.getDatabase(); // 1. Create a post as User A const post = await Post.create(db, { title: 'Secret', author: 'user:A' }); // 2. Try to delete as User B (simulated via vars) // Note: In real scenarios, you would use scope auth, // but for raw logic testing you can use $auth vars. await expect(db.query('DELETE $id', { id: post.id, auth: { id: 'user:B' } })) .rejects.toThrow(); }); ``` ## πŸ’‘ Best Practices 1. **Namespace Isolation**: Use a unique database/namespace name for each test runner if running in parallel. 2. **Global Configuration**: Use `Unreal.configure` in a global setup file to avoid passing the `db` instance everywhere. 3. **Snapshot Testing**: Use `Unreal.ast.extractSchema()` to snapshot your schema and prevent accidental DDL changes. --- ## We will be building a small **blog API** with users, posts, and comments using **Unreal-ORM** and **SurrealDB**. Source: guides/unreal-orm-tutorial.mdx We will be building a small **blog API** with users, posts, and comments using **Unreal-ORM** and **SurrealDB**. This tutorial focuses on Unreal-ORM features and how to use the ORM effectively. We expect it to take around **20-25 minutes** if you follow along. --- ## Setup Unreal-ORM is designed for **SurrealDB** and can run on **Node.js** or **Bun**. For this tutorial, we'll use Node.js with the in-memory SurrealDB database. ### Install Dependencies ```bash npm init -y npm install surrealdb@alpha unreal-orm@latest @surrealdb/node@alpha npm install -D typescript @types/node tsx ``` > **Note:** `@surrealdb/node` is required for running SurrealDB embedded locally in a Node.js environment. This is great for prototyping, development, and testing. ### Project Setup Create `src/index.ts`: ```ts // src/index.ts import { Surreal } from 'surrealdb'; import { surrealdbNodeEngines } from '@surrealdb/node'; import Table, { Field } from 'unreal-orm'; const db = new Surreal({ engines: surrealdbNodeEngines() }); async function main() { // Connect to in-memory database await db.connect('mem://'); await db.use({ namespace: 'blog', database: 'tutorial' }); console.log('Connected to SurrealDB!'); } main().catch(console.error); ``` Run it to verify setup: ```bash npx tsx 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 Table, { Field, applySchema } from 'unreal-orm'; class User extends Table.normal({ name: 'user', fields: { name: Field.string(), email: Field.string(), bio: Field.option(Field.string()), }, schemafull: true, }) {} async function main() { const db = new Surreal(); await db.connect('mem://'); await db.use({ namespace: 'blog', database: 'tutorial' }); // Apply schema to database await 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); } ``` Run this and you should see your first user created with type safety! --- ## Schema Generation Unreal-ORM automatically generates **SurrealQL DDL** statements for your models: > **Note on Required Fields:** In Unreal-ORM, 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; DEFINE FIELD bio ON TABLE user TYPE option; ``` The `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 { generateFullSchemaQl } from 'unreal-orm'; // Pass one or more model classes – returns a single SurrealQL script const ddl = 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 Unreal-ORM: performing CRUD operations, adding custom business logic to models, and defining database indexes. ### 1. Core Operations (CRUD) Unreal-ORM 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 class User extends Table.normal({ /* ...fields... */ }) { // 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 `applySchema` alongside your models. ```ts // 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 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 `() : any => OtherModel` inside `Field.record()` when two models reference each other. > The `: any` type annotation suppresses TypeScript's self-referencing complaint and disappears at runtime. ```ts class Post extends Table.normal({ name: 'post', fields: { title: Field.string(), content: Field.string(), author: Field.record(() => User), tags: Field.option(Field.array(Field.string())), published: Field.bool({ default: surql`false` }), }, schemafull: true, }) {} 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()}`); } ``` --- ## Advanced Topics ### 1. Field Options Customize fields with `default`, `assert` (validation), `comment`, etc.: ```ts class Post extends Table.normal({ name: 'post', fields: { title: Field.string({ assert: surql`$value.length > 5`, comment: 'Post title (min 6 chars)', default: surql`'Untitled'`, }), content: Field.string(), }, schemafull: true, }) {} ``` ### 2. Custom SurrealDB types ```ts const Duration = Field.custom('duration'); class Task extends Table.normal({ name: 'task', fields: { title: Field.string(), est: Duration, // stored as SurrealDB duration type, typed as number in TS }, schemafull: true, }) {} // Projection with type-safety const tasks = await Task.select(db, { fields: ['title'], // only 'title' will be in the result type }); ``` {/* ### 3. Parameterized Queries (`vars`) Always use `vars` to pass dynamic values into your queries. Whenever you build a custom `where` clause, always inject **parameters** instead of string-interpolating values. This avoids SurrealQL injection bugs. ```ts // Unsafe (🚫 NEVER string-interpolate user input!) await User.select(db, { where: `email = '${userInput}'` }); // Safe βœ… β€” use vars await User.select(db, { where: surql`email = $email`, vars: { email: userInput } }); ``` `vars` works in **all** Unreal-ORM query helpers. SurrealDB substitutes them server-side, giving you: * Protection against injection attacks * Clear separation between query text and data */} --- ## Validation & Error Handling Unreal-ORM 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`: ```ts class Comment extends Table.normal({ name: 'comment', fields: { content: Field.string(), author: Field.record(() => User), post: Field.record(() => Post), }, schemafull: true, }) {} // Edge table for likes class Liked extends Table.relation({ name: 'liked', fields: { in: Field.record(() => User), out: Field.record(() => Post), timestamp: Field.datetime({ default: surql`time::now()` }), }, schemafull: true, }) {} async function testEdges() { await applySchema(db, [User, Post, Comment, Liked]); // Create like relationship const like = await Liked.create(db, { in: user.id, out: 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 Table, { Field, Index, applySchema } from 'unreal-orm'; // Models class User extends Table.normal({ name: 'user', fields: { name: Field.string(), email: Field.string(), bio: Field.option(Field.string()), }, schemafull: true, }) { getDisplayName() { return `${this.name} <${this.email}>`; } } class Post extends Table.normal({ name: 'post', fields: { title: Field.string(), content: Field.string(), author: Field.record(() => User), published: Field.bool({ default: false }), }, schemafull: true, }) {} // Define a unique index const UserEmailIndex = Index.define(() => User, { name: 'user_email_unique', fields: ['email'], unique: true, }); async function main() { const db = new Surreal(); await db.connect('memory://'); await db.use({ namespace: 'blog', database: 'tutorial' }); // Apply schema for models and indexes await applySchema(db, [User, Post, UserEmailIndex]); // Create and test const author = await User.create(db, { name: 'Tutorial Author', email: 'author@example.com', bio: 'Learning Unreal-ORM' }); const post = await Post.create(db, { title: 'Getting Started with Unreal-ORM', 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 | Unreal-ORM | 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 `applySchema()` in setup/migration scripts - Handle SurrealDB native errors directly - Use `fetch` parameter for efficient relation hydration --- ## Next Steps - **Advanced Relations**: Explore more complex many-to-many patterns - **Permissions**: Add SurrealDB table-level permissions - **Migrations**: Version your schema changes - **Performance**: Learn about indexing and query optimization Check out the [API Reference](/docs/api/) for complete documentation! **Total tutorial time: ~20 minutes** ⏱️ --- ## import { Card, CardGrid } from '@astrojs/starlight/components'; Source: index.mdx import { Card, CardGrid } 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 that mirrors SurrealQL syntax while providing autocomplete and type checking. Includes dedicated [llms.txt](/llms.txt) and [llms-full.txt](/llms-full.txt) context files for a superior experience with LLMs and AI tools. ## Easy to Use ```typescript // Define your schema const User = Table.normal({ name: 'user', fields: { name: Field.string(), email: Field.string({ assert: surql`string::is::email($value)` }), createdAt: Field.datetime({ default: surql`time::now()` }), }, }); // Type-safe queries with bound parameters const pattern = '%@example.com'; const users = await User.select(db, { where: surql`email LIKE ${pattern}`, limit: 10, }); ``` ---