Skip to main content

Model

The Model class represents a schema-defined entity in RushDB. It provides methods to perform CRUD operations and manage relationships between records. Models are identified by labels in the database.

Usage​

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

// Define a schema
const UserSchema = {
name: { type: 'string', required: true },
email: { type: 'string', required: true, unique: true },
age: { type: 'number' },
active: { type: 'boolean', default: true }
}

// Create a model
const UserModel = new Model('User', UserSchema)

// Or with target label validation enabled
const StrictUserModel = new Model('User', UserSchema, { validateTargetLabel: true })

Type Parameters​

ParameterDescription
S extends Schema = anyThe schema type that defines the structure of the model's records

Constructor​

constructor(modelName: string, schema: S, config?: ModelConfig)

Creates a new Model instance.

Parameters​

ParameterTypeDescription
modelNamestringThe name/label of the model in the database
schemaSThe schema definition that describes the model's structure
configModelConfigOptional configuration (see below)

ModelConfig​

OptionTypeDefaultDescription
validateTargetLabelbooleanfalseWhen true, set() and update() verify the target record's label matches this model before proceeding. Adds one extra DB read per call.

Properties​

label​

public readonly label: string

The name/label of the model in the database.

schema​

public readonly schema: S

The schema definition that describes the model's structure.

draft​

readonly draft!: InferType<Model<S>>

Type helper for a draft version of the schema. Represents a flat object containing only the record's own properties (defined by the schema), excluding system fields such as __id, __label, and __proptypes. This type does not yet have a representation on the database side.

record​

readonly record!: DBRecord<S>

Type helper for a fully-defined record with database representation. Similar to the draft, but includes all fields that come with the record's database-side representation, such as __id, __label, and __proptypes.

searchQuery​

readonly searchQuery!: SearchQuery<S>

Type helper for a SearchQuery of the schema. Represents a structured query input that enables filtering, sorting, pagination, and aggregation of records based on schema-defined fields. Useful for composing reusable, type-safe search expressions.

recordInstance​

readonly recordInstance!: DBRecordInstance<S>

Type helper for a single record instance. Extends the record by providing additional methods to operate on this specific record, such as saving, updating, or deleting it.

recordsArrayInstance​

readonly recordsArrayInstance!: DBRecordsArrayInstance<S>

Type helper for an array of record instances. Similar to a single record instance but supports batch or bulk operations, allowing efficient management of multiple records simultaneously.

Type-only properties on Model​

These helpers are available for typing only and are not populated at runtime. They exist to let you derive precise types from a model without constructing values in code.

Properties that are type-only:

  • draft
  • record
  • recordInstance
  • recordsArrayInstance
  • searchQuery

Practical usage examples:

// DO: derive types using typeof
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>

// DON'T: read them at runtime — they aren’t real values
// console.log(UserModel.record) // undefined at runtime

If you need an actual instance at runtime, work with query results (e.g., find, findOne, create) or convert a raw record via toDBRecordInstance.

Typing results when using aggregations and groupBy​

When you add aggregations or grouping to a SearchQuery, the result shape may no longer match your model schema. To accommodate this, the helper type uses a wildcard extension: typeof Model.recordInstance & { data: T }.

How it works:

  • typeof Model.recordInstance gives you the instance methods and base record metadata.
  • & { data: T } lets you “overlay” the actual data payload you expect back from an aggregated/grouped query, bypassing strict schema checks in those scenarios.

Example:

// Self-group on an aggregated value
const deals = await DealModel.find({
select: {
totalAmount: { $sum: '$record.amount' }
},
groupBy: ['totalAmount']
})

// Tell TS what the aggregated row contains
type DealAggRow = typeof DealModel.recordInstance & { data: { totalAmount: number } }
const firstRow = deals.data[0] as DealAggRow

Notes:

  • Use this pattern whenever select and/or groupBy change the returned columns.
  • For regular record fetches (no aggregation/grouping), prefer the plain typeof Model.recordInstance without augmenting data.

Methods​

getLabel()​

public getLabel(): string

Retrieves the model's label.

Returns: The label/name of the model

toInstance()​

public toInstance(record: DBRecord<S>): DBRecordInstance<S>

Converts a database record to a record instance with additional methods.

Parameters:

  • record: The database record to convert

Returns: A record instance with additional methods

Throws: Error if no RushDB instance was provided during initialization

find()​

async find<Q extends SearchQuery<S> = SearchQuery<S>>(
searchQuery?: Q & { labels?: never },
transaction?: Transaction | string
): Promise<DBRecordsArrayInstance<S, Q>>

Finds records that match the given search criteria.

Parameters:

  • searchQuery: Optional search criteria
  • transaction: Optional transaction or transaction ID

Returns: Promise resolving to matching records

findOne()​

async findOne<Q extends SearchQuery<S> = SearchQuery<S>>(
searchQuery?: Q & {
labels?: never
limit?: never
skip?: never
},
transaction?: Transaction | string
): Promise<DBRecordInstance<S, Q> | null>

Finds a single record that matches the given search criteria.

Parameters:

  • searchQuery: Optional search criteria
  • transaction: Optional transaction or transaction ID

Returns: Promise resolving to the matching record or null if not found

findById()​

async findById(
id: string,
transaction?: Transaction | string
): Promise<DBRecordInstance<S> | null>

Finds a record by its ID.

Parameters:

  • id: The ID of the record to find
  • transaction: Optional transaction or transaction ID

Returns: Promise resolving to the matching record or null if not found

findUniq()​

async findUniq<Q extends SearchQuery<S> = SearchQuery<S>>(
searchQuery?: Q & {
labels?: never
limit?: never
skip?: never
},
transaction?: Transaction | string
): Promise<DBRecordInstance<S, Q> | null>

Finds a unique record that matches the given search criteria.

Parameters:

  • searchQuery: Optional search criteria
  • transaction: Optional transaction or transaction ID

Returns: Promise resolving to the unique matching record or null if not found

create()​

async create(
record: InferSchemaTypesWrite<S>,
transaction?: Transaction | string,
options?: CreateOptions
): Promise<DBRecordInstance<S> | null>

Creates a new record in the database.

Parameters:

  • record: The record data to create
  • transaction: Optional transaction or transaction ID
  • options: Optional behavior on uniqueness conflict

CreateOptions:

OptionTypeDefaultDescription
onConflict'throw' | 'upsert' | 'skip''throw''throw' throws UniquenessError, 'upsert' updates the conflicting record, 'skip' returns null

Returns: Promise resolving to the created record, or null when onConflict: 'skip' and a conflict exists

Throws: UniquenessError if the record violates uniqueness constraints (default behavior)

attach()​

attach(
{
source,
target,
options
}: {
source: DBRecordTarget
target: RelationTarget
options?: RelationOptions
},
transaction?: Transaction | string
): Promise<any>

Attaches a relationship between records.

Parameters:

  • params: Object containing source, target, and relationship options
  • transaction: Optional transaction or transaction ID

Returns: Promise resolving to the result of the attach operation

Type Definitions​

InferType​

export type InferType<M extends Model<any> = Model<any>> = FlattenTypes<InferSchemaTypesRead<M['schema']>>

Helper type that infers the type structure from a Model instance.

Type Parameters:

  • M: The Model type to infer from

TypeScript: extend SDK types for schema-aware suggestions​

To get first-class IDE autocomplete and end-to-end type safety that respects your model schemas (including nested related-queries), augment the SDK’s empty Models interface with your app’s models.

1) Define your models and export a Models map​

Create a file like types.ts in your app and export a mapping of label → schema, plus any handy derived types:

// types.ts

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

// Optional utility in your app
import { getCurrentISO } from '@/common/utils/getCurrentISO'

export const USER = 'USER' as const

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

// This map is used to teach the SDK about your labels and their schemas
export type Models = {
[USER]: typeof UserModel.schema
}

Notes:

  • The property for uniqueness is unique in the SDK.
  • Adjust any import aliases like @/common/... to match your project setup.

2) Augment the SDK’s Models interface​

Create a project-level declaration file (for example index.d.ts) that merges your Models into the SDK. Make sure this file is included by your tsconfig (see next section):

// index.d.ts (path can be anything, just ensure tsconfig includes it)
import {} from '@rushdb/javascript-sdk'

// Import the map you exported in step 1 (adjust the path for your project)
import { Models as AppModels } from '@/platform-app/types'

declare module '@rushdb/javascript-sdk' {
// This merges your labels/schemas so SearchQuery<Related> gets rich intellisense
export interface Models extends AppModels {}
}

After this merge, places that use Related<Models> (e.g., where clauses with nested related objects) will autocomplete and type-check against your actual fields.

3) TS config tips for great DX​

Ensure TypeScript can find your declaration file and resolve any path aliases used in your imports.

Minimal tsconfig settings to verify:

{
"compilerOptions": {
"strict": true,
"module": "esnext",
"target": "es2020",
"moduleResolution": "bundler", // or "node"/"node16" depending on your toolchain
"baseUrl": ".", // if you use path aliases
"paths": {
"@/*": ["src/*"] // adjust to match your alias usage like '@/...'
}
// Keep this true if you want to validate library .d.ts files too
// "skipLibCheck": false
},
"include": [
"src",
"index.d.ts" // include your augmentation (or its actual path)
]
}

Common gotchas:

  • If the module augmentation isn’t picked up, double-check that the .d.ts file is inside tsconfig’s include (or not excluded).
  • Monorepo/workspaces: put the augmentation in the consuming app/package and ensure that package’s tsconfig includes it.
  • If you use ESM + TS 5’s “verbatimModuleSyntax”, keep imports as type-only where needed.

With the augmentation in place, related objects in where become schema-aware:

const posts = await db.records.find({
labels: ['post'],
where: {
author: {
login: { $contains: 'john' } // autocomplete from Author schema
}
}
})

This setup gives you end-to-end safety: drafts, records, queries, and nested relations all derive types from your single source of truth—the model schemas you define.