npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

gadz

v0.1.0

Published

A MongoDB-compatible API with SQLite backend for TypeScript applications

Readme

🚀 Gadz

License: MIT Node.js TypeScript SQLite Jest

A MongoDB-compatible API with SQLite backend for TypeScript applications. Provides type-safe document storage with familiar MongoDB query syntax.

Built for Node.js 18+ with better-sqlite3 for optimal performance and reliability.

✨ Features

  • MongoDB-Style API: Use familiar MongoDB operations like find(), save(), updateMany()
  • Type Safety: Full TypeScript support with generic types
  • SQLite Backend: Fast, embedded database with WAL mode for concurrent access (powered by better-sqlite3)
  • Multi-Process Ready: Optimized for concurrent access across multiple Node.js processes
  • Collection Class: ActiveRecord-style ORM pattern
  • Query Operators: Support for $gt, $lt, $in, $exists, and more
  • Built-in Validation: Automatic document validation with _validate() method and uniqueness checks
  • Transactions: Built-in transaction support with nested transaction capability

📋 Requirements

  • Node.js: 18.0.0 or higher
  • TypeScript: 5.0.0 or higher (peer dependency)

📦 Installation

npm install gadz

🎯 Quick Start

Functional API

import { connect, save, find, get, where } from "gadz";

// Connect to database
await connect({ path: "db/app.db" });

// Define your model
class User {
  email: string;
  name?: string;
  age: number = 18;
  active: boolean = true;

  constructor(data: { email: string; name?: string; age?: number }) {
    this.email = data.email;
    this.name = data.name;
    if (data.age) this.age = data.age;
  }
}

// Save documents
const user = new User({ email: "[email protected]", age: 25 });
const saved = await save(user);

// Query documents
const users = await find<User>({ age: { $gt: 18 } });
const user = await get<User>(User, 1);

// Raw SQL WHERE clauses
const adults = await where(User, "age > ?", [18]);
const activeUsers = await where(User, "active = ? AND age > ?", [true, 21]);

Collection Class API

import { Collection, connect } from "gadz";

await connect({ path: "db/app.db" });

class User extends Collection<User> {
  email: string;
  name?: string;
  age: number;
  active: boolean;

  constructor(data: { email: string; name?: string; age: number; active?: boolean }) {
    super();
    this.email = data.email;
    this.name = data.name;
    this.age = data.age;
    this.active = data.active ?? true;
  }
}

// Static methods
const users = await User.find({ age: { $gt: 18 } });
const user = await User.findOne({ email: "[email protected]" });
const found = await User.get(1);

// Raw SQL WHERE clauses (also available on Collection classes)
const adults = await User.where("age > ?", [18]);
const activeAdults = await User.where("age > ? AND active = ?", [18, true]);

// Instance methods
const user = new User({ email: "[email protected]", age: 30 });
await user.save();
await user.delete();
await user.reload();

// Bulk operations
await User.saveMany(user1, user2, user3);
await User.updateMany({ active: false }, { $set: { active: true } });
await User.deleteMany({ age: { $lt: 18 } });

🔍 Query Operators

| Operator | Description | Example | |----------|-------------|---------| | $eq | Equal to | { age: { $eq: 25 } } | | $ne | Not equal to | { status: { $ne: "inactive" } } | | $gt | Greater than | { age: { $gt: 18 } } | | $gte | Greater than or equal | { age: { $gte: 21 } } | | $lt | Less than | { age: { $lt: 65 } } | | $lte | Less than or equal | { age: { $lte: 64 } } | | $in | In array | { status: { $in: ["active", "pending"] } } | | $nin | Not in array | { role: { $nin: ["admin", "super"] } } | | $exists | Field exists | { phone: { $exists: true } } |

🎯 Raw SQL WHERE Clauses

The where() method supports both MongoDB-style filters and raw SQL WHERE clauses with automatic JSON field conversion. This gives you the flexibility to write SQL-like queries while maintaining type safety.

Basic WHERE Clauses

import { where } from "gadz";

// Simple field comparisons (automatically converted to JSON_EXTRACT)
const adults = await where(User, "age > ?", [18]);
const activeUsers = await where(User, "active = ?", [true]);
const gmailUsers = await where(User, "email LIKE ?", ["%@gmail.com"]);

// Complex conditions
const eligibleUsers = await where(User, "age >= ? AND active = ?", [21, true]);
const searchResults = await where(User, "name LIKE ? OR email LIKE ?", ["%john%", "%john%"]);

Nested Field Access

// Dot notation for nested objects
const verifiedUsers = await where(User, "profile.verified = ?", [true]);
const nyUsers = await where(User, "address.city = ?", ["New York"]);
const premiumMembers = await where(User, "subscription.tier = ? AND subscription.active = ?", ["premium", true]);

Advanced SQL Operations

// IN and NOT IN operations
const teamMembers = await where(User, "role IN (?, ?)", ["admin", "moderator"]);
const excludedUsers = await where(User, "status NOT IN (?, ?)", ["banned", "suspended"]);

// NULL checks
const usersWithPhones = await where(User, "phone IS NOT NULL");
const incompleteProfiles = await where(User, "profile.bio IS NULL");

// Complex boolean logic
const targetAudience = await where(User, "(age BETWEEN ? AND ?) AND (active = ? OR premium = ?)", [25, 45, true, true]);

Query Options

// Combine WHERE clauses with sorting, limiting, and pagination
const results = await where(
  User, 
  "age > ? AND active = ?", 
  [18, true], 
  { 
    sort: { created_at: -1 }, 
    limit: 10, 
    skip: 20 
  }
);

// Alternative syntax when no params needed
const allActive = await where(User, "active = 1", [], { sort: { name: 1 } });

Automatic JSON_EXTRACT Conversion

Behind the scenes, Gadz automatically converts field references to proper SQLite JSON operations:

// You write this:
await where(User, "age > ? AND profile.verified = ?", [25, true]);

// Gadz converts it to:
// WHERE CAST(JSON_EXTRACT(data, '$.age') AS REAL) > ? AND JSON_EXTRACT(data, '$.profile.verified') = ?

System Columns

System columns (id, created_at, updated_at) are used directly without JSON conversion:

// Direct column access (no JSON_EXTRACT needed)
const recentUsers = await where(User, "created_at > ?", ["2024-01-01"]);
const specificUser = await where(User, "id = ?", [123]);
const updatedRecently = await where(User, "updated_at > ? AND active = ?", ["2024-12-01", true]);

WHERE Keyword Handling

The method intelligently handles the WHERE keyword:

// These are all equivalent:
await where(User, "age > ?", [18]);           // Adds WHERE automatically
await where(User, "WHERE age > ?", [18]);     // Uses as-is
await where(User, "where age > ?", [18]);     // Case-insensitive detection

Mixing with MongoDB-Style Queries

You can still use the original MongoDB-style syntax - both approaches work seamlessly:

// MongoDB-style (original)
const users1 = await where(User, { age: { $gt: 18 }, active: true });

// SQL-style (new)
const users2 = await where(User, "age > ? AND active = ?", [18, true]);

// Both return the same results!

✅ Validation

Gadz supports automatic document validation through the _validate() method. When you define this method in your classes, it will be automatically called before saving documents.

Basic Validation

class User {
  email: string;
  age: number;
  name?: string;

  constructor(data: { email: string; age: number; name?: string }) {
    this.email = data.email;
    this.age = data.age;
    this.name = data.name;
  }

  async _validate(): Promise<void> {
    // Required field validation
    if (!this.email || this.email.length === 0) {
      throw new Error("Email is required");
    }

    // Business logic validation
    if (this.age < 0 || this.age > 120) {
      throw new Error("Age must be between 0 and 120");
    }

    // Email format validation
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(this.email)) {
      throw new Error("Invalid email format");
    }
  }
}

// Validation runs automatically on save
const user = new User({ email: "invalid-email", age: 25 });
await save(user); // Throws: "Invalid email format"

Uniqueness Validation

Use the isUnique() helper function to ensure field uniqueness:

import { isUnique } from "gadz";

class User {
  email: string;
  username: string;

  constructor(data: { email: string; username: string }) {
    this.email = data.email;
    this.username = data.username;
  }

  async _validate(): Promise<void> {
    // Check email uniqueness
    const emailUnique = await isUnique(this, "email");
    if (!emailUnique) {
      throw new Error(`Email '${this.email}' already exists`);
    }

    // Check username uniqueness
    const usernameUnique = await isUnique(this, "username");
    if (!usernameUnique) {
      throw new Error(`Username '${this.username}' is taken`);
    }
  }
}

Validation Return Types

Your _validate() method can:

  1. Throw an Error: Validation fails with specific message
  2. Return false: Generic validation failure
  3. Return nothing/undefined: Validation passes
  4. Return truthy value: Validation passes
class Product {
  name: string;
  price: number;

  constructor(data: { name: string; price: number }) {
    this.name = data.name;
    this.price = data.price;
  }

  async _validate(): Promise<boolean> {
    // Return false for generic failure
    if (this.price <= 0) return false;
    
    // Return true for success
    return this.name.length > 0;
  }
}

Validation with Updates

The isUnique() function automatically handles updates by excluding the current document from uniqueness checks:

// Save initial user
const user = new User({ email: "[email protected]", username: "john" });
const saved = await save(user);

// Update same user - validation passes
saved.username = "john_updated";
await save(saved); // ✅ Works - excludes current document from uniqueness check

// Try to create new user with same email
const newUser = new User({ email: "[email protected]", username: "different" });
await save(newUser); // ❌ Throws: Email already exists

Inheritance and Custom Validation

class BaseUser {
  email: string;
  
  constructor(data: { email: string }) {
    this.email = data.email;
  }

  async _validate(): Promise<void> {
    if (!this.email) {
      throw new Error("Email is required");
    }
  }
}

class AdminUser extends BaseUser {
  permissions: string[];

  constructor(data: { email: string; permissions: string[] }) {
    super(data);
    this.permissions = data.permissions;
  }

  async _validate(): Promise<void> {
    // Call parent validation first
    await super._validate();

    // Add admin-specific validation
    if (!this.permissions || this.permissions.length === 0) {
      throw new Error("Admin users must have at least one permission");
    }

    if (!this.permissions.includes('admin')) {
      throw new Error("Admin users must have admin permission");
    }
  }
}

When Validation Runs

  • ✅ Automatically before save() operations
  • ✅ Automatically before saveMany() operations
  • ✅ Works with both functional API and Collection class
  • ❌ Does not run on updateMany() operations (by design)

Optional Validation

If a class doesn't define _validate(), documents save without validation:

class SimpleData {
  value: string;
  
  constructor(data: { value: string }) {
    this.value = data.value;
  }
  
  // No _validate method - saves without validation
}

const data = new SimpleData({ value: "" });
await save(data); // ✅ Saves successfully

⚙️ Configuration

import { connect } from "gadz";

await connect({
  path: "db/production.db",           // Database file path
  maintenanceIntervalMs: 300000       // Maintenance interval (optional)
});

Multi-Process Support

Gadz is optimized for multi-process environments (PM2, cluster mode, etc.):

  • WAL Mode: Enabled by default for concurrent read/write access
  • Busy Timeout: 60-second timeout handles process coordination
  • Shared Cache: Reduces memory usage across processes
  • Auto-Recovery: Connections auto-reconnect on database locks

🧪 Testing

npm test

All tests use isolated in-memory databases for fast, reliable testing. The project uses Jest as the testing framework.

📝 API Reference

Connection Management

  • connect(config?) - Connect to database
  • close() - Close database connection
  • isConnected() - Check connection status
  • getConnectionStatus() - Get detailed connection info

Functional API

  • save<T>(document, options?) - Save/upsert document
  • get<T>(constructor, id) - Get document by ID
  • find<T>(constructor, filter?, options?) - Find multiple documents
  • findOne<T>(constructor, filter?) - Find single document
  • where<T>(constructor, whereClause, params?, options?) - Query with SQL WHERE clauses or MongoDB filters
  • updateMany<T>(constructor, filter, update, options?) - Update multiple
  • deleteMany<T>(constructor, filter) - Delete multiple
  • isUnique<T>(document, fieldName) - Check field uniqueness for validation
  • raw<T>(sql) - Execute raw SQL

Collection Class

  • Static: find(), findOne(), get(), where(), saveMany(), updateMany(), deleteMany()
  • Instance: save(), delete(), reload(), toJSON(), toObject()

📊 Performance

  • better-sqlite3: High-performance synchronous SQLite driver for Node.js
  • WAL Mode: Concurrent read/write operations across multiple processes
  • Single Connection: One optimized connection per Node.js process
  • JSON Storage: Flexible document structure with SQLite's JSON functions
  • Optimized Queries: Proper indexing on ID and timestamps
  • Nested Transactions: Savepoint-based transaction nesting for complex operations

📄 License

MIT License - see LICENSE file for details.