147 lines
3.7 KiB
Markdown
147 lines
3.7 KiB
Markdown
# Research: Global Policy Search
|
|
|
|
**Feature**: 001-global-policy-search
|
|
**Date**: 2025-12-05
|
|
|
|
## Research Areas
|
|
|
|
### 1. Drizzle ORM Case-Insensitive Search
|
|
|
|
**Decision**: Use `ilike` operator for case-insensitive search in PostgreSQL via Drizzle ORM.
|
|
|
|
**Rationale**:
|
|
- Drizzle ORM supports PostgreSQL's `ilike` operator directly
|
|
- No additional configuration or extensions required
|
|
- Pattern: `ilike(field, '%searchterm%')` for substring matching
|
|
|
|
**Alternatives Considered**:
|
|
- Full-text search with `tsvector`: Overkill for v1, adds complexity
|
|
- `LOWER()` function: Works but `ilike` is more idiomatic for PostgreSQL
|
|
|
|
**Implementation**:
|
|
```typescript
|
|
import { ilike, or } from 'drizzle-orm';
|
|
|
|
const results = await db
|
|
.select()
|
|
.from(policySettings)
|
|
.where(
|
|
or(
|
|
ilike(policySettings.settingName, `%${searchTerm}%`),
|
|
ilike(policySettings.settingValue, `%${searchTerm}%`)
|
|
)
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
### 2. Server Actions for Search
|
|
|
|
**Decision**: Use Next.js Server Actions for search functionality instead of API routes.
|
|
|
|
**Rationale**:
|
|
- Constitution mandates Server-First architecture
|
|
- Server Actions provide type-safe, secure data fetching
|
|
- No client-side fetch code needed
|
|
- Built-in CSRF protection
|
|
|
|
**Pattern**:
|
|
```typescript
|
|
// lib/actions/search.ts
|
|
'use server';
|
|
|
|
export async function searchPolicySettings(formData: FormData) {
|
|
const searchTerm = formData.get('query') as string;
|
|
// ... DB query with tenant filtering
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 3. API Secret Authentication for Ingestion API
|
|
|
|
**Decision**: Use `X-API-SECRET` header validation for the `/api/policy-settings` endpoint.
|
|
|
|
**Rationale**:
|
|
- Simple and effective for server-to-server communication (n8n → API)
|
|
- No OAuth complexity needed for internal service
|
|
- Environment variable based, easy to rotate
|
|
|
|
**Pattern**:
|
|
```typescript
|
|
// app/api/policy-settings/route.ts
|
|
const apiSecret = request.headers.get('X-API-SECRET');
|
|
if (apiSecret !== process.env.POLICY_API_SECRET) {
|
|
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
}
|
|
```
|
|
|
|
**Environment Variable**: `POLICY_API_SECRET`
|
|
|
|
---
|
|
|
|
### 4. Tenant ID from Azure AD Session
|
|
|
|
**Decision**: Extract `tenantId` from NextAuth session/JWT token (Azure AD `tid` claim).
|
|
|
|
**Rationale**:
|
|
- Azure AD includes `tid` (tenant ID) in the ID token
|
|
- Can be accessed via NextAuth callbacks
|
|
- Ensures tenant isolation at authentication layer
|
|
|
|
**Implementation**:
|
|
```typescript
|
|
// lib/auth/utils.ts - extend session callback
|
|
callbacks: {
|
|
jwt: async ({ token, account, profile }) => {
|
|
if (account && profile) {
|
|
token.tenantId = (profile as any).tid;
|
|
}
|
|
return token;
|
|
},
|
|
session: ({ session, token }) => {
|
|
session.user.tenantId = token.tenantId as string;
|
|
return session;
|
|
},
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 5. Upsert Logic for Data Ingestion
|
|
|
|
**Decision**: Use Drizzle's `onConflictDoUpdate` for upsert operations.
|
|
|
|
**Rationale**:
|
|
- Atomic operation, prevents race conditions
|
|
- Single query instead of SELECT + INSERT/UPDATE
|
|
- Uses composite unique constraint on (tenantId, graphPolicyId, settingName)
|
|
|
|
**Pattern**:
|
|
```typescript
|
|
await db
|
|
.insert(policySettings)
|
|
.values(data)
|
|
.onConflictDoUpdate({
|
|
target: [policySettings.tenantId, policySettings.graphPolicyId, policySettings.settingName],
|
|
set: {
|
|
settingValue: data.settingValue,
|
|
lastSyncedAt: new Date(),
|
|
},
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Dependencies Identified
|
|
|
|
| Dependency | Version | Purpose |
|
|
|------------|---------|---------|
|
|
| drizzle-orm | ^0.44.x | Database ORM (already installed) |
|
|
| @shadcn/ui | latest | Input, Table components (already installed) |
|
|
| next-auth | ^4.x | Session with tenantId (already installed) |
|
|
|
|
## No New Dependencies Required
|
|
|
|
All features can be implemented with existing project dependencies.
|