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

cafea-db

v0.1.5

Published

MongoDB-compatible database client for local JSON files

Readme

CafeaDb

MongoDB-kompatibler Datenbank-Client für lokale JSON-Dateien. Pro Collection wird eine separate .json-Datei verwendet – kein Server, keine Abhängigkeiten zur Laufzeit.

Installation

npm install cafea-db

Voraussetzung: Node.js ≥ 18, ESM-Projekt ("type": "module" in package.json)

Schnellstart

import { CafeaDb } from "cafea-db";

const db = new CafeaDb("./data"); // Verzeichnis für JSON-Dateien
const orders = db.collection("orders"); // → data/orders.json

await orders.insertOne({ product: "Espresso", qty: 3, price: 2.5 });
const docs = await orders
    .find({ qty: { $gt: 1 } })
    .sort({ price: -1 })
    .toArray();

API-Referenz

new CafeaDb(dataDir: string)

Erstellt einen Client. dataDir ist das Verzeichnis, in dem die Collection-Dateien gespeichert werden. Fehlende Dateien werden beim ersten Schreibzugriff automatisch angelegt.

const db = new CafeaDb("./data");
const users = db.collection<User>("users"); // generisch typisierbar

Lesen

.find(filter?, options?): Cursor

Gibt einen Cursor mit allen passenden Dokumenten zurück.

// alle Dokumente
const all = await col.find().toArray();

// mit Filter
const active = await col.find({ active: true }).toArray();

// mit Optionen
const paged = await col.find({}, { sort: { name: 1 }, skip: 10, limit: 5 }).toArray();

.findOne(filter?): Promise<T | null>

Gibt das erste passende Dokument zurück, oder null.

const user = await users.findOne({ email: "[email protected]" });

.countDocuments(filter?): Promise<number>

const n = await orders.countDocuments({ status: "open" });

.distinct(field, filter?): Promise<unknown[]>

Gibt alle eindeutigen Werte eines Feldes zurück. Unterstützt Dot-Notation.

const cities = await users.distinct("address.city");

.aggregate(pipeline): Cursor

Führt eine Aggregations-Pipeline aus.

const result = await orders
    .aggregate([
        { $match: { status: "done" } },
        { $group: { _id: "$product", total: { $sum: "$qty" } } },
        { $sort: { total: -1 } },
    ])
    .toArray();

Schreiben

.insertOne(doc): Promise<InsertOneResult>

const { insertedId } = await products.insertOne({ name: "Latte", price: 3.8 });
// insertedId ist eine 24-stellige Hex-ID (MongoDB ObjectId-Format)

.insertMany(docs): Promise<InsertManyResult>

const { insertedCount, insertedIds } = await products.insertMany([
    { name: "Espresso", price: 2.5 },
    { name: "Cake", price: 4.0 },
]);

.updateOne(filter, update, options?): Promise<UpdateResult>

await orders.updateOne({ _id: "abc" }, { $set: { status: "done" } });

// mit upsert
await orders.updateOne({ _id: "xyz" }, { $set: { qty: 1 } }, { upsert: true });

.updateMany(filter, update, options?): Promise<UpdateResult>

await orders.updateMany({ status: "pending" }, { $inc: { retries: 1 } });

.deleteOne(filter): Promise<DeleteResult>

await sessions.deleteOne({ token: "expired-token" });

.replaceOne(filter, replacement, options?): Promise<UpdateResult>

Ersetzt das gesamte Dokument; die _id bleibt erhalten.

await users.replaceOne({ _id: "u1" }, { name: "Alice Neu", role: "admin" });

.bulkWrite(operations): Promise<BulkWriteResult>

await col.bulkWrite([
    { insertOne: { document: { name: "A" } } },
    { updateOne: { filter: { name: "B" }, update: { $set: { done: true } } } },
    { deleteOne: { filter: { name: "C" } } },
    { updateMany: { filter: { active: false }, update: { $unset: { token: "" } } } },
    { replaceOne: { filter: { _id: "x" }, replacement: { name: "X2" } } },
]);

Cursor

Cursor werden von .find() und .aggregate() zurückgegeben. Alle Methoden sind chainbar.

| Methode | Beschreibung | | ------------- | ------------------------------------------------------ | | .sort(spec) | { field: 1 } aufsteigend, { field: -1 } absteigend | | .skip(n) | Überspringt die ersten n Dokumente | | .limit(n) | Begrenzt auf n Dokumente | | .toArray() | Gibt Promise<T[]> zurück (löst den Cursor auf) |

const page = await col.find().sort({ createdAt: -1 }).skip(20).limit(10).toArray();

Query-Operatoren

| Operator | Beschreibung | | ---------------------------- | -------------------------------------------------------- | | $eq, $ne | Gleich / Ungleich | | $gt, $gte, $lt, $lte | Größer/Kleiner (auch Strings) | | $in, $nin | Wert in / nicht in Liste | | $exists | Feld vorhanden (true) oder fehlend (false) | | $regex | Regulärer Ausdruck; $options: 'i' für case-insensitive | | $and, $or | Logische Verknüpfungen |

col.find({
    $or: [{ price: { $lt: 2 } }, { tags: { $in: ["sale", "new"] } }],
    name: { $regex: "^E", $options: "i" },
});

Dot-Notation für verschachtelte Felder:

col.find({ "address.city": "Berlin" });

Update-Operatoren

| Operator | Beschreibung | | -------- | ------------------------------------------------------------ | | $set | Setzt Felder (Dot-Notation möglich) | | $unset | Entfernt Felder | | $inc | Inkrementiert/Dekrementiert einen Zahlenwert | | $push | Fügt Element(e) an ein Array an ({ $each: [...] } möglich) | | $pull | Entfernt Elemente aus einem Array (Wert oder Filterausdruck) |

col.updateOne(
    { _id: "x" },
    {
        $set: { "meta.updated": true },
        $inc: { views: 1 },
        $push: { tags: { $each: ["featured", "hot"] } },
        $pull: { errors: { code: { $lt: 400 } } },
        $unset: { draft: "" },
    },
);

Aggregations-Pipeline-Stages

| Stage | Beschreibung | | ------------ | ---------------------------------------------------------------------------------- | | $match | Filtert Dokumente (alle Query-Operatoren) | | $sort | Sortiert | | $limit | Begrenzt Anzahl | | $skip | Überspringt Dokumente | | $project | Inklusion/Exklusion von Feldern | | $addFields | Fügt berechnete Felder hinzu | | $group | Gruppiert mit $sum, $avg, $min, $max, $first, $last, $push, $count | | $unwind | Entfaltet Array-Felder | | $lookup | Left-Join mit einer anderen Collection-Datei | | $count | Gibt Anzahl im benannten Feld zurück | | $facet | Führt parallele Sub-Pipelines aus |

// $lookup – verknüpft orders mit products
const result = await orders
    .aggregate([
        {
            $lookup: {
                from: "products",
                localField: "productId",
                foreignField: "_id",
                as: "product",
            },
        },
        { $unwind: "$product" },
        { $project: { _id: 0, qty: 1, "product.name": 1 } },
    ])
    .toArray();

TypeScript-Typisierung

import { CafeaDb, type Filter, type UpdateFilter } from "cafea-db";

type Product = { _id?: string; name: string; price: number; tags: string[] };

const db = new CafeaDb("./data");
const products = db.collection<Product>("products");

const filter: Filter<Product> = { price: { $gt: 2 } };
const update: UpdateFilter<Product> = { $inc: { price: 0.1 } };

await products.updateMany(filter, update);

IDs

Neue Dokumente erhalten automatisch eine _id im 24-stelligen Hex-Format (MongoDB-ObjectId-kompatibel). Eine eigene ID kann übergeben werden:

await col.insertOne({ _id: "meine-id", name: "X" });

Zum manuellen Generieren:

import { generateObjectId } from "cafea-db";
const id = generateObjectId(); // z.B. "64ab3f2c1a9e4b0012ef7c01"

Dateistruktur

data/
├── users.json
├── orders.json
└── products.json

Jede Datei enthält ein JSON-Array. Die Dateien sind lesbar und direkt editierbar.


Entwicklung

npm run build      # TypeScript → dist/
npm run typecheck  # Nur Typ-Check, kein Output
npm test           # 114 Tests mit Vitest
npm run test:watch # Tests im Watch-Modus

Einschränkungen

  • Kein File-Locking: parallele Prozesse auf derselben Datei können zu Race Conditions führen
  • Kein Transaktions-Support
  • $lookup liest stets frisch von Disk (kein Join über In-Memory-Cache)
  • $push/$pull in $group sind nicht persistente Aggregatoren