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-dbVoraussetzung: Node.js ≥ 18, ESM-Projekt (
"type": "module"inpackage.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 typisierbarLesen
.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.jsonJede 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-ModusEinschränkungen
- Kein File-Locking: parallele Prozesse auf derselben Datei können zu Race Conditions führen
- Kein Transaktions-Support
$lookupliest stets frisch von Disk (kein Join über In-Memory-Cache)$push/$pullin$groupsind nicht persistente Aggregatoren
