diff --git a/.changeset/clear-all-db-adapter.md b/.changeset/clear-all-db-adapter.md new file mode 100644 index 00000000..5f8db005 --- /dev/null +++ b/.changeset/clear-all-db-adapter.md @@ -0,0 +1,5 @@ +--- +"zerithdb-db": patch +--- + +Add a `clearAll()` helper to remove every document from a collection. diff --git a/.changeset/support-unset-operator.md b/.changeset/support-unset-operator.md new file mode 100644 index 00000000..42f4febb --- /dev/null +++ b/.changeset/support-unset-operator.md @@ -0,0 +1,5 @@ +--- +"zerithdb-db": patch +--- + +Add complete support for the `$unset` operator in database update operations to allow deleting fields from documents. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml deleted file mode 100644 index 16c679fd..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ /dev/null @@ -1,38 +0,0 @@ ---- -name: "🐛 Bug Report" -about: "Something isn't working as expected" -labels: ["bug", "needs-investigation"] ---- - -## Description - - - -## Steps to Reproduce - -1. -2. -3. - -## Expected Behavior - - - -## Actual Behavior - - - -## Environment - -- ZerithDB version: -- Browser + version: -- OS: -- Package(s) affected: `@zerithdb/` - -## Reproduction - - - -## Additional Context - - diff --git a/.github/workflows/pr-welcome.yml b/.github/workflows/pr-welcome.yml new file mode 100644 index 00000000..f6d10534 --- /dev/null +++ b/.github/workflows/pr-welcome.yml @@ -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 + }); diff --git a/bot-test.txt b/bot-test.txt new file mode 100644 index 00000000..e69de29b diff --git a/packages/db/src/db-client.ts b/packages/db/src/db-client.ts index 2e3ef051..2529cedd 100644 --- a/packages/db/src/db-client.ts +++ b/packages/db/src/db-client.ts @@ -15,10 +15,14 @@ import { ZerithDBError, ErrorCode } from "zerithdb-core"; */ export class CollectionClient = Record> { constructor( - private readonly table: Table>, + private readonly db: Dexie, private readonly collectionName: string ) {} + private get table(): Table> { + return this.db.table(this.collectionName) as Table>; + } + /** * Insert a new document into the collection. * Automatically assigns `_id`, `_createdAt`, and `_updatedAt`. @@ -117,11 +121,21 @@ export class CollectionClient = Record ({ - ...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)[key]; + } + } + return { + ...newDoc, + _updatedAt: now, + }; + }) ); return matches.length; @@ -152,6 +166,21 @@ export class CollectionClient = Record { + 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. */ @@ -185,6 +214,7 @@ export class CollectionClient = Record(); + private readonly currentSchema: Record = {}; constructor(appId: string) { super(`zerithdb_${appId}`); @@ -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 = { [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 @@ -222,8 +248,8 @@ export class DbClient { collection>(name: string): CollectionClient { if (!this.collections.has(name)) { - const table = this.dexie.ensureCollection(name); - this.collections.set(name, new CollectionClient(table as Table>, name)); + this.dexie.ensureCollection(name); + this.collections.set(name, new CollectionClient(this.dexie, name)); } return this.collections.get(name) as CollectionClient; } diff --git a/tests/unit/db.test.ts b/tests/unit/db.test.ts index 7ca10ee2..e15247ac 100644 --- a/tests/unit/db.test.ts +++ b/tests/unit/db.test.ts @@ -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); }); @@ -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()", () => { @@ -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");