import type { Kysely } from "kysely"; import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { PluginStorageRepository, createPluginStorageAccessor, deleteAllPluginStorage, deletePluginCollection, } from "../../../src/database/repositories/plugin-storage.js"; import type { Database } from "../../../src/database/types.js"; import { setupTestDatabase, teardownTestDatabase } from "../../utils/test-db.js"; interface AnalyticsEvent { eventType: string; userId: string; timestamp: string; metadata: Record; } describe("Plugin Storage Integration", () => { let db: Kysely; beforeEach(async () => { db = await setupTestDatabase(); }); afterEach(async () => { await teardownTestDatabase(db); }); describe("full storage flow", () => { it("should support complete CRUD cycle", async () => { const repo = new PluginStorageRepository(db, "analytics-plugin", "events", [ "eventType", "userId", "timestamp", ]); // Create const event: AnalyticsEvent = { eventType: "pageview", userId: "user123", timestamp: new Date().toISOString(), metadata: { page: "/home", referrer: "google.com" }, }; await repo.put("event1", event); // Read const fetched = await repo.get("event1"); expect(fetched).toEqual(event); // Update const updatedEvent = { ...event, metadata: { ...event.metadata, duration: 5000 }, }; await repo.put("event1", updatedEvent); const refetched = await repo.get("event1"); expect(refetched?.metadata).toHaveProperty("duration", 5000); // Delete const deleted = await repo.delete("event1"); expect(deleted).toBe(true); expect(await repo.get("event1")).toBeNull(); }); it("should support complex queries with JSON extraction", async () => { const repo = new PluginStorageRepository(db, "analytics-plugin", "events", [ "eventType", "userId", "timestamp", ]); // Create events await repo.putMany([ { id: "e1", data: { eventType: "pageview", userId: "user1", timestamp: "2024-01-01T10:00:00Z", metadata: {}, }, }, { id: "e2", data: { eventType: "click", userId: "user1", timestamp: "2024-01-01T10:05:00Z", metadata: {}, }, }, { id: "e3", data: { eventType: "pageview", userId: "user2", timestamp: "2024-01-01T11:00:00Z", metadata: {}, }, }, ]); // Query by eventType const pageviews = await repo.query({ where: { eventType: "pageview" } }); expect(pageviews.items).toHaveLength(2); // Query by userId const user1Events = await repo.query({ where: { userId: "user1" } }); expect(user1Events.items).toHaveLength(2); // Combined query const user1Pageviews = await repo.query({ where: { eventType: "pageview", userId: "user1" }, }); expect(user1Pageviews.items).toHaveLength(1); }); }); describe("createPluginStorageAccessor", () => { it("should create accessor with multiple collections", async () => { const accessor = createPluginStorageAccessor(db, "my-plugin", { events: { indexes: ["eventType", "timestamp"] }, cache: { indexes: ["key", "expiresAt"] }, }); expect(accessor).toHaveProperty("events"); expect(accessor).toHaveProperty("cache"); // Use events collection await accessor.events.put("e1", { eventType: "test", timestamp: new Date().toISOString(), }); const event = await accessor.events.get("e1"); expect(event).toBeDefined(); // Use cache collection await accessor.cache.put("c1", { key: "test-key", value: "test-value", expiresAt: new Date().toISOString(), }); const cached = await accessor.cache.get("c1"); expect(cached).toBeDefined(); }); it("should isolate collections from each other", async () => { const accessor = createPluginStorageAccessor(db, "my-plugin", { events: { indexes: ["eventType"] }, cache: { indexes: ["key"] }, }); await accessor.events.put("item1", { eventType: "test" }); await accessor.cache.put("item1", { key: "test" }); // Both should exist independently expect(await accessor.events.get("item1")).toEqual({ eventType: "test" }); expect(await accessor.cache.get("item1")).toEqual({ key: "test" }); // Count should be separate expect( await (accessor.events as PluginStorageRepository).count({ eventType: "test", }), ).toBe(1); expect( await (accessor.cache as PluginStorageRepository).count({ key: "test", }), ).toBe(1); }); }); describe("deleteAllPluginStorage", () => { it("should delete all data for a plugin", async () => { const accessor = createPluginStorageAccessor(db, "cleanup-plugin", { events: { indexes: ["eventType"] }, cache: { indexes: ["key"] }, }); // Add data await accessor.events.put("e1", { eventType: "test" }); await accessor.events.put("e2", { eventType: "test2" }); await accessor.cache.put("c1", { key: "test" }); // Delete all const deleted = await deleteAllPluginStorage(db, "cleanup-plugin"); expect(deleted).toBe(3); // Verify empty expect(await accessor.events.get("e1")).toBeNull(); expect(await accessor.events.get("e2")).toBeNull(); expect(await accessor.cache.get("c1")).toBeNull(); }); it("should not affect other plugins", async () => { const plugin1 = createPluginStorageAccessor(db, "plugin1", { data: { indexes: ["key"] }, }); const plugin2 = createPluginStorageAccessor(db, "plugin2", { data: { indexes: ["key"] }, }); await plugin1.data.put("item1", { key: "test" }); await plugin2.data.put("item1", { key: "test" }); await deleteAllPluginStorage(db, "plugin1"); expect(await plugin1.data.get("item1")).toBeNull(); expect(await plugin2.data.get("item1")).toEqual({ key: "test" }); }); }); describe("deletePluginCollection", () => { it("should delete specific collection", async () => { const accessor = createPluginStorageAccessor(db, "my-plugin", { events: { indexes: ["eventType"] }, cache: { indexes: ["key"] }, }); await accessor.events.put("e1", { eventType: "test" }); await accessor.cache.put("c1", { key: "test" }); await deletePluginCollection(db, "my-plugin", "events"); expect(await accessor.events.get("e1")).toBeNull(); expect(await accessor.cache.get("c1")).toEqual({ key: "test" }); }); }); describe("pagination", () => { it("should paginate through large datasets", async () => { const repo = new PluginStorageRepository<{ index: number }>( db, "pagination-test", "items", [], ); // Create 25 items const items = Array.from({ length: 25 }, (_, i) => ({ id: `item-${String(i).padStart(3, "0")}`, data: { index: i }, })); await repo.putMany(items); // Paginate with limit of 10 const pages: Array> = []; let cursor: string | undefined; do { const result = await repo.query({ limit: 10, cursor }); pages.push(result.items); cursor = result.cursor; } while (cursor); expect(pages).toHaveLength(3); expect(pages[0]).toHaveLength(10); expect(pages[1]).toHaveLength(10); expect(pages[2]).toHaveLength(5); // Verify all items were retrieved const allItems = pages.flat(); expect(allItems).toHaveLength(25); expect(new Set(allItems.map((i) => i.id)).size).toBe(25); }); }); describe("concurrent operations", () => { it("should handle concurrent puts", async () => { const repo = new PluginStorageRepository<{ value: number }>( db, "concurrent-test", "items", [], ); // Concurrent puts await Promise.all([ repo.put("item1", { value: 1 }), repo.put("item2", { value: 2 }), repo.put("item3", { value: 3 }), repo.put("item4", { value: 4 }), repo.put("item5", { value: 5 }), ]); const count = await repo.count(); expect(count).toBe(5); }); }); });