tenantpilot/specs/001-global-policy-search/contracts/server-actions.md
2025-12-05 22:06:22 +01:00

4.4 KiB

Server Actions Contract: Policy Search

Feature: 001-global-policy-search
Type: Next.js Server Actions


Overview

Server Actions for the Global Policy Search feature. These actions are called directly from React Server Components and Client Components without API routes.


Action: searchPolicySettings

File: lib/actions/policySettings.ts

Signature

'use server';

export async function searchPolicySettings(
  searchTerm: string
): Promise<SearchResult>;

Input

Parameter Type Required Description
searchTerm string Yes Search query (min 2 chars)

Output

interface SearchResult {
  success: boolean;
  data?: PolicySettingSearchResult[];
  error?: string;
  totalCount?: number;
}

interface PolicySettingSearchResult {
  id: string;
  policyName: string;
  policyType: string;
  settingName: string;
  settingValue: string;
  lastSyncedAt: Date;
}

Behavior

  1. Authentication: Validates user session via getUserAuth()
  2. Tenant Isolation: Extracts tenantId from session, filters all queries
  3. Search: Case-insensitive search on settingName and settingValue
  4. Limit: Returns max 100 results, sorted by settingName

Error Responses

Condition Response
Not authenticated { success: false, error: 'Unauthorized' }
Search term < 2 chars { success: false, error: 'Search term too short' }
No tenant ID in session { success: false, error: 'Tenant not found' }
Database error { success: false, error: 'Search failed' }

Example Usage (Client Component)

'use client';

import { searchPolicySettings } from '@/lib/actions/policySettings';
import { useState, useTransition } from 'react';

function SearchForm() {
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  const handleSearch = (formData: FormData) => {
    const query = formData.get('query') as string;
    startTransition(async () => {
      const result = await searchPolicySettings(query);
      if (result.success) {
        setResults(result.data ?? []);
      }
    });
  };

  return (
    <form action={handleSearch}>
      <input name="query" placeholder="Search settings..." />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Searching...' : 'Search'}
      </button>
    </form>
  );
}

Action: getPolicySettingById

File: lib/actions/policySettings.ts

Signature

'use server';

export async function getPolicySettingById(
  id: string
): Promise<GetSettingResult>;

Input

Parameter Type Required Description
id string Yes Policy setting ID (CUID2)

Output

interface GetSettingResult {
  success: boolean;
  data?: PolicySetting;
  error?: string;
}

Behavior

  1. Authentication: Validates user session
  2. Tenant Isolation: Ensures setting belongs to user's tenant
  3. Return: Full policy setting record or null

Action: getRecentPolicySettings

File: lib/actions/policySettings.ts

Signature

'use server';

export async function getRecentPolicySettings(
  limit?: number
): Promise<RecentSettingsResult>;

Input

Parameter Type Required Default Description
limit number No 20 Max results (1-100)

Output

interface RecentSettingsResult {
  success: boolean;
  data?: PolicySettingSearchResult[];
  error?: string;
}

Behavior

  1. Authentication: Validates user session
  2. Tenant Isolation: Filters by tenantId
  3. Sort: By lastSyncedAt descending
  4. Limit: Capped at 100

Security Invariants

All Server Actions MUST:

  1. Call getUserAuth() at the start
  2. Return { success: false, error: 'Unauthorized' } if no session
  3. Extract tenantId from session
  4. Include tenantId in ALL database queries (WHERE clause)
  5. Never expose settings from other tenants
  6. Validate and sanitize all input parameters

Type Exports

// lib/actions/policySettings.ts

export type {
  SearchResult,
  GetSettingResult,
  RecentSettingsResult,
  PolicySettingSearchResult,
};