UNPKG

oneie

Version:

Build apps, websites, and AI agents in English. Zero-interaction setup for AI agents (Claude Code, Cursor, Windsurf). Download to your computer, run in the cloud, deploy to the edge. Open source and free forever.

1,935 lines (1,606 loc) 66.6 kB
--- title: 2 1 Dataprovider Interface dimension: things category: features tags: ai, architecture, backend, frontend, ontology related_dimensions: events, groups scope: global created: 2025-11-03 updated: 2025-11-03 version: 1.0.0 ai_context: | This document is part of the things dimension in the features category. Location: one/things/features/2-1-dataprovider-interface.md Purpose: Documents feature 2-1: dataprovider interface & convexprovider Related dimensions: events, groups For AI agents: Read this to understand 2 1 dataprovider interface. --- # Feature 2-1: DataProvider Interface & ConvexProvider **Feature ID:** `feature_2_1_dataprovider_interface` **Plan:** `plan_2_backend_agnostic_frontend` **Owner:** Backend Specialist **Status:** Detailed Specification Complete **Priority:** P0 (Critical Path - Blocks All Other Features) **Effort:** 1 week **Dependencies:** None --- ## Assignment This feature implements the **DataProvider interface** and **ConvexProvider implementation** that abstracts all backend operations behind a clean, typed API. This is the foundation for backend-agnostic frontend architecture. **Goal:** Create a single interface that handles ALL 6-dimension ontology operations, allowing the frontend to remain completely decoupled from Convex-specific APIs. --- ## 1. Complete Technical Specification ### 1.1 DataProvider Interface Complete TypeScript interface covering all 6 dimensions of the ontology: ```typescript // frontend/src/providers/DataProvider.ts import { Effect } from "effect"; import { Id } from "@/convex/_generated/dataModel"; /** * DataProvider Interface - Complete abstraction over backend operations * * This interface represents the SINGLE source of truth for all data operations * in the ONE platform. ANY backend can implement this interface. * * Design Principles: * 1. Effect.ts-based error handling (typed errors) * 2. Organization-scoped operations (multi-tenant by default) * 3. Immutable operations (pure functions) * 4. Type-safe parameters and return values * 5. No backend-specific code leaks through this interface */ export interface DataProvider { /** * DIMENSION 1: Organizations * Multi-tenant isolation boundary - who owns what at org level */ organizations: { /** * Get organization by ID * @returns Effect that succeeds with Organization or fails with OrganizationNotFoundError */ get: ( id: Id<"organizations">, ) => Effect.Effect<Organization, OrganizationNotFoundError>; /** * List organizations (platform owner only) * @returns Effect with array of organizations */ list: ( params: ListOrganizationsParams, ) => Effect.Effect<Organization[], Error>; /** * Create new organization * @returns Effect with organization ID */ create: ( input: CreateOrganizationInput, ) => Effect.Effect<Id<"organizations">, Error>; /** * Update organization settings * @returns Effect with updated organization */ update: ( id: Id<"organizations">, updates: Partial<Organization>, ) => Effect.Effect<Organization, Error>; /** * Get organization usage stats * @returns Effect with usage metrics */ getUsage: ( id: Id<"organizations">, ) => Effect.Effect<OrganizationUsage, Error>; }; /** * DIMENSION 2: People * Authorization & governance - who can do what */ people: { /** * Get person by ID * @returns Effect with Person or PersonNotFoundError */ get: (id: Id<"people">) => Effect.Effect<Person, PersonNotFoundError>; /** * List people in organization * @returns Effect with array of people */ list: (params: ListPeopleParams) => Effect.Effect<Person[], Error>; /** * Create new person (user) * @returns Effect with person ID */ create: (input: CreatePersonInput) => Effect.Effect<Id<"people">, Error>; /** * Update person profile * @returns Effect with updated person */ update: ( id: Id<"people">, updates: Partial<Person>, ) => Effect.Effect<Person, Error>; /** * Get person's organizations * @returns Effect with array of organizations */ getOrganizations: ( id: Id<"people">, ) => Effect.Effect<Organization[], Error>; /** * Check person's permissions * @returns Effect with boolean */ checkPermission: ( id: Id<"people">, permission: string, ) => Effect.Effect<boolean, Error>; }; /** * DIMENSION 3: Things * All entities - users, agents, content, tokens, courses */ things: { /** * Get thing by ID * @returns Effect with Thing or ThingNotFoundError */ get: (id: Id<"things">) => Effect.Effect<Thing, ThingNotFoundError>; /** * List things by type and filters * @returns Effect with array of things */ list: (params: ListThingsParams) => Effect.Effect<Thing[], Error>; /** * Create new thing * @returns Effect with thing ID */ create: (input: CreateThingInput) => Effect.Effect<Id<"things">, Error>; /** * Update thing * @returns Effect with updated thing */ update: ( id: Id<"things">, updates: Partial<Thing>, ) => Effect.Effect<Thing, Error>; /** * Delete thing (soft delete) * @returns Effect with void */ delete: (id: Id<"things">) => Effect.Effect<void, Error>; /** * Search things (full-text search) * @returns Effect with search results */ search: (params: SearchThingsParams) => Effect.Effect<Thing[], Error>; }; /** * DIMENSION 4: Connections * All relationships - owns, follows, holds_tokens, enrolled_in */ connections: { /** * Get connection by ID * @returns Effect with Connection or ConnectionNotFoundError */ get: ( id: Id<"connections">, ) => Effect.Effect<Connection, ConnectionNotFoundError>; /** * List connections from a thing * @returns Effect with array of connections */ listFrom: ( params: ListConnectionsFromParams, ) => Effect.Effect<Connection[], Error>; /** * List connections to a thing * @returns Effect with array of connections */ listTo: ( params: ListConnectionsToParams, ) => Effect.Effect<Connection[], Error>; /** * Create connection between things * @returns Effect with connection ID */ create: ( input: CreateConnectionInput, ) => Effect.Effect<Id<"connections">, Error>; /** * Update connection metadata * @returns Effect with updated connection */ update: ( id: Id<"connections">, updates: Partial<Connection>, ) => Effect.Effect<Connection, Error>; /** * Delete connection * @returns Effect with void */ delete: (id: Id<"connections">) => Effect.Effect<void, Error>; }; /** * DIMENSION 5: Events * All actions - created, updated, purchased, completed */ events: { /** * Get event by ID * @returns Effect with Event or EventNotFoundError */ get: (id: Id<"events">) => Effect.Effect<Event, EventNotFoundError>; /** * List events with filters * @returns Effect with array of events */ list: (params: ListEventsParams) => Effect.Effect<Event[], Error>; /** * Log new event * @returns Effect with event ID */ log: (input: LogEventInput) => Effect.Effect<Id<"events">, Error>; /** * Get event timeline for thing * @returns Effect with array of events */ getTimeline: (thingId: Id<"things">) => Effect.Effect<Event[], Error>; /** * Get event statistics * @returns Effect with event stats */ getStats: (params: EventStatsParams) => Effect.Effect<EventStats, Error>; }; /** * DIMENSION 6: Knowledge * Labels, embeddings, semantic search */ knowledge: { /** * Get knowledge item by ID * @returns Effect with Knowledge or KnowledgeNotFoundError */ get: ( id: Id<"knowledge">, ) => Effect.Effect<Knowledge, KnowledgeNotFoundError>; /** * List knowledge items * @returns Effect with array of knowledge items */ list: (params: ListKnowledgeParams) => Effect.Effect<Knowledge[], Error>; /** * Create knowledge item (label or chunk) * @returns Effect with knowledge ID */ create: ( input: CreateKnowledgeInput, ) => Effect.Effect<Id<"knowledge">, Error>; /** * Link knowledge to thing * @returns Effect with void */ link: (input: LinkKnowledgeInput) => Effect.Effect<void, Error>; /** * Unlink knowledge from thing * @returns Effect with void */ unlink: ( knowledgeId: Id<"knowledge">, thingId: Id<"things">, ) => Effect.Effect<void, Error>; /** * Vector search (semantic search) * @returns Effect with search results */ vectorSearch: ( params: VectorSearchParams, ) => Effect.Effect<Knowledge[], Error>; }; } /** * Type definitions for DataProvider operations */ // Organizations export interface Organization { _id: Id<"organizations">; name: string; slug: string; plan: "starter" | "pro" | "enterprise"; status: "active" | "suspended" | "trial" | "cancelled"; limits: { users: number; storage: number; apiCalls: number; cycle: number; }; usage: { users: number; storage: number; apiCalls: number; cycle: number; }; createdAt: number; updatedAt: number; } export interface ListOrganizationsParams { status?: Organization["status"]; plan?: Organization["plan"]; limit?: number; offset?: number; } export interface CreateOrganizationInput { name: string; slug: string; plan: Organization["plan"]; ownerId: Id<"people">; } export interface OrganizationUsage { organizationId: Id<"organizations">; users: number; storage: number; apiCalls: number; cycle: number; period: { start: number; end: number }; } // People export interface Person { _id: Id<"people">; email: string; username: string; displayName: string; role: "platform_owner" | "org_owner" | "org_user" | "customer"; organizationId?: Id<"organizations">; permissions?: string[]; createdAt: number; updatedAt: number; } export interface ListPeopleParams { organizationId?: Id<"organizations">; role?: Person["role"]; limit?: number; offset?: number; } export interface CreatePersonInput { email: string; username: string; displayName: string; role: Person["role"]; organizationId?: Id<"organizations">; permissions?: string[]; } // Things export interface Thing { _id: Id<"things">; type: ThingType; name: string; properties: Record<string, any>; status: "active" | "inactive" | "draft" | "published" | "archived"; organizationId?: Id<"organizations">; createdAt: number; updatedAt: number; } export type ThingType = | "creator" | "ai_clone" | "course" | "lesson" | "token" | "external_agent" | "mandate" | "product"; export interface ListThingsParams { type?: ThingType; status?: Thing["status"]; organizationId?: Id<"organizations">; limit?: number; offset?: number; } export interface CreateThingInput { type: ThingType; name: string; properties: Record<string, any>; status?: Thing["status"]; organizationId?: Id<"organizations">; } export interface SearchThingsParams { query: string; type?: ThingType; organizationId?: Id<"organizations">; limit?: number; } // Connections export interface Connection { _id: Id<"connections">; fromThingId: Id<"things">; toThingId: Id<"things">; relationshipType: ConnectionType; metadata?: Record<string, any>; validFrom?: number; validTo?: number; createdAt: number; } export type ConnectionType = | "owns" | "created_by" | "clone_of" | "holds_tokens" | "enrolled_in" | "transacted"; export interface ListConnectionsFromParams { fromThingId: Id<"things">; relationshipType?: ConnectionType; limit?: number; offset?: number; } export interface ListConnectionsToParams { toThingId: Id<"things">; relationshipType?: ConnectionType; limit?: number; offset?: number; } export interface CreateConnectionInput { fromThingId: Id<"things">; toThingId: Id<"things">; relationshipType: ConnectionType; metadata?: Record<string, any>; validFrom?: number; validTo?: number; } // Events export interface Event { _id: Id<"events">; type: EventType; actorId?: Id<"things">; targetId?: Id<"things">; timestamp: number; metadata?: Record<string, any>; } export type EventType = | "entity_created" | "entity_updated" | "entity_deleted" | "payment_event" | "communication_event"; export interface ListEventsParams { type?: EventType; actorId?: Id<"things">; targetId?: Id<"things">; startTime?: number; endTime?: number; limit?: number; offset?: number; } export interface LogEventInput { type: EventType; actorId?: Id<"things">; targetId?: Id<"things">; metadata?: Record<string, any>; } export interface EventStatsParams { type?: EventType; organizationId?: Id<"organizations">; period: { start: number; end: number }; } export interface EventStats { total: number; byType: Record<EventType, number>; byDay: Array<{ date: string; count: number }>; } // Knowledge export interface Knowledge { _id: Id<"knowledge">; knowledgeType: "label" | "chunk" | "document" | "vector_only"; text?: string; embedding?: number[]; embeddingModel?: string; embeddingDim?: number; sourceThingId?: Id<"things">; labels?: string[]; createdAt: number; } export interface ListKnowledgeParams { knowledgeType?: Knowledge["knowledgeType"]; sourceThingId?: Id<"things">; limit?: number; offset?: number; } export interface CreateKnowledgeInput { knowledgeType: Knowledge["knowledgeType"]; text?: string; embedding?: number[]; embeddingModel?: string; sourceThingId?: Id<"things">; labels?: string[]; } export interface LinkKnowledgeInput { knowledgeId: Id<"knowledge">; thingId: Id<"things">; role?: "label" | "summary" | "chunk_of" | "caption"; metadata?: Record<string, any>; } export interface VectorSearchParams { query: string; organizationId?: Id<"organizations">; limit?: number; } /** * Error types for DataProvider operations */ export class OrganizationNotFoundError { readonly _tag = "OrganizationNotFoundError"; constructor(public readonly id: Id<"organizations">) {} } export class PersonNotFoundError { readonly _tag = "PersonNotFoundError"; constructor(public readonly id: Id<"people">) {} } export class ThingNotFoundError { readonly _tag = "ThingNotFoundError"; constructor(public readonly id: Id<"things">) {} } export class ConnectionNotFoundError { readonly _tag = "ConnectionNotFoundError"; constructor(public readonly id: Id<"connections">) {} } export class EventNotFoundError { readonly _tag = "EventNotFoundError"; constructor(public readonly id: Id<"events">) {} } export class KnowledgeNotFoundError { readonly _tag = "KnowledgeNotFoundError"; constructor(public readonly id: Id<"knowledge">) {} } export class UnauthorizedError { readonly _tag = "UnauthorizedError"; constructor(public readonly message: string) {} } export class ValidationError { readonly _tag = "ValidationError"; constructor(public readonly message: string) {} } ``` ### 1.2 ConvexProvider Implementation Complete implementation wrapping Convex SDK: ```typescript // frontend/src/providers/ConvexProvider.ts import { Effect } from "effect"; import { ConvexReactClient } from "convex/react"; import { api } from "@/convex/_generated/api"; import type { DataProvider } from "./DataProvider"; import * as Errors from "./DataProvider"; /** * ConvexProvider - Concrete implementation of DataProvider using Convex SDK * * This provider: * 1. Wraps ALL Convex operations in Effect.ts * 2. Preserves authentication context * 3. Converts Convex errors to typed errors * 4. Ensures organization scoping * 5. Maintains performance (<10ms overhead) * * Design Principles: * - Thin wrapper (no business logic) * - Direct mapping to Convex queries/mutations * - Error transformation only * - Type-safe at compile time */ export class ConvexProvider implements DataProvider { constructor(private convex: ConvexReactClient) {} organizations = { get: (id: Id<"organizations">) => Effect.tryPromise({ try: () => this.convex.query(api.queries.organizations.get, { id }), catch: () => new Errors.OrganizationNotFoundError(id), }), list: (params: ListOrganizationsParams) => Effect.tryPromise({ try: () => this.convex.query(api.queries.organizations.list, params), catch: (error) => new Error(String(error)), }), create: (input: CreateOrganizationInput) => Effect.tryPromise({ try: () => this.convex.mutation(api.mutations.organizations.create, input), catch: (error) => new Error(String(error)), }), update: (id: Id<"organizations">, updates: Partial<Organization>) => Effect.tryPromise({ try: () => this.convex.mutation(api.mutations.organizations.update, { id, ...updates, }), catch: (error) => new Error(String(error)), }), getUsage: (id: Id<"organizations">) => Effect.tryPromise({ try: () => this.convex.query(api.queries.organizations.getUsage, { id }), catch: (error) => new Error(String(error)), }), }; people = { get: (id: Id<"people">) => Effect.tryPromise({ try: () => this.convex.query(api.queries.people.get, { id }), catch: () => new Errors.PersonNotFoundError(id), }), list: (params: ListPeopleParams) => Effect.tryPromise({ try: () => this.convex.query(api.queries.people.list, params), catch: (error) => new Error(String(error)), }), create: (input: CreatePersonInput) => Effect.tryPromise({ try: () => this.convex.mutation(api.mutations.people.create, input), catch: (error) => new Error(String(error)), }), update: (id: Id<"people">, updates: Partial<Person>) => Effect.tryPromise({ try: () => this.convex.mutation(api.mutations.people.update, { id, ...updates }), catch: (error) => new Error(String(error)), }), getOrganizations: (id: Id<"people">) => Effect.tryPromise({ try: () => this.convex.query(api.queries.people.getOrganizations, { id }), catch: (error) => new Error(String(error)), }), checkPermission: (id: Id<"people">, permission: string) => Effect.tryPromise({ try: () => this.convex.query(api.queries.people.checkPermission, { id, permission, }), catch: (error) => new Error(String(error)), }), }; things = { get: (id: Id<"things">) => Effect.tryPromise({ try: () => this.convex.query(api.queries.things.get, { id }), catch: () => new Errors.ThingNotFoundError(id), }), list: (params: ListThingsParams) => Effect.tryPromise({ try: () => this.convex.query(api.queries.things.list, params), catch: (error) => new Error(String(error)), }), create: (input: CreateThingInput) => Effect.tryPromise({ try: () => this.convex.mutation(api.mutations.things.create, input), catch: (error) => new Error(String(error)), }), update: (id: Id<"things">, updates: Partial<Thing>) => Effect.tryPromise({ try: () => this.convex.mutation(api.mutations.things.update, { id, ...updates }), catch: (error) => new Error(String(error)), }), delete: (id: Id<"things">) => Effect.tryPromise({ try: () => this.convex.mutation(api.mutations.things.delete, { id }), catch: (error) => new Error(String(error)), }), search: (params: SearchThingsParams) => Effect.tryPromise({ try: () => this.convex.query(api.queries.things.search, params), catch: (error) => new Error(String(error)), }), }; connections = { get: (id: Id<"connections">) => Effect.tryPromise({ try: () => this.convex.query(api.queries.connections.get, { id }), catch: () => new Errors.ConnectionNotFoundError(id), }), listFrom: (params: ListConnectionsFromParams) => Effect.tryPromise({ try: () => this.convex.query(api.queries.connections.listFrom, params), catch: (error) => new Error(String(error)), }), listTo: (params: ListConnectionsToParams) => Effect.tryPromise({ try: () => this.convex.query(api.queries.connections.listTo, params), catch: (error) => new Error(String(error)), }), create: (input: CreateConnectionInput) => Effect.tryPromise({ try: () => this.convex.mutation(api.mutations.connections.create, input), catch: (error) => new Error(String(error)), }), update: (id: Id<"connections">, updates: Partial<Connection>) => Effect.tryPromise({ try: () => this.convex.mutation(api.mutations.connections.update, { id, ...updates, }), catch: (error) => new Error(String(error)), }), delete: (id: Id<"connections">) => Effect.tryPromise({ try: () => this.convex.mutation(api.mutations.connections.delete, { id }), catch: (error) => new Error(String(error)), }), }; events = { get: (id: Id<"events">) => Effect.tryPromise({ try: () => this.convex.query(api.queries.events.get, { id }), catch: () => new Errors.EventNotFoundError(id), }), list: (params: ListEventsParams) => Effect.tryPromise({ try: () => this.convex.query(api.queries.events.list, params), catch: (error) => new Error(String(error)), }), log: (input: LogEventInput) => Effect.tryPromise({ try: () => this.convex.mutation(api.mutations.events.log, input), catch: (error) => new Error(String(error)), }), getTimeline: (thingId: Id<"things">) => Effect.tryPromise({ try: () => this.convex.query(api.queries.events.getTimeline, { thingId }), catch: (error) => new Error(String(error)), }), getStats: (params: EventStatsParams) => Effect.tryPromise({ try: () => this.convex.query(api.queries.events.getStats, params), catch: (error) => new Error(String(error)), }), }; knowledge = { get: (id: Id<"knowledge">) => Effect.tryPromise({ try: () => this.convex.query(api.queries.knowledge.get, { id }), catch: () => new Errors.KnowledgeNotFoundError(id), }), list: (params: ListKnowledgeParams) => Effect.tryPromise({ try: () => this.convex.query(api.queries.knowledge.list, params), catch: (error) => new Error(String(error)), }), create: (input: CreateKnowledgeInput) => Effect.tryPromise({ try: () => this.convex.mutation(api.mutations.knowledge.create, input), catch: (error) => new Error(String(error)), }), link: (input: LinkKnowledgeInput) => Effect.tryPromise({ try: () => this.convex.mutation(api.mutations.knowledge.link, input), catch: (error) => new Error(String(error)), }), unlink: (knowledgeId: Id<"knowledge">, thingId: Id<"things">) => Effect.tryPromise({ try: () => this.convex.mutation(api.mutations.knowledge.unlink, { knowledgeId, thingId, }), catch: (error) => new Error(String(error)), }), vectorSearch: (params: VectorSearchParams) => Effect.tryPromise({ try: () => this.convex.query(api.queries.knowledge.vectorSearch, params), catch: (error) => new Error(String(error)), }), }; } /** * Factory function to create ConvexProvider */ export const createConvexProvider = ( convex: ConvexReactClient, ): DataProvider => { return new ConvexProvider(convex); }; /** * React hook for using DataProvider */ import { useConvex } from "convex/react"; import { useMemo } from "react"; export const useDataProvider = (): DataProvider => { const convex = useConvex(); return useMemo(() => createConvexProvider(convex), [convex]); }; ``` ### 1.3 Effect.ts Layer Creation ```typescript // frontend/src/providers/layers.ts import { Layer, Context } from "effect"; import type { DataProvider } from "./DataProvider"; /** * DataProvider Context - Effect.ts service definition */ export class DataProviderService extends Context.Tag("DataProviderService")< DataProviderService, DataProvider >() {} /** * Create Layer from concrete DataProvider instance */ export const DataProviderLayer = ( provider: DataProvider, ): Layer.Layer<DataProviderService> => Layer.succeed(DataProviderService, provider); /** * Example usage in Effect program: * * const program = Effect.gen(function* () { * const provider = yield* DataProviderService; * const thing = yield* provider.things.get(thingId); * return thing; * }); * * const result = await Effect.runPromise( * program.pipe(Effect.provide(DataProviderLayer(convexProvider))) * ); */ ``` --- ## 2. Ontology Mapping ### Organizations (Dimension 1) **Role:** Multi-tenant isolation and backend provider configuration - **Thing Type:** `external_connection` (type for provider configs) - **Properties:** ```typescript { platform: "convex" | "supabase" | "firebase" | "custom", name: "Production Backend", baseUrl: "https://shocking-falcon-870.convex.cloud", connectionType: "rest" | "websocket" | "graphql", status: "active" | "inactive" | "error", organizationId: Id<"organizations"> } ``` - **Organization Scoping:** EVERY DataProvider operation MUST filter by `organizationId` - **Backend Selection:** Organizations can configure which backend provider to use (stored in `organizations.properties.backendProvider`) ### People (Dimension 2) **Role:** Authorization - who can configure backend providers - **Permissions:** Only `platform_owner` and `org_owner` can configure backend providers - **Actions:** - `provider:configure` - Change backend provider - `provider:view` - View provider configuration - `provider:test` - Test provider connection - **Audit:** ALL provider configuration changes MUST log events with actorId ### Things (Dimension 3) **Role:** Provider configurations stored as things - **Thing Type:** `external_connection` - **Created When:** Organization selects/configures backend provider - **Properties:** Provider-specific configuration (baseUrl, apiKey, etc.) - **Status:** `active` (current provider) or `inactive` (past providers) - **Relationships:** - Organization → external_connection (`owns`) - Organization → external_connection (`configured_by`) ### Connections (Dimension 4) **Role:** Link organizations to their backend provider configuration - **Connection Type:** `configured_by` - **Pattern:** ```typescript { fromThingId: organizationId, toThingId: externalConnectionId, relationshipType: "configured_by", metadata: { configuredAt: Date.now(), configuredBy: personId, previousProvider?: "convex" | "supabase" } } ``` ### Events (Dimension 5) **Role:** Log all provider configuration changes - **Event Types:** - `provider_configured` - Initial provider setup - `provider_changed` - Switch from one provider to another - `provider_tested` - Connection test performed - `provider_error` - Provider connection failed - **Pattern:** ```typescript { type: "provider_changed", actorId: personId, targetId: organizationId, timestamp: Date.now(), metadata: { oldProvider: "convex", newProvider: "supabase", reason: "performance" } } ``` ### Knowledge (Dimension 6) **Role:** Labels and tags for provider capabilities - **Labels:** - `capability:realtime` - Real-time subscriptions - `capability:vector_search` - Vector search support - `capability:file_storage` - File upload/storage - `protocol:rest` - REST API support - `protocol:websocket` - WebSocket support - `protocol:graphql` - GraphQL API support - **Usage:** Filter available providers by required capabilities --- ## 3. User Stories ### Story 1: Developer Uses Interface **As a** frontend developer **I want** to use a single `DataProvider` interface for all backend operations **So that** I don't need to learn Convex-specific APIs **Acceptance Criteria:** 1. Can import `DataProvider` interface from `@/providers/DataProvider` 2. All operations return `Effect.Effect<T, Error>` (typed errors) 3. No Convex imports needed in components (only `useDataProvider` hook) 4. TypeScript autocomplete shows all available operations 5. Documentation includes examples for each operation ### Story 2: Backend Developer Wraps Convex **As a** backend developer **I want** to implement `ConvexProvider` that wraps Convex SDK **So that** frontend can use Convex without coupling **Acceptance Criteria:** 1. `ConvexProvider` implements ALL methods in `DataProvider` interface 2. Each method wraps corresponding Convex query/mutation 3. Convex errors are converted to typed errors (`ThingNotFoundError`, etc.) 4. Authentication context is preserved through all operations 5. Performance overhead is <10ms per operation ### Story 3: Developer Tests in Isolation **As a** developer **I want** to mock `DataProvider` in tests **So that** I can test components without real backend **Acceptance Criteria:** 1. Can create mock implementation of `DataProvider` 2. Mock returns `Effect.succeed()` or `Effect.fail()` as needed 3. Tests run without Convex connection 4. Tests run in <1 second total 5. Test coverage >90% for all provider operations ### Story 4: Organization Configures Provider **As an** organization owner **I want** to configure which backend provider my organization uses **So that** I can choose the best solution for my needs **Acceptance Criteria:** 1. Can view current backend provider in organization settings 2. Can switch to different provider (Convex → Supabase) 3. Provider change is logged as event with actorId 4. Old provider configuration is archived (not deleted) 5. Application continues working after provider switch ### Story 5: Developer Uses Effect.ts **As a** developer **I want** to compose DataProvider operations using Effect.ts **So that** I can build complex workflows with proper error handling **Acceptance Criteria:** 1. All operations return `Effect.Effect<T, Error>` 2. Can use `Effect.gen()` to compose operations 3. Can use `Effect.catchAll()` to handle errors 4. Can use `Effect.provide()` to inject provider 5. TypeScript infers error types correctly --- ## 4. Implementation Steps ### Phase 1: Interface Definition (Days 1-2) **Step 1:** Create DataProvider interface file ```bash mkdir -p frontend/src/providers touch frontend/src/providers/DataProvider.ts ``` **Step 2:** Define base DataProvider interface with all 6 dimensions ```typescript // Copy complete interface from section 1.1 ``` **Step 3:** Define all type definitions (Organization, Person, Thing, etc.) ```typescript // Copy all type definitions from section 1.1 ``` **Step 4:** Define error classes with `_tag` pattern ```typescript // Copy all error classes from section 1.1 ``` **Step 5:** Export all types and interfaces ```typescript export type { DataProvider, Organization, Person, Thing, Connection, Event, Knowledge, }; export { OrganizationNotFoundError, PersonNotFoundError, ThingNotFoundError /* etc */, }; ``` **Step 6:** Validate TypeScript compiles ```bash cd frontend && bunx astro check ``` ### Phase 2: ConvexProvider Implementation (Days 3-4) **Step 7:** Create ConvexProvider implementation file ```bash touch frontend/src/providers/ConvexProvider.ts ``` **Step 8:** Implement ConvexProvider class ```typescript // Copy complete implementation from section 1.2 ``` **Step 9:** Implement factory function `createConvexProvider` ```typescript // Copy factory function from section 1.2 ``` **Step 10:** Implement React hook `useDataProvider` ```typescript // Copy React hook from section 1.2 ``` **Step 11:** Validate ConvexProvider implements DataProvider ```bash bunx astro check ``` **Step 12:** Test ConvexProvider methods manually ```bash bun run dev # Visit http://localhost:4321/test-provider ``` ### Phase 3: Effect.ts Integration (Day 5) **Step 13:** Create Layer definitions ```bash touch frontend/src/providers/layers.ts ``` **Step 14:** Define DataProviderService context ```typescript // Copy from section 1.3 ``` **Step 15:** Create Layer factory function ```typescript // Copy from section 1.3 ``` **Step 16:** Test Effect.ts composition ```typescript // Create test program const program = Effect.gen(function* () { const provider = yield* DataProviderService; const things = yield* provider.things.list({ type: "course" }); return things; }); const result = await Effect.runPromise( program.pipe(Effect.provide(DataProviderLayer(convexProvider))), ); ``` **Step 17:** Validate all Effect operations compile ```bash bunx astro check ``` ### Phase 4: Backend Queries Implementation (Days 6-7) **Step 18:** Create backend query files ```bash mkdir -p backend/convex/queries touch backend/convex/queries/organizations.ts touch backend/convex/queries/people.ts touch backend/convex/queries/things.ts touch backend/convex/queries/connections.ts touch backend/convex/queries/events.ts touch backend/convex/queries/knowledge.ts ``` **Step 19:** Implement organizations queries ```typescript // backend/convex/queries/organizations.ts import { query } from "../_generated/server"; import { v } from "convex/values"; export const get = query({ args: { id: v.id("organizations") }, handler: async (ctx, args) => { return await ctx.db.get(args.id); }, }); export const list = query({ args: { status: v.optional(v.string()), plan: v.optional(v.string()), limit: v.optional(v.number()), offset: v.optional(v.number()), }, handler: async (ctx, args) => { let q = ctx.db.query("organizations"); if (args.status) { q = q.filter((q) => q.eq(q.field("status"), args.status)); } return await q.take(args.limit || 100); }, }); export const getUsage = query({ args: { id: v.id("organizations") }, handler: async (ctx, args) => { const org = await ctx.db.get(args.id); if (!org) throw new Error("Organization not found"); return { organizationId: args.id, users: org.usage.users, storage: org.usage.storage, apiCalls: org.usage.apiCalls, cycle: org.usage.cycle, period: { start: Date.now() - 30 * 24 * 60 * 60 * 1000, end: Date.now() }, }; }, }); ``` **Step 20:** Implement people queries (similar pattern) ```typescript // backend/convex/queries/people.ts // Follow same pattern as organizations ``` **Step 21:** Implement things queries (with search) ```typescript // backend/convex/queries/things.ts import { query } from "../_generated/server"; import { v } from "convex/values"; export const get = query({ args: { id: v.id("things") }, handler: async (ctx, args) => { return await ctx.db.get(args.id); }, }); export const list = query({ args: { type: v.optional(v.string()), status: v.optional(v.string()), organizationId: v.optional(v.id("organizations")), limit: v.optional(v.number()), offset: v.optional(v.number()), }, handler: async (ctx, args) => { let q = ctx.db.query("things"); if (args.type) { q = q.withIndex("by_type", (q) => q.eq("type", args.type)); } if (args.organizationId) { q = q.filter((q) => q.eq(q.field("organizationId"), args.organizationId)); } if (args.status) { q = q.filter((q) => q.eq(q.field("status"), args.status)); } return await q.take(args.limit || 100); }, }); export const search = query({ args: { query: v.string(), type: v.optional(v.string()), organizationId: v.optional(v.id("organizations")), limit: v.optional(v.number()), }, handler: async (ctx, args) => { return await ctx.db .query("things") .withSearchIndex("search_things", (q) => q.search("name", args.query)) .filter((q) => { if (args.type) return q.eq(q.field("type"), args.type); return true; }) .take(args.limit || 20); }, }); ``` **Step 22:** Implement connections queries ```typescript // backend/convex/queries/connections.ts // Implement listFrom and listTo with indexes ``` **Step 23:** Implement events queries ```typescript // backend/convex/queries/events.ts // Implement list, getTimeline, getStats ``` **Step 24:** Implement knowledge queries ```typescript // backend/convex/queries/knowledge.ts // Implement list, vectorSearch ``` **Step 25:** Test all queries from frontend ```bash cd frontend && bun run dev # Test each query type ``` ### Phase 5: Backend Mutations Implementation (Days 8-9) **Step 26:** Create backend mutation files ```bash mkdir -p backend/convex/mutations touch backend/convex/mutations/organizations.ts touch backend/convex/mutations/people.ts touch backend/convex/mutations/things.ts touch backend/convex/mutations/connections.ts touch backend/convex/mutations/events.ts touch backend/convex/mutations/knowledge.ts ``` **Step 27:** Implement organizations mutations ```typescript // backend/convex/mutations/organizations.ts import { mutation } from "../_generated/server"; import { v } from "convex/values"; export const create = mutation({ args: { name: v.string(), slug: v.string(), plan: v.string(), ownerId: v.id("people"), }, handler: async (ctx, args) => { const orgId = await ctx.db.insert("organizations", { name: args.name, slug: args.slug, plan: args.plan as any, status: "trial", limits: { users: 5, storage: 1, apiCalls: 1000, cycle: 100 }, usage: { users: 1, storage: 0, apiCalls: 0, cycle: 0 }, createdAt: Date.now(), updatedAt: Date.now(), }); // Log event await ctx.db.insert("events", { type: "organization_created", actorId: args.ownerId, targetId: orgId, timestamp: Date.now(), metadata: { plan: args.plan }, }); return orgId; }, }); export const update = mutation({ args: { id: v.id("organizations"), name: v.optional(v.string()), plan: v.optional(v.string()), status: v.optional(v.string()), }, handler: async (ctx, args) => { const { id, ...updates } = args; await ctx.db.patch(id, { ...updates, updatedAt: Date.now() }); const updated = await ctx.db.get(id); return updated; }, }); ``` **Step 28:** Implement people mutations ```typescript // backend/convex/mutations/people.ts // Follow same pattern ``` **Step 29:** Implement things mutations (create, update, delete) ```typescript // backend/convex/mutations/things.ts import { mutation } from "../_generated/server"; import { v } from "convex/values"; export const create = mutation({ args: { type: v.string(), name: v.string(), properties: v.any(), status: v.optional(v.string()), organizationId: v.optional(v.id("organizations")), }, handler: async (ctx, args) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Not authenticated"); const thingId = await ctx.db.insert("things", { type: args.type as any, name: args.name, properties: args.properties, status: (args.status as any) || "draft", organizationId: args.organizationId, createdAt: Date.now(), updatedAt: Date.now(), }); // Log event await ctx.db.insert("events", { type: "entity_created", actorId: identity.tokenIdentifier, targetId: thingId, timestamp: Date.now(), metadata: { entityType: args.type }, }); return thingId; }, }); export const update = mutation({ args: { id: v.id("things"), name: v.optional(v.string()), properties: v.optional(v.any()), status: v.optional(v.string()), }, handler: async (ctx, args) => { const { id, ...updates } = args; await ctx.db.patch(id, { ...updates, updatedAt: Date.now() }); const updated = await ctx.db.get(id); return updated; }, }); export const delete = mutation({ args: { id: v.id("things") }, handler: async (ctx, args) => { await ctx.db.patch(args.id, { deletedAt: Date.now() }); }, }); ``` **Step 30:** Implement connections mutations ```typescript // backend/convex/mutations/connections.ts // Implement create, update, delete ``` **Step 31:** Implement events mutations ```typescript // backend/convex/mutations/events.ts export const log = mutation({ args: { type: v.string(), actorId: v.optional(v.id("things")), targetId: v.optional(v.id("things")), metadata: v.optional(v.any()), }, handler: async (ctx, args) => { return await ctx.db.insert("events", { type: args.type as any, actorId: args.actorId, targetId: args.targetId, timestamp: Date.now(), metadata: args.metadata, }); }, }); ``` **Step 32:** Implement knowledge mutations ```typescript // backend/convex/mutations/knowledge.ts // Implement create, link, unlink ``` **Step 33:** Test all mutations from frontend ```bash cd frontend && bun run dev # Test each mutation type ``` ### Phase 6: Testing (Day 10) **Step 34:** Create test directory ```bash mkdir -p frontend/test/providers touch frontend/test/providers/DataProvider.test.ts touch frontend/test/providers/ConvexProvider.test.ts ``` **Step 35:** Write DataProvider interface tests ```typescript // frontend/test/providers/DataProvider.test.ts import { describe, it, expect } from "vitest"; import type { DataProvider } from "@/providers/DataProvider"; describe("DataProvider Interface", () => { it("should define all 6 dimensions", () => { const operations: Array<keyof DataProvider> = [ "organizations", "people", "things", "connections", "events", "knowledge", ]; // Type-level test: this compiles if interface is complete expect(operations.length).toBe(6); }); }); ``` **Step 36:** Write ConvexProvider unit tests ```typescript // frontend/test/providers/ConvexProvider.test.ts import { describe, it, expect, vi } from "vitest"; import { Effect } from "effect"; import { ConvexProvider } from "@/providers/ConvexProvider"; import type { ConvexReactClient } from "convex/react"; describe("ConvexProvider", () => { it("should get organization by id", async () => { const mockConvex = { query: vi.fn().mockResolvedValue({ _id: "org_123", name: "Test Org", plan: "pro", }), } as unknown as ConvexReactClient; const provider = new ConvexProvider(mockConvex); const result = await Effect.runPromise( provider.organizations.get("org_123" as any), ); expect(result.name).toBe("Test Org"); }); it("should handle not found errors", async () => { const mockConvex = { query: vi.fn().mockRejectedValue(new Error("Not found")), } as unknown as ConvexReactClient; const provider = new ConvexProvider(mockConvex); const result = Effect.runPromiseExit( provider.organizations.get("org_123" as any), ); await expect(result).rejects.toMatchObject({ _tag: "OrganizationNotFoundError", }); }); }); ``` **Step 37:** Write integration tests ```typescript // frontend/test/providers/integration.test.ts import { describe, it, expect } from "vitest"; import { ConvexHttpClient } from "convex/browser"; import { createConvexProvider } from "@/providers/ConvexProvider"; import { Effect } from "effect"; describe("ConvexProvider Integration", () => { const convex = new ConvexHttpClient(import.meta.env.PUBLIC_CONVEX_URL); const provider = createConvexProvider(convex); it("should list things", async () => { const result = await Effect.runPromise( provider.things.list({ type: "course", limit: 10 }), ); expect(Array.isArray(result)).toBe(true); }); it("should create and delete thing", async () => { const createResult = await Effect.runPromise( provider.things.create({ type: "course", name: "Test Course", properties: { description: "Test" }, }), ); expect(createResult).toBeDefined(); await Effect.runPromise(provider.things.delete(createResult)); }); }); ``` **Step 38:** Run all tests ```bash cd frontend && bun test test/providers ``` **Step 39:** Measure test coverage ```bash bun test --coverage test/providers # Target: >90% coverage ``` **Step 40:** Write performance tests ```typescript // frontend/test/providers/performance.test.ts import { describe, it, expect } from "vitest"; import { createConvexProvider } from "@/providers/ConvexProvider"; import { Effect } from "effect"; describe("ConvexProvider Performance", () => { it("should have <10ms overhead per operation", async () => { const provider = createConvexProvider(convex); const start = performance.now(); await Effect.runPromise( provider.things.list({ type: "course", limit: 100 }), ); const end = performance.now(); const overhead = end - start; expect(overhead).toBeLessThan(10); }); }); ``` --- ## 5. Testing Strategy ### Unit Tests (90%+ coverage) **Test 1: DataProvider Interface Type Checking** ```typescript // Ensures interface is complete import type { DataProvider } from "@/providers/DataProvider"; const validateInterface = (provider: DataProvider) => { // TypeScript enforces all methods exist expect(provider.organizations).toBeDefined(); expect(provider.people).toBeDefined(); expect(provider.things).toBeDefined(); expect(provider.connections).toBeDefined(); expect(provider.events).toBeDefined(); expect(provider.knowledge).toBeDefined(); }; ``` **Test 2: ConvexProvider Method Wrapping** ```typescript describe("ConvexProvider.things.get", () => { it("should call convex.query with correct args", async () => { const mockQuery = vi.fn().mockResolvedValue({ _id: "thing_123" }); const mockConvex = { query: mockQuery } as any; const provider = new ConvexProvider(mockConvex); await Effect.runPromise(provider.things.get("thing_123" as any)); expect(mockQuery).toHaveBeenCalledWith( expect.any(Function), // api.queries.things.get { id: "thing_123" }, ); }); }); ``` **Test 3: Error Transformation** ```typescript describe("ConvexProvider Error Handling", () => { it("should transform 404 to ThingNotFoundError", async () => { const mockConvex = { query: vi.fn().mockRejectedValue(new Error("Not found")), } as any; const provider = new ConvexProvider(mockConvex); const result = await Effect.runPromiseExit( provider.things.get("thing_123" as any), ); expect(result._tag).toBe("Failure"); expect(result.cause._tag).toBe("ThingNotFoundError"); }); }); ``` **Test 4: Effect.ts Composition** ```typescript describe("Effect.ts Composition", () => { it("should compose multiple operations", async () => { const program = Effect.gen(function* () { const provider = yield* DataProviderService; const thing = yield* provider.things.get("thing_123" as any); const connections = yield* provider.connections.listFrom({ fromThingId: thing._id, }); return { thing, connections }; }); const result = await Effect.runPromise( program.pipe(Effect.provide(DataProviderLayer(mockProvider))), ); expect(result.thing).toBeDefined(); expect(result.connections).toBeDefined(); }); }); ``` **Test 5: All 6 Dimensions Covered** ```typescript describe("Full DataProvider Coverage", () => { const dimensions = [ "organizations", "people", "things", "connections", "events", "knowledge", ] as const; dimensions.forEach((dimension) => { it(`should implement ${dimension} operations`, () => { const provider = createConvexProvider(mockConvex); expect(provider[dimension]).toBeDefined(); expect(typeof provider[dimension]).toBe("object"); }); }); }); ``` ### Integration Tests (80%+ coverage) **Test 6: Create Thing Flow** ```typescript describe("Create Thing Integration", () => { it("should create thing, log event, return ID", async () => { const provider = createConvexProvider(convex); const thingId = await Effect.runPromise( provider.things.create({ type: "course", name: "Integration Test Course", properties: { description: "Test" }, }), ); expect(thingId).toMatch(/^[a-z0-9]+$/); // Verify thing was created const thing = await Effect.runPromise(provider.things.get(thingId)); expect(thing.name).toBe("Integration Test Course"); // Verify event was logged const events = await Effect.runPromise( provider.events.list({ targetId: thingId }), ); expect(events.some((e) => e.type === "entity_created")).toBe(true); // Cleanup await Effect.runPromise(provider.things.delete(thingId)); }); }); ``` **Test 7: Connection Creation Flow** ```typescript describe("Create Connection Integration", () => { it("should create connection between two things", async () => { const provider = createCo