Skip to main content

Models

In this section, we focus on how to define models using the RushDB SDK. Defining models accurately is crucial as it not only aids in validating the fields according to the schema but also enhances the developer experience with features like autocomplete and field name suggestions.

Understanding Schema

The Schema is at the core of model definitions in RushDB. It specifies the structure and constraints of the data fields within your model. Here's a breakdown of the properties you can define within a Schema:

type Schema = Record<string, {
default?: SchemaDefaultValue;
multiple?: boolean;
required?: boolean;
type: PropertyType;
uniq?: boolean;
}>;

Schema Properties Explained:

  • default: This is the initial value of the field if no value is provided during record creation. It can be a static value or a function that returns a value asynchronously, allowing for dynamic default values.
  • multiple: Indicates whether the field can hold multiple values (array) or just a single value.
  • required: Specifies whether a field is mandatory. If set to true, you cannot create a record without providing a value for this field.
  • type: Defines the data type of the field. The type determines the available search operators and how data is validated and stored. Possible types include:
    • boolean
    • datetime (can be either a detailed object or an ISO string)
    • null
    • number
    • string
    • vector (for embedding vectors used in similarity search)
  • uniq: If set to true, the field must have a unique value across all records in the database, useful for fields like email addresses or custom identifiers.

Working with Default Values

Default values are especially useful for automatically setting fields like timestamps, status flags, or counters without requiring explicit values for each record creation. RushDB supports both static default values and dynamic values generated by functions:

// Helper function to get current ISO timestamp
const getCurrentISO = () => new Date().toISOString();

// Using static and dynamic default values
const UserModel = new Model('USER', {
name: { type: 'string' },
avatar: { type: 'string' },
login: { type: 'string', uniq: true },
password: { type: 'string' },
active: { type: 'boolean', default: true }, // Static default
createdAt: { type: 'datetime', default: getCurrentISO }, // Dynamic default
tags: { type: 'string', multiple: true, required: false },
});

When you create a record without specifying values for fields with defaults, the system automatically applies these defaults:

// The createdAt field will be automatically set to the current date/time
// The active field will be set to true
const newUser = await UserModel.create({
name: 'John Doe',
login: 'johndoe',
password: 'securePassword123',
avatar: 'avatar.jpg'
});

Default value functions can also be asynchronous, allowing for operations like fetching configuration values:

const ConfigModel = new Model('CONFIG', {
key: { type: 'string', uniq: true },
value: { type: 'string' },
expiresAt: {
type: 'datetime',
default: async () => {
// Default expiration is 7 days from now
const date = new Date();
date.setDate(date.getDate() + 7);
return date.toISOString();
}
}
});

Creating a Model with Model

With an understanding of Schema, you can define a model in the RushDB system. Here's how to define a simple Author model:

const Author = new Model('author', {
name: { type: 'string' },
email: { type: 'string', uniq: true }
});

Model Constructor Parameters:

  • label: A unique string identifier for the model, which represents a Label in RushDB. It's used to categorize records and define their type in the database system. Labels are crucial for organizing and querying your data.
  • schema: The schema definition based on Schema, which dictates the structure and rules of the data stored.

Type Helpers in Models

The Model class offers several built-in type helpers that enhance TypeScript integration:

// These are defined in the Model class and available as readonly properties
readonly draft!: InferType<Model<S>>
readonly record!: DBRecord<S>
readonly recordInstance!: DBRecordInstance<S>
readonly recordsArrayInstance!: DBRecordsArrayInstance<S>

Type Helpers Explained:

  • draft: Represents a draft version of the schema - a flat object containing only the record's own properties defined by the schema, excluding system fields such as __id, __label, and __proptypes. This is useful when creating new records.
  • record: Represents a fully-defined record with database representation, including all fields that come with the record's database-side representation.
  • recordInstance: Extends the record by providing additional methods to operate on a specific record, such as saving, updating, or deleting it.
  • recordsArrayInstance: Similar to a single record instance but supports batch or bulk operations for efficient management of multiple records.

Practical Type Helpers Example

Here's a practical example of how to use the type helpers to create strongly-typed variables and functions in your application:

// Define the Label as a constant
export const USER = 'USER' as const;

// Create a model with the USER label
export const UserModel = new Model(USER, {
name: { type: 'string' },
avatar: { type: 'string' },
login: { type: 'string', uniq: true },
password: { type: 'string' },
createdAt: { type: 'datetime', default: getCurrentISO },
tags: { type: 'string', multiple: true, required: false },
});

// Export type definitions derived from model
export type UserRecord = typeof UserModel.record;
export type UserRecordResult<T extends Record<PropertyKey, any> = never> =
typeof UserModel.recordInstance & { data: T };
export type UserRecordsArrayResult = typeof UserModel.recordsArrayInstance;
export type UserRecordDraft = typeof UserModel.draft;
export type UserSearchQuery = SearchQuery<typeof UserModel.schema>;

Model Implementation Architecture

The Model class uses the same architectural pattern as other SDK components like Transaction and DBRecordInstance. It uses the static RushDB.init() method to access the API:

// Internal implementation pattern (from model.ts)
async someMethod(params) {
const instance = await this.getRushDBInstance()
return await instance.someApi.someMethod(params)
}

// getRushDBInstance method in Model class
public async getRushDBInstance(): Promise<RushDB> {
const instance = RushDB.getInstance()
if (instance) {
return await RushDB.init()
}
throw new Error('No RushDB instance found. Please create a RushDB instance first: new RushDB("RUSHDB_API_KEY")')
}

This architecture ensures consistent API access across all SDK components.

These exported types can then be used throughout your application to ensure type safety:

// Function that accepts a user draft (without system fields)
function prepareUserForRegistration(user: UserRecordDraft): UserRecordDraft {
return {
...user,
// Add additional processing if needed
};
}

// Function that works with a complete user record (with system fields)
function getUserDisplayName(user: UserRecord): string {
return user.name || user.__id;
}

// Function that receives a user recordInstance with additional methods
async function updateUserAvatar(user: UserRecordResult): Promise<UserRecordResult> {
const newAvatar = generateAvatarUrl(user.data.name);
return await UserModel.update(user.data.__id, { avatar: newAvatar });
}

// Function that creates a type-safe search query
function buildUserSearchQuery(nameFilter: string): UserSearchQuery {
return {
where: {
name: { $contains: nameFilter },
// TypeScript will ensure only valid fields and operators are used
},
sort: { createdAt: 'desc' }
};
}

This approach gives you several advantages:

  • Consistent Type Definitions: All user-related types are derived from a single source of truth.
  • Autocomplete Support: Your IDE will suggest valid field names and types.
  • Type Safety: TypeScript will catch errors if you try to access non-existent fields.
  • Maintainability: Changes to the model automatically propagate to all derived types.

Registering and Managing Models

Models in RushDB don't need to be registered explicitly. When you create a model, it's ready to use right away:

// Create the model
const AuthorModel = new Model('author', {
name: { type: 'string' },
email: { type: 'string', uniq: true }
});

// Start using it directly
const author = await AuthorModel.create({
name: "Jane Doe",
email: "jane@example.com"
});

Important: RushDB Initialization Architecture

Due to the async initialization architecture of RushDB, it's important to initialize the RushDB instance early in your application's lifecycle. This is because JavaScript modules are lazy-loaded and only executed when imported.

To ensure that the RushDB instance is available when needed by your models, it's recommended to:

  1. Create your RushDB instance in a dedicated file at the root of your application
  2. Export this instance so it can be imported by other modules
  3. Import this file early in your application's bootstrap process

Example of proper initialization:

// db.ts (at the root of your project)
import RushDB from '@rushdb/javascript-sdk';

// Initialize RushDB with your API token
export const db = new RushDB('RUSHDB_API_KEY');

// You can also export a helper function to access the instance
export const getRushDBInstance = async () => {
return await RushDB.init();
};
// app.ts or index.ts (your application entry point)
import { db } from './db';
// Import your models after importing the db
import { UserModel, PostModel } from './models';

// The rest of your application code...

This approach ensures that the RushDB instance is initialized before any model tries to use it, preventing "No RushDB instance found" errors.

Model CRUD Operations

After creating a model, you can perform CRUD (Create, Read, Update, Delete) operations through the model's methods.

Creating Records

// Create a single record
const newAuthor = await AuthorModel.create({
name: 'Alice Smith',
email: 'alice.smith@example.com'
});

// Create multiple records
const authors = await AuthorModel.createMany([
{ name: 'Bob Johnson', email: 'bob.johnson@example.com' },
{ name: 'Carol Davis', email: 'carol.davis@example.com' }
]);

Reading Records

// Find all records of this model
const allAuthors = await AuthorModel.find();

// Find specific records with search criteria
const specificAuthors = await AuthorModel.find({
where: { name: { $contains: 'Smith' } }
});

// Find a single record
const oneAuthor = await AuthorModel.findOne({
where: { email: 'alice.smith@example.com' }
});

// Find by unique identifier
const authorById = await AuthorModel.findById('author_id_123');

Updating Records

// Update a specific record by ID
await AuthorModel.update('author_id_123', {
name: 'Alice Johnson-Smith'
});

// Set all values of a record (replace existing data)
await AuthorModel.set('author_id_123', {
name: 'Alice Johnson',
email: 'alice.johnson@example.com'
});

Deleting Records

// Delete records matching criteria
await AuthorModel.delete({
where: { name: { $contains: 'temp' } }
});

// Delete records by ID
await AuthorModel.deleteById(['author_id_123', 'author_id_456']);

Working with Relationships

// Attach a relationship
await AuthorModel.attach({
source: 'author_id_123',
target: 'book_id_456',
options: { type: 'WROTE' }
});

// Detach a relationship
await AuthorModel.detach({
source: 'author_id_123',
target: 'book_id_456',
options: { type: 'WROTE' }
});

Advanced TypeScript Support

When working with RushDB SDK, achieving perfect TypeScript contracts ensures a seamless development experience. TypeScript's strong typing system allows for precise autocomplete suggestions and error checking, particularly when dealing with complex queries and nested models. This section will guide you on how to enhance TypeScript support by defining comprehensive type definitions for your models.

Defining Comprehensive TypeScript Types

To fully leverage TypeScript's capabilities, you can define types that include all schemas you've registered with Model. This will allow you to perform complex queries with nested model fields, ensuring type safety and better autocompletion.

Step 1: Create Models with Model

First, define your models using Model:

import { Model } from '@rushdb/javascript-sdk'

// Create models
const AuthorModel = new Model('author', {
name: { type: 'string' },
email: { type: 'string', uniq: true }
});

const PostModel = new Model('post', {
created: { type: 'datetime', default: () => new Date().toISOString() },
title: { type: 'string' },
content: { type: 'string' },
rating: { type: 'number' }
});

const BlogModel = new Model('blog', {
title: { type: 'string' },
description: { type: 'string' }
});

Step 2: Create an Exportable Type for All Schemas

Next, create an exportable type that includes all the schemas defined in your application:

export type MyModels = {
author: typeof AuthorModel.schema
post: typeof PostModel.schema
blog: typeof BlogModel.schema
}

Step 3: Extend the Models Interface

Add this type declaration to your project. This ensures that RushDB SDK is aware of your models:

// index.d.ts or other d.ts file added to include in tsconfig.json

import { MyModels } from './types';

declare module '@rushdb/javascript-sdk' {
export interface Models extends MyModels {}
}

Example Usage: Complex Queries with Type Safety

By following these steps, you can now write complex queries with confidence, knowing that TypeScript will help you avoid errors and provide accurate autocomplete suggestions. Here's an example demonstrating how you can leverage this setup:

const query = await db.records.find({
labels: ['post'],
where: {
author: {
name: { $contains: 'John' }, // Checking if the author's name contains 'John'
post: {
rating: { $gt: 5 } // Posts with rating greater than 5
}
}
}
});

In this example, the db.records.find method allows you to use nested fields in the where condition, thanks to the enhanced TypeScript definitions. This ensures that you can easily and accurately query your data, leveraging the full power of TypeScript.

By defining comprehensive type definitions for your models and extending the Models interface, you can significantly enhance your TypeScript support when working with RushDB SDK. This approach ensures type safety, better autocompletion, and a more efficient development experience.

Working with Transactions

Model operations can be performed within transactions to ensure data integrity. For more information on using transactions with models, see the Transactions documentation.

Conclusion

Defining models with Model and Schema sets a robust foundation for your application's data architecture. It enables strong type-checking, validation, and inter-model relationships, enhancing the robustness and scalability of your applications. In subsequent sections, we will explore how to interact with these models to create, retrieve, update, and delete records.


For a more in-depth understanding of the RushDB TypeScript SDK and its capabilities, refer to these related sections:

  • Introduction to TypeScript SDK - Learn about the basics of using the SDK
  • Transactions - Learn how to use transactions with models for atomic operations
  • Labels - Understand how Labels work in RushDB and how they're used to categorize records