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.
  • rushDBInstance (optional): An instance of RushDB SDK that will automatically register the model. If provided, the model will be registered with this instance.

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>;

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

When working with models in RushDB, there are two ways to register them. The recommended approach is to pass the RushDB instance directly to the model constructor. To make it clear which approach you're using in your code, we recommend following this naming convention:

// RECOMMENDED: Register the model during creation (use ${Name}Repo naming convention)
const AuthorRepo = new Model('author', {
name: { type: 'string' },
email: { type: 'string', uniq: true }
}, db); // Pass the RushDB instance here

// The model is automatically registered and ready to use

Alternatively, you can register a model after creation:

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

// Then register it with the RushDB SDK
const AuthorRepo = db.registerModel(AuthorModel);

Following this naming convention (${Name}Repo vs ${Name}Model) makes it immediately clear in your code which approach was used to register the model.

Working with Models in RushDB class:

  • registerModel: Registers the model with the RushDB SDK, making it ready for data operations
  • getModel and getModels: Retrieve registered models from the SDK, useful for accessing model details programmatically:
public getModel(label: string): Model
public getModels(): Map<string, Model>

Model CRUD Operations

After registering 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 AuthorRepo.create({
name: 'Alice Smith',
email: 'alice.smith@example.com'
});

// Create multiple records
const authors = await AuthorRepo.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 AuthorRepo.find();

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

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

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

Updating Records

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

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

Deleting Records

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

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

Working with Relationships

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

// Detach a relationship
await AuthorRepo.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'

// Using the recommended approach with RushDB instance in constructor
const AuthorRepo = new Model('author', {
name: { type: 'string' },
email: { type: 'string', uniq: true }
}, db);

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

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

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 AuthorRepo.schema
post: typeof PostRepo.schema
blog: typeof BlogRepo.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('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

Conclusion

Using the RestAPI methods in the RushDB class provides a flexible way to perform CRUD operations without registering models. This approach is particularly useful for dynamic or ad-hoc operations, offering a straightforward way to interact with your data. However, for more complex applications where type safety and structure are important, defining and registering models is recommended.