kilncast は何をしますか?
Thin typed wrapper over Firestore. It types and coerces, it does not validate: the schema is used for type inference only and is never executed, so there is no runtime validation. Underneath it is plain Firestore; every handle exposes .ref to drop to the raw SDK.
Setup
# server
npm install kilncast firebase-admin
# web
npm install kilncast firebase
Three entrypoints:
kilncast: neutral. Schema definition, sentinels, types,KilncastError. Imports no Firebase, safe in shared modules and frontend bundles.kilncast/admin:createDatabase(getFirestore())overfirebase-admin, plus typed ref helpers. Re-exports the whole neutral core.kilncast/web: the same over the modularfirebaseSDK.
Quick start
import { collection, increment } from "kilncast";
// or "kilncast/web"
import { createDatabase } from "kilncast/admin";
import { getFirestore } from "firebase-admin/firestore";
import { z } from "zod";
// Plain, unbound definition, define once in a shared module importing only "kilncast"
const posts = collection(
"posts",
z.object({ title: z.string(), likes: z.number(), createdAt: z.date() }),
);
const db = createDatabase(getFirestore());
await db
.collection(posts)
.set("hello", { title: "Hello", likes: 0, createdAt: new Date() });
// Doc | null, Doc = T & { id }
const post = await db.collection(posts).get("hello");
await db.collection(posts).update("hello", { likes: increment(1) });
const popular = await db
.collection(posts)
.where("likes", ">=", 10)
.orderBy("likes", "desc")
.limit(10)
.get();
The schema is any Standard Schema validator (Zod, Valibot, ArkType). It describes one document's fields, excluding the id.
Core rules
- Ids are path-only. Never declare an
idfield in a schema (compile error). Reads mergeidin flat; writes exclude it.add(data)returns the generated id. - Missing reads are
null.get()returnsDoc<S> | null; there is no.existssnapshot to check (use.doc(id).exists()for a boolean). - Values coerce at the boundary. Write a
Date, read aDate(stored asTimestamp); write aUint8Array, read aUint8Array(stored as SDK bytes). Deep through maps and arrays. - Sentinels are
kilncast's own. ImportserverTimestamp,increment,arrayUnion,arrayRemove,deleteFieldfromkilncast, neverFieldValuefrom an SDK. Each is type-constrained to the fields it fits (incrementon numbers,deleteFieldon optional fields only). - Queries are typed to the schema.
where/orderBytake typed dotted paths into nested maps ("customer.address.city"); a misspelt field or wrong value type is a compile error. Target the id withdocumentId()fromkilncast. - Dotted-path keys work in
updateonly.update("o1", { "totals.items": increment(1) })touches one nested field. In a mergeseta dotted key is a literal field name, so nest an object instead. - Transaction and batch writes take no
await. Reads in a transaction are async; writes returnvoidand buffer until commit. Batches are write-only (noget, noadd) and apply atomically on.commit(). - No runtime validation. A document drifted from its schema comes back typed as valid. Where drift matters, run the schema on the result yourself.
- SDK semantics win.
kilncastnever wraps Firestore, network, or permission errors;KilncastErrorcovers onlykilncast's own failures. Anythingkilncastdoes not wrap is reachable via.ref.
Common operations
const col = db.collection(posts);
// Reads
// Doc | null
await col.get("hello");
// boolean
await col.doc("hello").exists();
// Writes, also on col.doc("hello")
await col.set("hello", { title: "Hi", likes: 0, createdAt: new Date() });
await col.set("hello", { likes: 1 }, { merge: true });
await col.update("hello", { likes: increment(1) });
// returns the new id
const id = await col.add({ title: "New", likes: 0, createdAt: new Date() });
await col.delete(id);
// Queries, immutable builder, .get() alias .list()
await col.where("likes", ">=", 10).orderBy("likes", "desc").limit(5).get();
await col.count();
// 0 over empty match
await col.sum("likes");
// null over empty match
await col.average("likes");
// returns unsubscribe fn
const unsub = col.onSnapshot((docs) => {});
// Subcollection, same def reusable at any depth
db.collection(posts).doc("hello").collection(comments);
// Collection group, every collection with that name
await db.collectionGroup(comments).orderBy("createdAt").get();
// Transaction, reads async, writes sync
await db.runTransaction(async (tx) => {
const post = await tx.collection(posts).get("hello");
if (post) tx.collection(posts).update("hello", { likes: post.likes + 1 });
});
// Batch, write-only, atomic on commit
const batch = db.batch();
batch.collection(posts).update("hello", { likes: increment(1) });
await batch.commit();
Full reference
See REFERENCE.md for queries and cursors, sentinels and nested updates, live updates, transactions and batches in detail, raw fields and full-precision timestamps, bytes, neutral value types, the .ref escape hatch, and gotchas.