Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/clear-all-db-adapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"zerithdb-db": patch
---

Add a `clearAll()` helper to remove every document from a collection.
5 changes: 5 additions & 0 deletions .changeset/support-unset-operator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"zerithdb-db": patch
---

Add complete support for the `$unset` operator in database update operations to allow deleting fields from documents.
38 changes: 0 additions & 38 deletions .github/ISSUE_TEMPLATE/bug_report.yml

This file was deleted.

27 changes: 27 additions & 0 deletions .github/workflows/pr-welcome.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: PR Welcome Bot

on:
pull_request_target:
types: [opened]

jobs:
welcome-comment:
name: Post Welcome Comment
runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: write
steps:
- name: Welcome Comment
uses: actions/github-script@v7
with:
script: |
const creator = context.payload.pull_request.user.login;
const message = "### Hello @" + creator + "!\n\nOur reviewers will be checking your PR shortly. In the meantime, please join our Discord server to connect with the team and other contributors:\n\n**Join Discord:** https://discord.gg/mwCayEMK4h";

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: message
});
Empty file added bot-test.txt
Empty file.
54 changes: 40 additions & 14 deletions packages/db/src/db-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@ import { ZerithDBError, ErrorCode } from "zerithdb-core";
*/
export class CollectionClient<T extends Record<string, any> = Record<string, any>> {
constructor(
private readonly table: Table<Document<T>>,
private readonly db: Dexie,
private readonly collectionName: string
) {}

private get table(): Table<Document<T>> {
return this.db.table(this.collectionName) as Table<Document<T>>;
}

/**
* Insert a new document into the collection.
* Automatically assigns `_id`, `_createdAt`, and `_updatedAt`.
Expand Down Expand Up @@ -117,11 +121,21 @@ export class CollectionClient<T extends Record<string, any> = Record<string, any
const now = Date.now();

await this.table.bulkPut(
matches.map((doc) => ({
...doc,
...(spec.$set ?? {}),
_updatedAt: now,
}))
matches.map((doc) => {
const newDoc = { ...doc };
if (spec.$set) {
Object.assign(newDoc, spec.$set);
}
if (spec.$unset) {
for (const key of Object.keys(spec.$unset)) {
delete (newDoc as Record<string, any>)[key];
}
}
return {
...newDoc,
_updatedAt: now,
};
})
);

return matches.length;
Expand Down Expand Up @@ -152,6 +166,21 @@ export class CollectionClient<T extends Record<string, any> = Record<string, any
}
}

/**
* Delete every document in the collection.
*/
async clearAll(): Promise<void> {
try {
await this.table.clear();
} catch (err) {
throw new ZerithDBError(
ErrorCode.DB_DELETE_FAILED,
`Failed to clear collection "${this.collectionName}"`,
{ cause: err }
);
}
}

/**
* Count documents matching a filter.
*/
Expand Down Expand Up @@ -185,6 +214,7 @@ export class CollectionClient<T extends Record<string, any> = Record<string, any

class ZerithDBDexie extends Dexie {
private readonly tableMap = new Map<string, Table>();
private readonly currentSchema: Record<string, string> = {};

constructor(appId: string) {
super(`zerithdb_${appId}`);
Expand All @@ -194,12 +224,8 @@ class ZerithDBDexie extends Dexie {
if (!this.tableMap.has(name)) {
// Dexie requires version upgrade to add tables — we use a dynamic schema pattern
const version = (this.verno ?? 0) + 1;
const existingTableNames = this.tableMap.keys();
const schema: Record<string, string> = { [name]: "_id, _createdAt, _updatedAt" };
for (const existingName of existingTableNames) {
schema[existingName] = "_id, _createdAt, _updatedAt";
}
this.version(version).stores(schema);
this.currentSchema[name] = "_id, _createdAt, _updatedAt";
this.version(version).stores(this.currentSchema);
this.tableMap.set(name, this.table(name));
}
// biome-ignore lint: map guarantees this is defined
Expand All @@ -222,8 +248,8 @@ export class DbClient {

collection<T extends Record<string, any>>(name: string): CollectionClient<T> {
if (!this.collections.has(name)) {
const table = this.dexie.ensureCollection(name);
this.collections.set(name, new CollectionClient<T>(table as Table<Document<T>>, name));
this.dexie.ensureCollection(name);
this.collections.set(name, new CollectionClient<T>(this.dexie, name));
}
return this.collections.get(name) as CollectionClient<T>;
}
Expand Down
39 changes: 38 additions & 1 deletion tests/unit/db.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import { DbClient } from "../../packages/db/src/db-client.js";
import type { ZerithDBConfig } from "../../packages/core/src/index.js";

const testConfig: ZerithDBConfig = {
appId: "test-db-" + Math.random().toString(36).slice(2),
appId: "test-db",
};

describe("DbClient — CollectionClient", () => {
let db: DbClient;

beforeEach(() => {
testConfig.appId = "test-db-" + Math.random().toString(36).slice(2);
db = new DbClient(testConfig);
});

Expand Down Expand Up @@ -129,6 +130,18 @@ describe("DbClient — CollectionClient", () => {
const after = (await col.findById(id))?._updatedAt ?? 0;
expect(after).toBeGreaterThanOrEqual(before);
});

it("should support $unset operator to remove fields", async () => {
const col = db.collection<{ text: string; category?: string }>("unset_test");
const { id } = await col.insert({ text: "do homework", category: "school" });

const count = await col.update({ _id: id } as never, { $unset: { category: true } });
expect(count).toBe(1);

const doc = await col.findById(id);
expect(doc?.text).toBe("do homework");
expect(doc?.category).toBeUndefined();
});
});

describe("delete()", () => {
Expand All @@ -142,6 +155,30 @@ describe("DbClient — CollectionClient", () => {
});
});

describe("clearAll()", () => {
it("should remove every document in the collection", async () => {
const col = db.collection<{ done: boolean }>("tasks");
await col.insertMany([{ done: true }, { done: false }, { done: true }]);

await col.clearAll();

expect(await col.find({})).toHaveLength(0);
expect(await col.count()).toBe(0);
});

it("should not clear other collections", async () => {
const tasks = db.collection<{ done: boolean }>("tasks");
const notes = db.collection<{ text: string }>("notes");
await tasks.insertMany([{ done: true }, { done: false }]);
await notes.insert({ text: "keep me" });

await tasks.clearAll();

expect(await tasks.count()).toBe(0);
expect(await notes.count()).toBe(1);
});
});

describe("count()", () => {
it("should return correct document count", async () => {
const col = db.collection<{ x: number }>("counts");
Expand Down