Skip to content

Configuration

ReActive Record provides a flexible configuration system that allows you to define your database structure, models, and relationships. This guide covers all available configuration options and their usage.

Basic Configuration

The configuration object is passed to the ReactiveDatabase constructor when initializing your database:

typescript
import { ReactiveDatabase } from '@nhtio/web-re-active-record'
import { joi, makeModelConstraints } from '@nhtio/web-re-active-record/constraints'

interface User {
  id: number
  email: string
  name: string
}

interface Post {
  id: number
  user_id: number
  title: string
  content: string
}

const db = new ReactiveDatabase<{
  users: User
  posts: Post
}>({
  namespace: 'my-app',
  version: 1,
  psk: 'your-pre-shared-key',
  models: {
    users: {
      schema: '++id, email, name',
      properties: ['id', 'email', 'name'],
      primaryKey: 'id',
      relationships: {
        posts: [HasMany, 'posts', 'user_id']
      }
    },
    posts: {
      schema: '++id, user_id, title, content',
      properties: ['id', 'user_id', 'title', 'content'],
      primaryKey: 'id',
      relationships: {
        user: [BelongsTo, 'users', 'user_id']
      }
    }
  }
})

Configuration Options

namespace

  • Type: string
  • Required: Yes

The namespace is used as the database name in IndexedDB. Each namespace creates a separate database, allowing you to run multiple instances without conflicts.

typescript
{
  namespace: 'my-app'
}

version

  • Type: number
  • Required: Yes
  • Minimum: 1

The database version number. Increment this when making schema changes to trigger IndexedDB's version change event.

typescript
{
  version: 1
}

psk (Pre-Shared Key)

  • Type: string
  • Required: Yes
  • Minimum Length: 16 characters

A key used for encrypting data when synchronizing between browser contexts. While not meant for security, it should be unique to your application.

typescript
{
  psk: 'your-pre-shared-key-at-least-16-chars'
}

models

  • Type: Record<string, ModelDefinition>
  • Required: Yes

Defines the structure of your database models. Each key represents a table name, and its value defines the model's schema and relationships.

typescript
{
  models: {
    users: {
      schema: '++id, email, name',
      properties: ['id', 'email', 'name'],
      primaryKey: 'id',
    }
  }
}

Model Definition

Each model in the models object requires the following configuration:

schema

  • Type: string
  • Required: Yes

The Dexie schema string that defines the table structure. Uses Dexie's schema syntax:

  • ++ for auto-incrementing fields
  • & for unique fields
  • * for indexed fields
typescript
{
  schema: '++id, &email, *name, createdAt'
}

For more information about the Dexie schema syntax, please see the Dexie Documentation.

Warning

You should only define "Indexable Types" in your schema. Per Dexie's documentation:

Only properties of certain types can be indexed. This includes string, number, Date and Array but NOT boolean, null or undefined.

That means that, unlike in the properties definition, you do not need to define all of the properties of the model.

properties

  • Type: string[]
  • Required: Yes

An array of property names that the model can access. Must include all fields that will be stored in the database.

typescript
{
  properties: ['id', 'email', 'name', 'createdAt']
}

primaryKey

  • Type: string
  • Required: Yes

The property name that serves as the primary key for the model. Must be included in both schema and properties.

typescript
{
  primaryKey: 'id'
}

relationships

  • Type: Record<string, RelationshipConfiguration>
  • Required: No
  • Default: {}

Defines the relationships between models. Each key is the relationship name, and the value is a tuple defining the relationship type and configuration.

typescript
{
  relationships: {
    // One-to-Many: User has many posts
    posts: [HasMany, 'posts', 'user_id'],
    
    // Belongs To: Post belongs to a user
    user: [BelongsTo, 'users', 'user_id'],
    
    // Many-to-Many: User has many roles through user_roles
    roles: [ManyToMany, 'roles', 'user_roles', 'user_id', 'role_id']
  }
}

For more information about relationships, see the Relationships Documentation.

constraints

  • Type: ModelConstraints<T>
  • Required: No
  • Default: undefined

Defines validation constraints for the model using joi schemas. These constraints are checked before saving the model, ensuring data integrity. Use makeModelConstraints to create type-safe validation schemas:

typescript
import { joi, makeModelConstraints } from '@nhtio/web-re-active-record/constraints'

interface User {
  id: number
  email: string
  name: string
  age: number
  status: 'active' | 'inactive'
}

{
  constraints: makeModelConstraints<User>({
    email: joi.string().email().required(),
    name: joi.string().min(2).required(),
    age: joi.number().min(18).required(),
    status: joi.string().valid('active', 'inactive').default('active')
  })
}

Why makeModelConstraints?

The constraints property of a model definition expects to receive a joi.ObjectSchema object, and technically you can use joi.object to create your constraints. ReActive Record provides makeModelConstraints for better compile-time type-safety.

The makeModelConstraints function provides type safety by ensuring that:

  • All constraint keys match your model's properties
  • You can't accidentally add constraints for non-existent properties

makeModelConstraints Strict Mode

By default, makeModelConstraints is in a permissive mode which allows properties which aren't defined in the constraints to exist and not be validated. This is useful for when you only want to make sure that a small subset of your models properties have specific validation constraints. However if you define all of your models properties in your constraints, you can use strict mode validation to ensure that you don't have unexpected properties on your model (somehow). To enable strict mode, simply pass true as the second argument to the makeModelConstraints call

typescript
import { joi, makeModelConstraints } from '@nhtio/web-re-active-record/constraints'

interface User {
  id: number
  email: string
  name: string
  age: number
  status: 'active' | 'inactive'
}

{
  constraints: makeModelConstraints<User>(
    {
      email: joi.string().email().required(),
      name: joi.string().min(2).required(),
      age: joi.number().min(18).required(),
      status: joi.string().valid('active', 'inactive').default('active')
    },
    true // This enables strict mode
  )
}

When a model has constraints defined:

  • Validation occurs automatically before saving
  • If validation fails, a ReactiveModelFailedConstraintsException is thrown

Initial Options

Options under the initial configuration attribute provide a way to configure the initial state of database functionality which you would normally have to wait until you have constructed an instance of the database to configure, such as event subscriptions.

initial.loggers

  • Type: Record<LogLevel, Function[]>
  • Required: No
  • Default: Empty arrays for each log level

Configure level-specific logger callbacks. Available levels: emerg, alert, crit, error, warning, notice, info, debug.

typescript
{
  initial: {
    loggers: {
      error: [(...args) => console.error(...args)],
      info: [(...args) => console.info(...args)],
      debug: [(...args) => console.debug(...args)]
    }
  }
}

For more information about debugging, see the Debugging Documentation.

initial.subscriptions

  • Type: Array<[LogLevel, Function]>
  • Required: No
  • Default: []

Configure severity-based subscriptions that receive logs at or above the specified level. Each subscription is a tuple of [level, callback].

typescript
{
  initial: {
    subscriptions: [
      // Receives emerg, alert, crit, and error logs
      ['error', (msg) => handleSevereIssues(msg)],
      
      // Receives warning and all more severe logs
      ['warning', (msg) => trackIssues(msg)]
    ]
  }
}

For more information about debugging, see the Debugging Documentation.

Advanced Usage

Hooks for Reactive Framework Integration

ReActive Record supports advanced integration with reactive frameworks (such as Vue, Svelte, etc.) by allowing you to define hooks that wrap returned model and query instances. This is useful for scenarios where you want to automatically make models or query results reactive, proxied, or otherwise enhanced for your framework.

You can provide these hooks via the hooks property in your configuration:

typescript
const db = new ReactiveDatabase({
  // ...other config...
  hooks: {
    wrapReactiveModel: (model) => makeReactive(model),
    wrapReactiveQueryCollection: (collection) => makeReactive(collection),
    wrapReactiveQueryResult: (result) => makeReactive(result),
  }
})
  • wrapReactiveModel: Called for every ReactiveModel instance before it is returned to your code. Receives the original model instance and must return the (optionally wrapped) instance.
  • wrapReactiveQueryCollection: Called for every ReactiveQueryCollection instance before it is returned. Receives the original collection instance and must return the (optionally wrapped) instance.
  • wrapReactiveQueryResult: Called for every ReactiveQueryResult instance before it is returned. Receives the original result instance and must return the (optionally wrapped) instance.

Default Behavior

If you do not provide a hook, the default is an identity function (the original instance is returned unmodified).

For more information about advanced integrations, see the Advanced Integrations Documentation

Complete Configuration Example

Here's a complete example showing all configuration options:

typescript
import { ReactiveDatabase } from '@nhtio/web-re-active-record'

const db = new ReactiveDatabase({
  // Database identification
  namespace: 'my-app',
  version: 1,
  psk: 'your-pre-shared-key-at-least-16-chars',

  // Model definitions
  models: {
    users: {
      schema: '++id, &email, name, createdAt',
      properties: ['id', 'email', 'name', 'createdAt'],
      primaryKey: 'id',
      relationships: {
        posts: [HasMany, 'posts', 'user_id'],
        profile: [HasOne, 'profiles', 'user_id'],
        roles: [ManyToMany, 'roles', 'user_roles', 'user_id', 'role_id']
      },
      constraints: makeModelConstraints<User>({
        email: joi.string().email().required(),
        name: joi.string().min(2).required(),
        createdAt: joi.date().default(Date.now)
      })
    },
    posts: {
      schema: '++id, user_id, title, content, publishedAt',
      properties: ['id', 'user_id', 'title', 'content', 'publishedAt'],
      primaryKey: 'id',
      relationships: {
        user: [BelongsTo, 'users', 'user_id'],
        comments: [HasMany, 'comments', 'post_id']
      },
      constraints: makeModelConstraints<Post>({
        title: joi.string().min(3).max(100).required(),
        content: joi.string().required(),
        publishedAt: joi.date().allow(null)
      })
    }
  },

  // Initial configuration
  initial: {
    // Level-specific loggers
    loggers: {
      error: [(...args) => console.error(...args)],
      info: [(...args) => console.info(...args)],
      debug: process.env.NODE_ENV === 'development' 
        ? [(...args) => console.debug(...args)]
        : []
    },
    // Severity-based subscriptions
    subscriptions: [
      // Monitor all severe issues (emerg, alert, crit, error)
      ['error', (...args) => errorTrackingService.capture(...args)],
      // Track warnings and above for analytics
      ['warning', (...args) => analyticsService.track(...args)]
    ]
  }
})

Type Safety

ReActive Record is built with TypeScript and provides full type safety for your models. Define your model types and pass them to the ReactiveDatabase constructor:

typescript
interface User {
  id: number
  email: string
  name: string
  createdAt: Date
}

interface Post {
  id: number
  userId: number
  title: string
  content: string
  publishedAt: Date
}

const db = new ReactiveDatabase<{
  users: User
  posts: Post
}>({
  // ... configuration as above
})

This ensures type safety when querying and manipulating your models:

typescript
// Fully typed - TypeScript knows the shape of user
const user = await db.model('users').find(1)
console.log(user.name) // TypeScript knows this is a string

For more information about type safety, see the Typescript Documentation.