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.

259 lines (222 loc) 7.37 kB
--- title: Service Template dimension: knowledge category: patterns tags: backend, things related_dimensions: events, groups, things scope: global created: 2025-11-03 updated: 2025-11-03 version: 1.0.0 ai_context: | This document is part of the knowledge dimension in the patterns category. Location: one/knowledge/patterns/backend/service-template.md Purpose: Documents pattern: effect.ts service template Related dimensions: events, groups, things For AI agents: Read this to understand service template. --- # Pattern: Effect.ts Service Template **Category:** Backend **Context:** When creating business logic services with Effect.ts for type-safe, composable operations **Problem:** Need consistent service structure that handles errors, dependencies, and effects properly ## Solution Use Effect.ts services for all business logic. Keep Convex functions thin (just call services). Services are pure, testable, and composable. ## Template ```typescript // convex/services/{EntityName}Service.ts import { Effect, Context } from "effect"; import { Doc, Id } from "../_generated/dataModel"; // Service interface export class {EntityName}Service extends Context.Tag("{EntityName}Service")< {EntityName}Service, { create: (data: Create{EntityName}Input) => Effect.Effect<Id<"things">, {EntityName}Error>; getById: (id: Id<"things">) => Effect.Effect<Doc<"things">, {EntityName}Error>; update: (id: Id<"things">, data: Update{EntityName}Input) => Effect.Effect<void, {EntityName}Error>; delete: (id: Id<"things">) => Effect.Effect<void, {EntityName}Error>; } >() {} // Error types (tagged union) export type {EntityName}Error = | { _tag: "{EntityName}NotFound"; id: string } | { _tag: "{EntityName}AlreadyExists"; name: string } | { _tag: "{EntityName}ValidationError"; message: string } | { _tag: "DatabaseError"; message: string }; // Input types export interface Create{EntityName}Input { name: string; properties: Record<string, any>; groupId: Id<"groups">; } export interface Update{EntityName}Input { name?: string; properties?: Record<string, any>; status?: "draft" | "active" | "archived"; } // Implementation export const {EntityName}ServiceLive = {EntityName}Service.of({ create: (data) => Effect.gen(function* (_) { // Validate input if (!data.name) { return yield* _(Effect.fail<{EntityName}Error>({ _tag: "{EntityName}ValidationError", message: "Name is required" })); } // Create entity const entityId = yield* _( Effect.tryPromise({ try: () => db.insert("things", { type: "{entityType}", name: data.name, properties: data.properties, groupId: data.groupId, status: "draft", createdAt: Date.now(), updatedAt: Date.now(), }), catch: (error) => ({ _tag: "DatabaseError" as const, message: String(error) }) }) ); // Log event (optional) yield* _(logEvent({ type: "{entity}_created", actorId: "current-user-id", targetId: entityId, timestamp: Date.now(), })); return entityId; }), getById: (id) => Effect.gen(function* (_) { const entity = yield* _( Effect.tryPromise({ try: () => db.get(id), catch: () => ({ _tag: "{EntityName}NotFound" as const, id: id }) }) ); if (!entity) { return yield* _(Effect.fail<{EntityName}Error>({ _tag: "{EntityName}NotFound", id: id })); } return entity; }), update: (id, data) => Effect.gen(function* (_) { // Get existing entity const existing = yield* _({EntityName}Service.getById(id)); // Update yield* _( Effect.tryPromise({ try: () => db.patch(id, { ...data, updatedAt: Date.now(), }), catch: (error) => ({ _tag: "DatabaseError" as const, message: String(error) }) }) ); // Log event (optional) yield* _(logEvent({ type: "{entity}_updated", actorId: "current-user-id", targetId: id, timestamp: Date.now(), })); }), delete: (id) => Effect.gen(function* (_) { // Verify exists yield* _({EntityName}Service.getById(id)); // Soft delete (set status) yield* _( Effect.tryPromise({ try: () => db.patch(id, { status: "archived", updatedAt: Date.now(), }), catch: (error) => ({ _tag: "DatabaseError" as const, message: String(error) }) }) ); // Log event (optional) yield* _(logEvent({ type: "{entity}_deleted", actorId: "current-user-id", targetId: id, timestamp: Date.now(), })); }), }); ``` ## Variables - `{EntityName}` - PascalCase entity name (e.g., `Course`, `Lesson`, `Enrollment`) - `{things}` - Table name (always "things" in 6-dimension ontology) - `{entityType}` - Entity type value from 66 canonical types (e.g., "course", "lesson") - `{entity}` - Lowercase entity name for events (e.g., "course_created", "course_updated") ## Usage 1. Copy template to `convex/services/{EntityName}Service.ts` 2. Replace all `{EntityName}` with your entity name 3. Add entity-specific validation logic 4. Add custom methods as needed 5. Use in Convex mutations/queries (thin wrappers) ## Example (Course Service) ```typescript // convex/services/CourseService.ts export class CourseService extends Context.Tag("CourseService")< CourseService, { create: (data: CreateCourseInput) => Effect.Effect<Id<"things">, CourseError>; getById: (id: Id<"things">) => Effect.Effect<Doc<"things">, CourseError>; publish: (id: Id<"things">) => Effect.Effect<void, CourseError>; } >() {} // Used in Convex mutation export const create = mutation({ args: { name: v.string(), description: v.string(), }, handler: async (ctx, args) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Not authenticated"); const program = CourseService.create({ name: args.name, properties: { description: args.description }, groupId: identity.groupId, }); return await Effect.runPromise( program.pipe( Effect.provideService(CourseService, CourseServiceLive), Effect.provideService(DatabaseService, DatabaseServiceLive(ctx.db)) ) ); }, }); ``` ## Common Mistakes - **Mistake:** Putting business logic in Convex functions - **Fix:** Keep Convex functions thin, move logic to services - **Mistake:** Using `any` types in service methods - **Fix:** Define explicit input/output types - **Mistake:** Not handling errors with tagged unions - **Fix:** Use `{ _tag: "ErrorType" }` pattern - **Mistake:** Direct database calls without Effect - **Fix:** Wrap db calls in `Effect.tryPromise` ## Related Patterns - **mutation-template.md** - How to call services from Convex mutations - **query-template.md** - How to call services from Convex queries - **event-logging.md** - How to log events from services