stored-data-object is a lightweight JSON-based data persistence Node library (JS only), ideal for prototypes, demos, or small applications. It supports schema validation, type safety and automatically handles JSON file operations.
npm install stored-data-objectimport SDO from 'stored-data-object';
// Define schema
const settingsSchema = SDO.schema({
theme: 'string',
fontSize: 'number',
notifications: 'boolean?',
});
// Create or open file
const settings = await SDO.create({
file: './data/settings.json',
schema: settingsSchema,
default: { theme: 'dark', fontSize: 14 },
});
// Use it
settings.data.theme = 'light';
await settings.write();
// Reload from file
await settings.reload();
// Reset to default
await settings.reset();const userSchema = SDO.schema({
id: 'number',
name: 'string',
email: 'string',
active: 'boolean',
});
// Define schema with array of users
const usersSchema = SDO.schema({
users: [userSchema], // Array of user objects
lastUpdated: 'number',
});
const db = await SDO.create({
file: './data/users.json',
schema: usersSchema,
default: { users: [], lastUpdated: Date.now() },
});
// Add new user
db.data.users.push({
id: 1,
name: 'Alice',
email: 'alice@example.com',
active: true,
});
db.data.lastUpdated = Date.now();
await db.write();const profileSchema = SDO.schema({
user: {
name: 'string',
age: 'number?',
contact: {
email: 'string',
phone: 'string?',
},
},
preferences: {
theme: 'string',
language: 'string',
},
});
const profile = await SDO.create({
file: './data/profile.json',
schema: profileSchema,
});
profile.data.user.name = 'Bob';
profile.data.user.contact.email = 'bob@example.com';
await profile.write();Define a schema for type inference and validation. Returns the schema object itself (used for TypeScript/JSDoc type inference).
Parameters:
schemaDef— Object defining the data structure (see Schema Types section)
Returns:
- Schema definition object (as-is)
Example:
const mySchema = SDO.schema({
id: 'number',
name: 'string',
tags: ['string'], // Array of strings
});Create or open a stored data object from a JSON file. If the file doesn't exist, it will be automatically created with default values.
config Parameters:
file: string— Path to JSON file (relative or absolute)schema: SchemaDefinition— Schema defining the data structuredefault?: any— Initial value when file doesn't exist (if not provided, uses default values generated from schema)
options Parameters (optional):
encoding?: BufferEncoding— File encoding, defaults to'utf8'autoValidate?: boolean— Automatically validate data, defaults totrue
Returns:
Promise resolving to an object with properties:
{
data: T, // Data typed according to schema
filePath: string, // Absolute path to file
write(): Promise<void>, // Write data to file
reload(): Promise<void>, // Reload from file
reset(newDefault?: T): Promise<void> // Reset to default value
}Example:
const store = await SDO.create({
file: './data.json',
schema: SDO.schema({ count: 'number' }),
default: { count: 0 },
});Write current data to file. If autoValidate: true, validates before writing.
Returns: Promise<void>
Throws: Error if validation fails (when autoValidate: true)
Example:
store.data.count = 42;
await store.write();Reload data from file and update store.data in-place (preserving object references).
Returns: Promise<void>
Note: This method updates the current object reference rather than creating a new object, ensuring other components holding references continue to work correctly.
Example:
const dataRef = store.data;
await store.reload();
console.log(dataRef === store.data); // true - same referenceReset data to original default value (or newDefault if provided) and write to file.
Parameters:
newDefault?: T— New value to reset to (optional)
Returns: Promise<void>
Example:
// Reset to original default
await store.reset();
// Reset to new value
await store.reset({ count: 100 });Schemas define data structure and types. Each property can be:
| Schema Type | TypeScript Type | Default Value | Description |
|---|---|---|---|
'string' |
string |
'' |
Required string |
'string?' |
string | undefined |
undefined |
Optional string |
'number' |
number |
0 |
Required number |
'number?' |
number | undefined |
undefined |
Optional number |
'boolean' |
boolean |
false |
Required boolean |
'boolean?' |
boolean | undefined |
undefined |
Optional boolean |
To define arrays, use the syntax [itemSchema]:
const schema = SDO.schema({
tags: ['string'], // Array of strings
scores: ['number'], // Array of numbers
items: [
{
// Array of objects
id: 'number',
name: 'string',
},
],
});Note: Array schema must be a tuple with exactly 1 element (the item schema).
const schema = SDO.schema({
user: {
// Nested object
profile: {
// Deeply nested
name: 'string',
age: 'number?',
},
settings: {
theme: 'string',
},
},
});const blogSchema = SDO.schema({
posts: [
{
id: 'number',
title: 'string',
content: 'string',
published: 'boolean',
tags: ['string'],
author: {
name: 'string',
email: 'string',
},
metadata: {
views: 'number',
likes: 'number',
createdAt: 'number',
},
},
],
config: {
siteName: 'string',
postsPerPage: 'number',
},
});Use $record when you need dynamic keys (like a dictionary / map).
const schema = SDO.schema({
users: {
$record: {
name: 'string',
age: 'number',
},
},
});Resulting Type:
{
users: Record<string, {
name: string
age: number
}>
}Example Usage:
const db = await SDO.create({
file: './users.json',
schema: SDO.schema({
users: {
$record: {
name: 'string',
age: 'number',
},
},
}),
default: { users: {} },
});
db.data.users['user_1'] = { name: 'Alice', age: 25 };
db.data.users['user_2'] = { name: 'Bob', age: 30 };
await db.write();const schema = SDO.schema({
flags: {
$record: 'boolean',
},
});- Keys are unknown (user IDs, cache keys, etc.)
- Need O(1) lookup instead of array
.find() - Data behaves like a dictionary / hashmap
- Need ordered data → use array
- Need fixed structure → use object schema
When autoValidate: true (default), data is validated in these cases:
- During initialization - Validates
defaultvalue - When reading file - Validates data from file
- Before writing - Validates
store.databeforewrite() - When resetting - Validates
newDefaultvalue
Use $record when you need dynamic keys (like a dictionary / map).
const schema = SDO.schema({
users: {
$record: {
name: 'string',
age: 'number',
},
},
});Resulting Type:
{
users: Record<string, {
name: string
age: number
}>
}Example Usage:
const db = await SDO.create({
file: './users.json',
schema: SDO.schema({
users: {
$record: {
name: 'string',
age: 'number',
},
},
}),
default: { users: {} },
});
db.data.users['user_1'] = { name: 'Alice', age: 25 };
db.data.users['user_2'] = { name: 'Bob', age: 30 };
await db.write();const schema = SDO.schema({
flags: {
$record: 'boolean',
},
});- Keys are unknown (user IDs, cache keys, etc.)
- Need O(1) lookup instead of array
.find() - Data behaves like a dictionary / hashmap
- Need ordered data → use array
- Need fixed structure → use object schema
When validation fails, the error message specifies:
- Which field has the error
- Expected type vs actual type
- Current value (JSON)
Example errors:
Field 'user.age' must be a number, got string: "25"
Field 'items[2].active' must be a boolean, got undefined
To disable validation (not recommended), set autoValidate: false:
const store = await SDO.create(
{
file: './data.json',
schema: mySchema,
},
{
autoValidate: false, // Disable validation
}
);The library implements in-process file locking to ensure operations (write, reload, reset) don't race within the same Node.js process.
Note: This is not inter-process locking. If multiple processes access the same file, you need an external solution (like a database or external locking mechanism).
If the file doesn't exist:
- Automatically creates parent directories (recursive)
- Creates file with
defaultvalue or default values from schema - Formats JSON with indentation (tabs)
When reload() or reset() is called, data is updated in-place instead of creating a new object:
const store = await SDO.create({
file: './data.json',
schema: SDO.schema({ count: 'number' }),
});
const ref1 = store.data;
await store.reload();
const ref2 = store.data;
console.log(ref1 === ref2); // true - same referenceThis is important when multiple parts of your application hold references to store.data.
If file contains invalid JSON:
try {
const store = await SDO.create({
file: './corrupted.json',
schema: mySchema,
});
} catch (error) {
// Error: Invalid JSON in file: /path/to/corrupted.json. Unexpected token...
}If data doesn't match schema (with autoValidate: true):
try {
await store.write();
} catch (error) {
// Error: Data validation failed before write: Field 'age' must be a number, got string: "25"
}If there are no read/write permissions:
try {
const store = await SDO.create({
file: '/root/protected.json',
schema: mySchema,
});
} catch (error) {
// Error: EACCES: permission denied
}// Good ✓
const schema = SDO.schema({
name: 'string',
age: 'number',
});
// OK but loses type inference
const schema = {
name: 'string',
age: 'number',
};// Good ✓
store.data.count++;
await store.write();
// Bad ✗ - Changes not persisted
store.data.count++;
// ... other codetry {
await store.write();
} catch (error) {
console.error('Failed to save:', error.message);
// Rollback or retry
}const store = await SDO.create({
file: './data.json',
schema: SDO.schema({
users: [{ id: 'number', name: 'string' }],
settings: { theme: 'string' },
}),
default: {
users: [], // Empty array ready to use
settings: { theme: 'light' }, // Has default value
},
});// Good ✓ - Simple, clear
store.data.count = 10;
await store.write();
// Risky ⚠ - Deep mutation, hard to track
const deepRef = store.data.nested.deeply.buried;
deepRef.value = 'changed';
await store.write();const config = await SDO.create({
file: './config.json',
schema: SDO.schema({
apiUrl: 'string',
timeout: 'number',
retries: 'number',
debug: 'boolean?',
}),
default: {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
},
});const db = await SDO.create({
file: './todos.json',
schema: SDO.schema({
todos: [
{
id: 'number',
text: 'string',
completed: 'boolean',
createdAt: 'number',
},
],
}),
default: { todos: [] },
});
// CRUD operations
const addTodo = async (text) => {
db.data.todos.push({
id: Date.now(),
text,
completed: false,
createdAt: Date.now(),
});
await db.write();
};
const toggleTodo = async (id) => {
const todo = db.data.todos.find((t) => t.id === id);
if (todo) {
todo.completed = !todo.completed;
await db.write();
}
};const cache = await SDO.create({
file: './cache.json',
schema: SDO.schema({
entries: [
{
key: 'string',
value: 'string',
expiry: 'number',
},
],
}),
default: { entries: [] },
});
const setCache = async (key, value, ttl = 3600000) => {
const idx = cache.data.entries.findIndex((e) => e.key === key);
const entry = {
key,
value: JSON.stringify(value),
expiry: Date.now() + ttl,
};
if (idx >= 0) {
cache.data.entries[idx] = entry;
} else {
cache.data.entries.push(entry);
}
await cache.write();
};
const getCache = (key) => {
const entry = cache.data.entries.find((e) => e.key === key && e.expiry > Date.now());
return entry ? JSON.parse(entry.value) : null;
};- Not suitable for production apps with high traffic or large datasets
- No inter-process locking - Not safe when multiple processes access the same file
- No transactions - Changes are applied immediately, no automatic rollback
- No indexing - Array searches are O(n)
- No query language - Must use JavaScript to filter/find
- File-based - Performance depends on filesystem
If you need these features, consider a real database (SQLite, PostgreSQL, MongoDB, etc.)
The library is written with JSDoc and provides full type inference for TypeScript:
import SDO from 'stored-data-object';
const schema = SDO.schema({
count: 'number',
name: 'string',
active: 'boolean?',
});
const store = await SDO.create({
file: './data.json',
schema,
});
// TypeScript knows exact types:
store.data.count; // number
store.data.name; // string
store.data.active; // boolean | undefinedMIT