248 lines
6.4 KiB
Markdown
248 lines
6.4 KiB
Markdown
# Implementation Plan: Global Policy Search
|
|
|
|
**Feature**: 001-global-policy-search
|
|
**Branch**: `001-global-policy-search`
|
|
**Version**: 1.0.0
|
|
**Created**: 2025-12-05
|
|
|
|
---
|
|
|
|
## Technical Context
|
|
|
|
| Aspect | Decision | Notes |
|
|
|--------|----------|-------|
|
|
| Database ORM | Drizzle ORM | Existing in project |
|
|
| Search Method | PostgreSQL `ilike` | Case-insensitive substring search |
|
|
| Auth Provider | Azure AD (NextAuth) | Extract `tenantId` from `tid` claim |
|
|
| UI Components | Shadcn UI | Input, Table, Card |
|
|
| Data Fetching | Server Actions | Constitution: Server-First |
|
|
| API Security | X-API-SECRET header | For n8n ingestion |
|
|
|
|
---
|
|
|
|
## Constitution Check
|
|
|
|
| Principle | Compliant | Implementation |
|
|
|-----------|-----------|----------------|
|
|
| Server-First | ✅ | Server Actions for search, API route for ingestion |
|
|
| TypeScript Strict | ✅ | Full types for schema, validators, actions |
|
|
| Drizzle ORM | ✅ | `policySettings` table with proper indexes |
|
|
| Shadcn UI | ✅ | Input, Table components for search UI |
|
|
| Azure AD Multi-Tenancy | ✅ | tenantId from session, all queries filtered |
|
|
|
|
---
|
|
|
|
## Gate Evaluation
|
|
|
|
### Gate 1: Scope Check
|
|
- [x] Feature spec exists: `specs/001-global-policy-search/spec.md`
|
|
- [x] User stories defined: 3 stories (Search, Tenant Isolation, Ingestion API)
|
|
- [x] Requirements numbered: F-001 through F-009
|
|
- **Result**: PASS
|
|
|
|
### Gate 2: Constitution Compliance
|
|
- [x] No client-side data fetching for primary flows
|
|
- [x] All types explicitly defined (no `any`)
|
|
- [x] Database via Drizzle ORM only
|
|
- [x] UI components from Shadcn library
|
|
- [x] Tenant isolation enforced
|
|
- **Result**: PASS
|
|
|
|
### Gate 3: Security Review
|
|
- [x] Authentication required for search (getUserAuth)
|
|
- [x] Tenant isolation in all queries (tenantId filter)
|
|
- [x] API secret for ingestion endpoint (X-API-SECRET)
|
|
- [x] Input validation with Zod schemas
|
|
- **Result**: PASS
|
|
|
|
---
|
|
|
|
## Phase 0: Research (Complete)
|
|
|
|
See: `specs/001-global-policy-search/research.md`
|
|
|
|
Resolved:
|
|
- Drizzle `ilike` for case-insensitive search
|
|
- Server Actions pattern for search
|
|
- API_SECRET header authentication
|
|
- Azure AD tenantId extraction from session
|
|
- Upsert with `onConflictDoUpdate`
|
|
|
|
---
|
|
|
|
## Phase 1: Design (Complete)
|
|
|
|
### Artifacts Generated
|
|
|
|
| Artifact | Path | Status |
|
|
|----------|------|--------|
|
|
| Data Model | `specs/001-global-policy-search/data-model.md` | ✅ |
|
|
| API Contract | `specs/001-global-policy-search/contracts/policy-settings-api.yaml` | ✅ |
|
|
| Server Actions Contract | `specs/001-global-policy-search/contracts/server-actions.md` | ✅ |
|
|
| Quickstart Guide | `specs/001-global-policy-search/quickstart.md` | ✅ |
|
|
|
|
---
|
|
|
|
## Phase 2: Implementation Tasks
|
|
|
|
### Task 1: Database Schema
|
|
**File**: `lib/db/schema/policySettings.ts`
|
|
**Priority**: P1
|
|
**Estimate**: 30 min
|
|
|
|
- Create `policySettings` pgTable
|
|
- Add indexes for tenant filtering and search
|
|
- Export types: `PolicySetting`, `NewPolicySetting`
|
|
- Run `npm run db:push`
|
|
|
|
### Task 2: Zod Validators
|
|
**File**: `lib/validators/policySettings.ts`
|
|
**Priority**: P1
|
|
**Estimate**: 15 min
|
|
|
|
- `policySettingSchema` for single setting
|
|
- `bulkPolicySettingsSchema` for API input
|
|
- Export input types
|
|
|
|
### Task 3: Extend NextAuth Session
|
|
**File**: `lib/auth/utils.ts`
|
|
**Priority**: P1
|
|
**Estimate**: 20 min
|
|
|
|
- Add `jwt` callback to extract `tid` claim
|
|
- Add `session` callback to include `tenantId`
|
|
- Update TypeScript types for extended session
|
|
|
|
### Task 4: Server Actions
|
|
**File**: `lib/actions/policySettings.ts`
|
|
**Priority**: P1
|
|
**Estimate**: 45 min
|
|
|
|
- `searchPolicySettings(searchTerm)` with ilike query
|
|
- `getPolicySettingById(id)` with tenant check
|
|
- `getRecentPolicySettings(limit)` sorted by lastSyncedAt
|
|
- Full tenant isolation in all queries
|
|
|
|
### Task 5: Ingestion API Route
|
|
**File**: `app/api/policy-settings/route.ts`
|
|
**Priority**: P2
|
|
**Estimate**: 30 min
|
|
|
|
- POST handler with X-API-SECRET validation
|
|
- Bulk upsert with `onConflictDoUpdate`
|
|
- DELETE handler for tenant cleanup
|
|
- Proper error responses (400, 401, 500)
|
|
|
|
### Task 6: Environment Variable
|
|
**File**: `.env` (local), `lib/env.mjs`
|
|
**Priority**: P2
|
|
**Estimate**: 10 min
|
|
|
|
- Add `POLICY_API_SECRET` to .env
|
|
- Add optional validation in env.mjs
|
|
|
|
### Task 7: Search Page UI
|
|
**File**: `app/(app)/search/page.tsx`
|
|
**Priority**: P1
|
|
**Estimate**: 45 min
|
|
|
|
- Search input with debounce
|
|
- Results table with columns
|
|
- Loading and empty states
|
|
- Error handling
|
|
|
|
### Task 8: Search Components
|
|
**Files**: `components/search/*.tsx`
|
|
**Priority**: P1
|
|
**Estimate**: 30 min
|
|
|
|
- `SearchInput.tsx` - Input with search icon
|
|
- `ResultsTable.tsx` - Table with policy data
|
|
- `EmptyState.tsx` - No results message
|
|
|
|
### Task 9: Navigation Update
|
|
**File**: `config/nav.ts`
|
|
**Priority**: P1
|
|
**Estimate**: 5 min
|
|
|
|
- Add search link to sidebar
|
|
- Icon: Search or MagnifyingGlass
|
|
|
|
---
|
|
|
|
## Dependencies (Implementation Order)
|
|
|
|
```
|
|
Task 1 (Schema)
|
|
↓
|
|
Task 2 (Validators)
|
|
↓
|
|
Task 3 (Auth) ──────┬──→ Task 4 (Server Actions)
|
|
│
|
|
└──→ Task 5 (API Route)
|
|
↓
|
|
Task 6 (Env Var)
|
|
|
|
Task 4 (Server Actions)
|
|
↓
|
|
Task 7 (Search Page) ←── Task 8 (Components)
|
|
↓
|
|
Task 9 (Navigation)
|
|
```
|
|
|
|
---
|
|
|
|
## Acceptance Criteria
|
|
|
|
From spec requirements:
|
|
|
|
- [ ] F-001: Full-text search across settings (ilike on name/value)
|
|
- [ ] F-002: Display policy name, type, setting name/value
|
|
- [ ] F-003: Debounced instant search (300ms)
|
|
- [ ] F-004: User only sees own tenant's data
|
|
- [ ] F-005: No tenant impersonation possible
|
|
- [ ] F-006: API_SECRET header required for ingestion
|
|
- [ ] F-007: Bulk upsert endpoint operational
|
|
- [ ] F-008: Zod validation on all API inputs
|
|
- [ ] F-009: Upsert logic prevents duplicates
|
|
|
|
---
|
|
|
|
## Test Plan
|
|
|
|
### Manual Testing
|
|
|
|
1. **Search Flow**
|
|
- Login with Azure AD
|
|
- Navigate to /search
|
|
- Enter search term
|
|
- Verify results from own tenant only
|
|
|
|
2. **Ingestion API**
|
|
- POST without secret → 401
|
|
- POST with wrong secret → 401
|
|
- POST with correct secret → 200
|
|
- Verify data appears in search
|
|
|
|
3. **Tenant Isolation**
|
|
- Ingest data for tenant A
|
|
- Login as tenant B user
|
|
- Search should return empty
|
|
|
|
---
|
|
|
|
## Environment Variables Required
|
|
|
|
| Variable | Purpose | Required In |
|
|
|----------|---------|-------------|
|
|
| `POLICY_API_SECRET` | n8n ingestion auth | Production, Development |
|
|
|
|
---
|
|
|
|
## Rollback Plan
|
|
|
|
If issues arise:
|
|
1. Revert branch to previous commit
|
|
2. Run `npm run db:push` to restore schema
|
|
3. No data migration needed (new table)
|