Documentation
Understand the Firestore adapter contract, runtime capabilities, value conversion, and in-memory testing.
The core modeling layer does not import a Firebase SDK. A FirestoreAdapter
translates Fireclass's neutral CRUD and query operations to Firebase Admin, the
Firebase client SDK, an emulator, or a custom backend implementation.
Model / BaseModel
|
v
FirestoreAdapter + QuerySpec
|
v
Firebase Admin, Firebase Client, or FakeAdaptercreateFireclass() constructs the runtime adapter and binds it to a generated
BaseModel. Every model extending that class shares the same adapter instance.
interface FirestoreAdapter {
add(
collection: string,
data: Record<string, unknown>,
): Promise<{ id: string }>;
set(
collection: string,
id: string,
data: Record<string, unknown>,
options: { merge: boolean },
): Promise<void>;
get(
collection: string,
id: string,
): Promise<Record<string, unknown> | null>;
query(collection: string, spec: QuerySpec): Promise<DocSnapshot[]>;
delete(collection: string, id: string): Promise<void>;
}| Method | Used by |
|---|---|
add | save() when the model has no id |
set | save() when the model has an id |
get | findById() and deleteById() |
query | findMany(), findOne(), and deleteMany() |
delete | Instance delete and delete fallbacks |
set receives { merge: true } from BaseModel.save().
interface FirestoreAdapter {
batchDelete?(collection: string, ids: string[]): Promise<void>;
count?(collection: string, spec: QuerySpec): Promise<number>;
subscribe?(
collection: string,
spec: QuerySpec,
callback: (docs: DocSnapshot[]) => void,
): () => void;
convert?(
data: Record<string, unknown>,
): Record<string, unknown>;
}| Capability | Behavior when absent |
|---|---|
batchDelete | deleteMany() calls required delete sequentially. |
count | count() throws UnsupportedAdapterCapabilityError. |
subscribe | Realtime consumers require a different adapter. |
convert | Snapshot data is passed to the model unchanged. |
The React runtime extends the core subscription contract with an error callback
and subscribeDoc() for useDoc.
| Adapter | Firebase runtime | Batch delete | Count | Realtime | Convert |
| --- | --- | --- | --- | --- | --- | --- |
| AdminAdapter | firebase-admin | Yes | Yes | No | Yes |
| ClientAdapter | Firebase client SDK | Yes | Yes | Yes | Yes |
| FakeAdapter | In-memory JavaScript | Yes | Yes | Yes | Yes |
@dharayush7/fireclass-ssr uses the same AdminAdapter as
@dharayush7/fireclass-js, wrapped in Next.js server-only initialization.
The Admin and Client adapters split delete batches at Firestore's 500-write limit. Their count methods use the SDK's server-side aggregation support.
Fireclass lowers public Query Options into a runtime- neutral structure:
interface QuerySpec {
where: Array<{
field: string;
op:
| "eq"
| "gt"
| "gte"
| "lt"
| "lte"
| "in"
| "not-in"
| "array-contains"
| "array-contains-any";
value: unknown;
}>;
orderBy?: {
field: string;
direction: "asc" | "desc";
};
limit?: number;
startAfter?: unknown[];
}Adapters map this structure to their native SDK. Adapter errors are not wrapped by core, so Firebase SDK errors retain their original codes and metadata.
The built-in adapters use convertFirestoreTypes() when hydrating reads. It
recursively walks arrays and objects, replacing values that expose a successful
toDate() call with JavaScript Date objects.
const post = await Post.findById("post_123");
post?.createdAt instanceof Date; // true for a Firestore Timestamp valueIf toDate() throws, Fireclass preserves the original value. Primitive values
and ordinary objects are otherwise unchanged.
Import test utilities from the dedicated core subpath. This keeps unit tests independent of credentials, the network, and the Firebase emulator.
import { Collection, defineBaseModel } from "@dharayush7/fireclass-core";
import { FakeAdapter } from "@dharayush7/fireclass-core/testing";
import { describe, expect, it } from "vitest";
describe("User", () => {
it("queries active users", async () => {
const adapter = new FakeAdapter({
seed: {
users: {
ada: { name: "Ada", active: true },
grace: { name: "Grace", active: false },
},
},
idFactory: () => "generated_user",
});
const BaseModel = defineBaseModel(adapter);
@Collection("users")
class User extends BaseModel<User> {
name!: string;
active!: boolean;
}
const users = await User.findMany({
where: { active: { equals: true } },
});
expect(users.map((user) => user.name)).toEqual(["Ada"]);
});
});Seed data is cloned on input and output. idFactory makes create tests
deterministic. The adapter supports every filter operator, single-field
ordering, limits, count, batch delete, conversion, and collection subscriptions.
FakeAdapter cursor limitation
FakeAdapter currently ignores startAfter. Test cursor pagination through an
Admin or Client adapter connected to the Firebase emulator.
Implement the required contract, then bind it with defineBaseModel:
import {
defineBaseModel,
type FirestoreAdapter,
} from "@dharayush7/fireclass-core";
import { MyAdapter } from "./my-adapter.js";
const adapter: FirestoreAdapter = new MyAdapter();
export const BaseModel = defineBaseModel(adapter);
export { adapter };A production adapter should preserve Fireclass's contract: return null for a
missing document, return ids separately from document data, honor merge writes,
and return an unsubscribe function from subscribe.
Continue with Errors for capability failures and error propagation.