JSON-Database-ST

A Secure, Simple, and Fast JSON File Database for Node.js

What is it?

JSON-Database-ST is a lightweight, promise-based database that uses a single JSON file for storage. It's designed for projects that need simple, persistent data storage without the overhead of a traditional database server. With a focus on security, performance, and developer experience, it includes features like atomic operations, data indexing, schema validation, and built-in encryption.

Key Features

  • 🔒 Secure by Default

    Built-in AES-256-GCM encryption at rest and path traversal protection to keep your data safe.

  • ⚡ Fast Indexed Lookups

    Create indexes on your data to retrieve records instantly (O(1)) instead of slow scanning.

  • 🤝 Atomic Operations

    All writes (set, push, batch, transaction) are atomic, ensuring data integrity even during concurrent operations.

  • ✅ Schema Validation

    Integrate with validation libraries like Zod or Joi to enforce data structures and prevent bad data.

  • 🕊️ Modern Promise-Based API

    A clean, async/await-friendly API that is intuitive and easy to use.

  • 📢 Event-Driven

    Emits events for 'write', 'change', and 'error', allowing for reactive programming and auditing.

Installation

# Required peer dependency
npm install lodash

npm install json-database-st

Quick Start

const JSONDatabase = require('./JSONDatabase');

const db = new JSONDatabase('./my-secure-db.json', {
  // IMPORTANT: Store this key in environment variables, not in code!
  encryptionKey: 'd0a7e8c1b2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9',
});

async function main() {
  await db.set('users.alice', { name: 'Alice', age: 30, tags: ['active'] });
  const alice = await db.get('users.alice');
  console.log(alice); // -> { name: 'Alice', age: 30, tags: ['active'] }

  await db.transaction(data => {
    data.users.alice.age++;
    return data;
  });
  
  console.log(await db.get('users.alice.age')); // -> 31
  await db.close();
}
main();

API Reference

new JSONDatabase(filename, [options])

Creates a new database instance.

.get(path, [defaultValue])

Retrieves a value from the database using a lodash path. Returns the default value if the path does not exist.

const theme = await db.get('config.theme', 'light');

.set(path, value)

Atomically sets or replaces a value at a specific path.

await db.set('users.bob', { name: 'Bob', age: 40 });

.has(path)

Checks if a path exists in the database. Returns true even if the value at the path is `null` or `undefined`.

if (await db.has('users.bob')) {
  console.log('Bob exists!');
}

.delete(path)

Atomically deletes a property at a specified path. Returns `true` if the property existed and was deleted.

const wasDeleted = await db.delete('users.temporary');
if (wasDeleted) console.log('Temporary user cleaned up.');

.push(path, ...items)

Pushes one or more unique items into an array at a given path. Creates the array if it doesn't exist. Uses deep comparison for uniqueness.

await db.push('users.alice.tags', 'verified', 'premium');

.pull(path, ...itemsToRemove)

Removes one or more items from an array at a given path. Uses deep comparison to find items to remove.

await db.pull('users.alice.tags', 'active');

.transaction(asyncFn)

Performs a complex, multi-step operation atomically. The provided function receives a deep clone of the data and MUST return the modified data object.

await db.transaction(async (data) => {
  data.users.alice.logins = (data.users.alice.logins || 0) + 1;
  data.users.alice.lastLogin = Date.now();
  return data; // IMPORTANT: must return the data
});

.batch(ops, [options])

Executes multiple simple operations atomically in a single disk write for high performance.

await db.batch([
  { type: 'set', path: 'users.jane', value: { age: 30 } },
  { type: 'push', path: 'users.jane.hobbies', values: ['reading'] },
  { type: 'delete', path: 'users.oldUser' }
]);

.find(collectionPath, predicate)

Finds the first entry in a collection (an object or array) that satisfies the predicate function.

const adminUser = await db.find('users', (user, key) => {
  return user.tags.includes('admin');
});

.findByIndex(indexName, value)

Instantly finds an object in a collection using a pre-configured index. This is the fastest way to look up data.

Requires setup in constructor:
const db = new JSONDatabase('db', {
  indices: [{ name: 'user-email', path: 'users', field: 'email', unique: true }]
});

// Later...
const user = await db.findByIndex('user-email', 'alice@example.com');

.clear()

Clears the entire database content, replacing it with an empty object (`{}`). Use with caution!

await db.clear();
console.log('Database has been reset.');

.getStats()

Returns a synchronous object containing operational statistics for the database instance.

const stats = db.getStats();
console.log(`DB writes: ${stats.writes}, DB reads: ${stats.reads}`);

.close()

Waits for any pending write operations to complete, then closes the database instance. Call this for a graceful shutdown.

await db.close();
console.log('Database connection closed safely.');