Skip to main content

Storage

The RADFish storage system provides a robust and flexible solution for managing data in your applications. It's designed to work seamlessly with offline-first applications, providing a consistent API for storing and retrieving data from device storage.

Overview

The storage system is built around several key components that work together:

  1. Connector - Connects to a specific storage backend (e.g., IndexedDB).
  2. Schema - Defines the structure and validation rules for your data.
  3. Store - Manages collections and provides access to data.
  4. Collection - Handles CRUD operations for a specific type of data.

Setting Up Storage in an Application

The most common way to set up storage in a RADFish application is through the Application constructor. This approach automatically handles connecting to the storage backend and setting up collections.

import { Application } from "@nmfs-radfish/radfish";
import { IndexedDBConnector } from "@nmfs-radfish/radfish/storage";

const app = new Application({
stores: {
weatherSurvey: {
connector: new IndexedDBConnector("weather-survey-app"),
collections: {
Form: {
schema: {
fields: {
id: {
type: "string",
primaryKey: true,
},
surveyDate: {
type: "string",
required: true,
},
submittedBy: {
type: "string",
required: true,
},
submittedAt: {
type: "timestamp",
},
status: {
type: "string",
required: true,
},
},
},
},
},
},
}
});

Key Components

Connector

A Connector is responsible for connecting to a specific storage backend and providing an interface for collections to interact with that backend.

RADFish provides two built-in connectors:

  1. IndexedDBConnector - Uses IndexedDB for storage (recommended for most applications)
  2. LocalStorageConnector - Uses localStorage for storage (limited capacity, but simpler)
import { IndexedDBConnector } from "@nmfs-radfish/radfish/storage";

const connector = new IndexedDBConnector("weather-survey-app");

Schema

A Schema defines the structure and validation rules for a collection of data. It defines all the fields, their types, and constraints for a particular type of data.

const formSchema = {
fields: {
id: {
type: "string",
primaryKey: true,
},
surveyDate: {
type: "string",
required: true,
},
submittedBy: {
type: "string",
required: true,
},
submittedAt: {
type: "timestamp",
},
// ... other fields
},
};

Schema Field Properties

PropertyTypeDescription
typestringThe data type of the field (see supported types below)
requiredbooleanIndicates if the field is required
primaryKeybooleanMarks the field as the primary key for the collection
defaultanyDefault value for the field if not provided
minLengthnumberMinimum length for string fields
minimumnumberMinimum value for number fields
maximumnumberMaximum value for number fields
patternRegExpRegular expression pattern for string validation
uniquebooleanIndicates if the field value must be unique in collection

Supported Data Types

The Schema system supports the following data types:

TypeDescriptionValid Values
stringText valuesAny string
numberNumeric values (integers or decimals)Any number
booleanBoolean valuestrue or false
timestampDate and time values with timezoneDate object or ISO string (e.g., 2025-05-16T14:23:00Z)
dateDate values (without time)Date object or ISO date string (e.g., 2025-05-16)
timeTime values (without date)String in 24-hour format (e.g., 14:23:00)
datetime-localDate and time values without timezoneDate object or ISO string (e.g., 2025-05-16T14:23:00)

Schema Validation

The Schema class provides a validate method to validate data against the schema rules. It can be used to check if data is valid before attempting to save it to a collection.

// Get a collection's schema
const schema = formCollection.schema;

// Data to validate
const data = {
surveyDate: "2025-05-16",
submittedBy: "John Smith",
status: "draft",
};

// Validate data (non-strict mode)
const validation = schema.validate(data);

if (!validation.isValid) {
console.error("Validation errors:", validation.errors);
// Example output: [{field: "id", error: "Field is required"}]
} else {
// Data is valid
await formCollection.create(data);
}

The validate method returns an object with two properties:

  • isValid (boolean): Indicates if the data is valid
  • errors (array): List of validation error objects if any

Each error object in the errors array has the following format:

  • field (string): The name of the field that failed validation
  • error (string): A description of the validation error
Strict Mode Validation

You can also use strict mode, which throws a ValidationError instead of returning an error object:

try {
// Passing 'true' as the second parameter enables strict mode
schema.validate(data, true);

// If validation passes, create the record
await formCollection.create(data);
} catch (error) {
if (error.name === "ValidationError") {
console.error("Validation errors:", error.errors);
// Handle validation errors
} else {
// Handle other errors
console.error("Error:", error.message);
}
}

Store

A Store manages collections for a specific domain of your application. Each store has a connector and can have multiple collections.

import { Store } from "@nmfs-radfish/radfish/storage";

const store = new Store({
name: "weatherSurvey",
connector: connector
});

Collection

A Collection provides methods for creating, reading, updating, and deleting data for a specific schema. Collections validate data against their schema before performing any operations.

// Get a collection from the Application
const formCollection = app.stores.weatherSurvey.getCollection("Form");

// Create a new form
const newForm = await formCollection.create({
surveyDate: "2025-05-16",
submittedBy: "John Smith",
status: "draft"
});

// Find forms
const draftForms = await formCollection.find({ status: "draft" });

// Update a form
const updatedForm = await formCollection.update({
id: newForm.id,
status: "submitted",
submittedAt: new Date().toISOString()
});

// Delete a form
await formCollection.delete({ id: newForm.id });

Working with Collections

Creating Records

To create a new record, call the create method on a collection:

const newForm = await formCollection.create({
surveyDate: "2025-05-16",
submittedBy: "John Smith",
status: "draft"
});

console.log("Created form with ID:", newForm.id);

Finding Records

To retrieve records from a collection, use the find method:

// Find all draft forms
const draftForms = await formCollection.find({ status: "draft" });

// Find a specific form by ID
const form = await formCollection.find({ id: "form-123" });

Updating Records

To update a record, use the update method:

const updatedForm = await formCollection.update({
id: "form-123",
status: "submitted",
submittedAt: new Date().toISOString()
});

Deleting Records

To delete records, use the delete method:

// Delete a specific form
await formCollection.delete({ id: "form-123" });

// Delete all draft forms
await formCollection.delete({ status: "draft" });

Event Handling

The storage system emits events during various operations, allowing you to hook into the lifecycle of data operations.

// Listen for events at the collection level
formCollection.addEventListener('beforeCreate', (event) => {
// Add defaults if needed
if (!event.detail.data.status) {
event.detail.data.status = "draft";
}

console.log('About to create form:', event.detail.data);
});

formCollection.addEventListener('create', (event) => {
console.log('Form created:', event.detail.data);
});

// Listen for events at the connector level (across all collections)
connector.addEventListener('create', (event) => {
console.log(`Created record in ${event.detail.schema.name}:`, event.detail.data);
});

Best Practices

  1. Define Clear Schemas: Well-defined schemas ensure data integrity and make your application more maintainable.

  2. Use IndexedDB for Larger Applications: IndexedDB provides more storage space and better performance than LocalStorage.

  3. Handle Offline Scenarios: Implement sync strategies to handle data that needs to be synchronized with a server when connectivity is restored.

  4. Use Events for Side Effects: Use event listeners for logging, caching, and other side effects in response to changes to your data.

  5. Validate User Input: While collections validate data against schemas, it's still good practice to validate user input before calling collection methods.

Example: Complete Weather Survey Application

Here's an example of how to set up a complete weather survey application with forms:

import { Application } from "@nmfs-radfish/radfish";
import { IndexedDBConnector } from "@nmfs-radfish/radfish/storage";

// Initialize the application with storage
const app = new Application({
stores: {
weatherSurvey: {
connector: new IndexedDBConnector("weather-survey-app"),
collections: {
Form: {
schema: {
fields: {
id: {
type: "string",
primaryKey: true,
},
surveyDate: {
type: "string",
required: true,
},
submittedBy: {
type: "string",
required: true,
},
submittedAt: {
type: "timestamp",
},
status: {
type: "string",
required: true,
},
},
},
},
},
},
}
});

// When the application is ready
app.on("ready", async () => {
// Get collections
const formCollection = app.stores.weatherSurvey.getCollection("Form");

// Create a new form
const newForm = await formCollection.create({
surveyDate: "2025-05-16",
submittedBy: "John Smith",
status: "draft"
});

// Mark the form as submitted
await formCollection.update({
id: newForm.id,
status: "submitted",
submittedAt: new Date().toISOString()
});
});

By following these patterns, you can create powerful applications that work seamlessly online and offline, providing a consistent user experience regardless of network conditions.