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;
unique?: 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:booleandatetime(can be either a detailed object or an ISO string)nullnumberstringvector(for embedding vectors used in similarity search)
unique: 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', unique: 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', unique: 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', unique: 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 onSchema, 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', unique: 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.getInstance() method to access the API:
// Internal implementation pattern (from model.ts)
async someMethod(params) {
const instance = RushDB.getInstance()
return await instance.someApi.someMethod(params)
}
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', unique: 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:
- Create your RushDB instance in a dedicated file at the root of your application
- Export this instance so it can be imported by other modules
- 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 = () => {
return RushDB.getInstance();
};
// 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
For a complete, up-to-date guide on configuring declaration merging, path aliases, and schema-aware intellisense (including typed related queries), see the Model reference: TypeScript: extend SDK types for schema-aware suggestions.
Note on result typing with aggregations/grouping: when you use aggregate or groupBy, the result shape can differ from your schema. You can augment the instance type as typeof Model.recordInstance & { data: T } to describe the returned payload. See the dedicated explanation and examples in the Model reference, and the concepts for Aggregations and Grouping.
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.
Related Documentation
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