4.4 KiB
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
- Authentication: Validates user session via
getUserAuth() - Tenant Isolation: Extracts
tenantIdfrom session, filters all queries - Search: Case-insensitive search on
settingNameandsettingValue - 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
- Authentication: Validates user session
- Tenant Isolation: Ensures setting belongs to user's tenant
- 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
- Authentication: Validates user session
- Tenant Isolation: Filters by
tenantId - Sort: By
lastSyncedAtdescending - Limit: Capped at 100
Security Invariants
All Server Actions MUST:
- ✅ Call
getUserAuth()at the start - ✅ Return
{ success: false, error: 'Unauthorized' }if no session - ✅ Extract
tenantIdfrom session - ✅ Include
tenantIdin ALL database queries (WHERE clause) - ✅ Never expose settings from other tenants
- ✅ Validate and sanitize all input parameters
Type Exports
// lib/actions/policySettings.ts
export type {
SearchResult,
GetSettingResult,
RecentSettingsResult,
PolicySettingSearchResult,
};