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:
- Connector - Connects to a specific storage backend (e.g., IndexedDB).
- Schema - Defines the structure and validation rules for your data.
- Store - Manages collections and provides access to data.
- 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:
- IndexedDBConnector - Uses IndexedDB for storage (recommended for most applications)
- 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
Property | Type | Description |
---|---|---|
type | string | The data type of the field (see supported types below) |
required | boolean | Indicates if the field is required |
primaryKey | boolean | Marks the field as the primary key for the collection |
default | any | Default value for the field if not provided |
minLength | number | Minimum length for string fields |
minimum | number | Minimum value for number fields |
maximum | number | Maximum value for number fields |
pattern | RegExp | Regular expression pattern for string validation |
unique | boolean | Indicates if the field value must be unique in collection |
Supported Data Types
The Schema system supports the following data types:
Type | Description | Valid Values |
---|---|---|
string | Text values | Any string |
number | Numeric values (integers or decimals) | Any number |
boolean | Boolean values | true or false |
timestamp | Date and time values with timezone | Date object or ISO string (e.g., 2025-05-16T14:23:00Z ) |
date | Date values (without time) | Date object or ISO date string (e.g., 2025-05-16 ) |
time | Time values (without date) | String in 24-hour format (e.g., 14:23:00 ) |
datetime-local | Date and time values without timezone | Date 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 validerrors
(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 validationerror
(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
-
Define Clear Schemas: Well-defined schemas ensure data integrity and make your application more maintainable.
-
Use IndexedDB for Larger Applications: IndexedDB provides more storage space and better performance than LocalStorage.
-
Handle Offline Scenarios: Implement sync strategies to handle data that needs to be synchronized with a server when connectivity is restored.
-
Use Events for Side Effects: Use event listeners for logging, caching, and other side effects in response to changes to your data.
-
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.