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:
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.
{
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.
{
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.
{
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.
{
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
{
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.
{
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
.
{
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.
{
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:
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
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
.
{
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]
.
{
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:
const db = new ReactiveDatabase({
// ...other config...
hooks: {
wrapReactiveModel: (model) => makeReactive(model),
wrapReactiveQueryCollection: (collection) => makeReactive(collection),
wrapReactiveQueryResult: (result) => makeReactive(result),
}
})
wrapReactiveModel
: Called for everyReactiveModel
instance before it is returned to your code. Receives the original model instance and must return the (optionally wrapped) instance.wrapReactiveQueryCollection
: Called for everyReactiveQueryCollection
instance before it is returned. Receives the original collection instance and must return the (optionally wrapped) instance.wrapReactiveQueryResult
: Called for everyReactiveQueryResult
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:
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:
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:
// 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.