Back to catalog

api-service

This skill teaches the agent how to implement the business logic layer in the API project. MANDATORY - You MUST read this skill before modifying any service files.

  • Category: discovered
  • Tags: github, community
  • Version: 1.0.0
  • Author: walid
  • Downloads: 0
  • Updated: 2025-11-22

Download the latest version (v1.0.0) from this page.

SKILL.md

---
name: api-service
description: This skill teaches the agent how to implement the business logic layer in the API project. MANDATORY - You MUST read this skill before modifying any service files.
---

# API Service Layer Skill

## Purpose
This skill covers the service layer of the API project - the business logic layer that orchestrates repositories, handles authorization, and manages complex transactions.

## When to Use This Skill
- Implementing new business features
- Orchestrating multiple data operations
- Adding OpenFGA authorization logic
- Managing complex transactions with rollback logic
- Validating business rules before data persistence

## Key Patterns from Codebase

### 1. Service Structure
```typescript
// api/src/services/community.service.ts
import { communityRepository } from '@/repositories/community.repository';
import { appUserRepository } from '@/repositories/appUser.repository';
import { openfgaService } from '@/services/openfga.service';
import { AppError } from '@/utils/errors';
import logger from '@/utils/logger';

class CommunityService {
  async createCommunity(data: CreateCommunityDto, userId: string): Promise<Community> {
    // 1. Verify prerequisites
    const user = await appUserRepository.findById(userId);
    if (!user) {
      throw new AppError("User profile not found. Please complete registration.", 404);
    }

    // 2. Create main resource
    const community = await communityRepository.create({
      ...data,
      createdBy: userId
    });

    // 3. Critical: Handle related operations with rollback
    try {
      await communityMemberRepository.addMember(community.id, userId, "admin");
      await openfgaService.writeTuple({
        user: `user:${userId}`,
        relation: 'admin',
        object: `community:${community.id}`
      });
    } catch (err) {
      // Rollback on failure
      await communityRepository.delete(community.id);
      logger.error("Failed to assign admin role", { communityId: community.id, userId, error: err });
      throw new AppError("Failed to create community. Admin assignment failed.", 500);
    }

    return community;
  }
}

export const communityService = new CommunityService();
```

### 2. Authorization Checks (Using Permissions)
```typescript
// IMPORTANT: Always check PERMISSIONS, not roles
// The permission system automatically includes admin + feature roles + trust roles

async updateCommunity(id: string, data: UpdateCommunityDto, userId: string): Promise<Community> {
  // 1. Verify resource exists
  const community = await communityRepository.findById(id);
  if (!community) {
    throw new AppError("Community not found", 404);
  }

  // 2. Check permissions via OpenFGA - Use checkAccess()
  // This checks: admin OR has explicit update permission OR has trust-based update access
  const canUpdate = await openFGAService.checkAccess(
    userId,
    'community',  // Resource type
    id,           // Resource ID
    'update'      // Action (maps to 'can_update')
  );

  if (!canUpdate) {
    throw new AppError("You do not have permission to update this community", 403);
  }

  // 3. Perform update
  return await communityRepository.update(id, data);
}
```

**Why this pattern?**
- `checkAccess()` automatically checks all permission sources (admin + roles + trust)
- Action names map to permissions ('update' → 'can_update')
- Trust-based access is automatic (no manual trust checks needed)
- Cleaner, more maintainable code

### 3. Feature-Specific Permission Checks
```typescript
async manageForumCategory(
  categoryId: string,
  userId: string,
  action: 'update' | 'delete'
): Promise<void> {
  // 1. Get category and verify it exists
  const category = await forumRepository.findById(categoryId);
  if (!category) throw new AppError("Category not found", 404);

  // 2. Check feature-specific permission
  // This checks: admin OR forum_manager OR trust_forum_manager
  const canManage = await openFGAService.checkAccess(
    userId,
    'community',
    category.communityId,
    'can_manage_forum'  // Feature permission
  );

  if (!canManage) {
    throw new AppError("You do not have permission to manage forum categories", 403);
  }

  // 3. Perform action
  if (action === 'delete') {
    await forumRepository.delete(categoryId);
  }
}
```

### 4. Transaction Management with Multiple Repositories
```typescript
async shareWealthToCouncil(
  wealthId: string,
  councilId: string,
  userId: string
): Promise<WealthShare> {
  // 1. Verify all resources exist
  const wealth = await wealthRepository.findById(wealthId);
  if (!wealth) throw new AppError("Wealth item not found", 404);

  const council = await councilRepository.findById(councilId);
  if (!council) throw new AppError("Council not found", 404);

  // 2. Check authorization - Use permission, not manual trust check
  const canShare = await openFGAService.checkAccess(
    userId,
    'community',
    wealth.communityId,
    'can_create_wealth'  // Permission includes trust check automatically
  );

  if (!canShare) {
    throw new AppError("You do not have permission to share wealth", 403);
  }

  // 3. Create wealth share (auto-fulfilled for councils)
  const share = await wealthRepository.createShare({
    wealthId,
    targetType: 'council',
    targetId: councilId,
    status: 'fulfilled',
    fulfilledAt: new Date()
  });

  // 4. Update council inventory
  try {
    await councilRepository.addToInventory(councilId, {
      itemId: wealthId,
      quantity: wealth.quantity,
      sourceUserId: userId
    });
  } catch (err) {
    // Rollback the share if inventory update fails
    await wealthRepository.deleteShare(share.id);
    logger.error("Council inventory update failed", { shareId: share.id, error: err });
    throw new AppError("Failed to transfer wealth to council", 500);
  }

  return share;
}
```

### 5. Batch Operations
```typescript
async getUserCommunitiesWithRoles(userId: string): Promise<CommunityWithRole[]> {
  // 1. Get user memberships
  const memberships = await communityMemberRepository.findByUser(userId);
  const communityIds = memberships.map(m => m.resourceId);

  if (communityIds.length === 0) return [];

  // 2. Batch fetch communities
  const communities = await communityRepository.findByIds(communityIds);

  // 3. Fetch base roles from OpenFGA (parallel for performance)
  const rolePromises = communityIds.map(id =>
    openFGAService.getUserBaseRole(userId, 'community', id)
  );
  const roles = await Promise.all(rolePromises);

  // 4. Combine data
  return communities.map((community, index) => ({
    ...community,
    role: roles[index] // 'admin' | 'member' | null
  }));
}
```

### 6. Error Handling Best Practices
```typescript
async deleteCommunity(id: string, userId: string): Promise<void> {
  try {
    // 1. Verify exists
    const community = await communityRepository.findById(id);
    if (!community) {
      throw new AppError("Community not found", 404);
    }

    // 2. Check admin permission
    const isAdmin = await communityMemberRepository.isAdmin(id, userId);
    if (!isAdmin) {
      throw new AppError("Only admins can delete communities", 403);
    }

    // 3. Soft delete (cascading handled in repository)
    await communityRepository.delete(id);

    logger.info("Community deleted", { communityId: id, deletedBy: userId });
  } catch (err) {
    if (err instanceof AppError) {
      throw err;
    }
    logger.error("Unexpected error deleting community", { communityId: id, error: err });
    throw new AppError("Failed to delete community", 500);
  }
}
```

## Testing Services

Services should have comprehensive unit tests with mocked repositories:

```typescript
// api/src/services/community.service.test.ts
import { describe, it, expect, beforeEach, mock } from "bun:test";
import { communityService } from "@/services/community.service";
import { communityRepository } from "@/repositories/community.repository";
import { AppError } from "@/utils/errors";
import { testData } from "../../tests/helpers/testUtils";

const mockCommunityRepository = {
  create: mock(() => Promise.resolve(testData.community)),
  findById: mock(() => Promise.resolve(testData.community)),
  update: mock(() => Promise.resolve(testData.community)),
  delete: mock(() => Promise.resolve(testData.community)),
};

describe("CommunityService", () => {
  beforeEach(() => {
    Object.values(mockCommunityRepository).forEach((m) => m.mockReset());
    (communityRepository.create as any) = mockCommunityRepository.create;
    (communityRepository.findById as any) = mockCommunityRepository.findById;
  });

  describe("createCommunity", () => {
    it("should create a community and assign creator as admin", async () => {
      const result = await communityService.createCommunity(
        { name: "Test Community", description: "Test" },
        "user-123"
      );

      expect(result).toEqual(testData.community);
      expect(mockCommunityRepository.create).toHaveBeenCalledWith({
        name: "Test Community",
        description: "Test",
        createdBy: "user-123"
      });
    });

    it("should throw AppError if user not found", async () => {
      mockAppUserRepository.findById.mockResolvedValue(null);

      await expect(
        communityService.createCommunity({ name: "Test" }, "invalid-user")
      ).rejects.toThrow(AppError);
    });
  });
});
```

## OpenFGA Permission Guidelines

### ✅ DO: Check Permissions (Not Roles)

```typescript
// ✅ CORRECT: Check the permission
const canManage = await openFGAService.checkAccess(
  userId,
  'community',
  communityId,
  'can_manage_forum'
);

// ❌ WRONG: Don't check roles directly
const hasRole = await openFGAService.check({
  user: `user:${userId}`,
  relation: 'forum_manager',  // Don't check roles
  object: `community:${communityId}`
});
```

### Available Permissions by Feature

```typescript
// Trust Feature
'can_view_trust', 'can_award_trust'

// Wealth Feature
'can_view_wealth', 'can_create_wealth'

// Poll Feature
'can_view_poll', 'can_create_poll'

// Dispute Feature
'can_view_dispute', 'can_handle_dispute'

// Pool Feature
'can_view_pool', 'can_create_pool'

// Council Feature
'can_view_council', 'can_create_council'

// Forum Feature
'can_view_forum', 'can_manage_forum', 'can_create_thread',
'can_upload_attachment', 'can_flag_content', 'can_review_flag'

// Items Feature
'can_view_item', 'can_manage_item'

// Analytics Feature
'can_view_analytics'

// Generic CRUD (can be used with any resource)
'can_read', 'can_update', 'can_delete'
```

### How Permissions Work

Each permission is a **union** of multiple sources:
```
can_manage_forum = admin OR forum_manager OR trust_forum_manager
```

Where:
- `admin` = Base role (always has all permissions)
- `forum_manager` = Feature role assigned by admin
- `trust_forum_manager` = Trust role (auto-granted at threshold)

**You only check the permission** - OpenFGA evaluates all sources automatically.

### When to Use Each Method

```typescript
// Permission checks (most common)
checkAccess(userId, resourceType, resourceId, permission)

// Get user's base role (admin/member)
getUserBaseRole(userId, resourceType, resourceId)

// Assign base role (admin/member) - Only for membership
assignBaseRole(userId, resourceType, resourceId, 'admin' | 'member')

// Assign feature role - Only for admins granting specific permissions
assignFeatureRole(userId, communityId, featureRole)

// Low-level checks - Only when you need exact tuple control
check({ user, relation, object })
```

## Key Principles

1. **Check Permissions, Not Roles**: Always use `checkAccess()` with permissions (`can_*`)
2. **Single Responsibility**: Each service method handles one business operation
3. **Repository Orchestration**: Services coordinate multiple repositories
4. **Trust is Automatic**: Never manually check trust - it's included in permissions
5. **Rollback on Failure**: Critical operations must rollback on error
6. **Comprehensive Logging**: Log all operations for debugging and audit
7. **Error Handling**: Use AppError for all business logic errors
8. **Type Safety**: Use DTOs and types from `@/types/` directory
9. **Testability**: Design for easy mocking of dependencies

## Related Skills
- `api-repository` - Data access layer
- `api-controller` - HTTP request handling
- `api-testing` - Testing patterns
- `api-config` - OpenFGA and Keycloak configuration

## Feature Documentation
Before implementing service logic, **MUST READ** the relevant feature documentation in `docs/features/` to understand:
- Business rules and validation requirements
- Trust threshold configurations
- Permission models (role-based vs trust-based)
- Related database tables