feat: Vorbereitung-Module auf 100% — Persistenz, Backend-Services, UCCA Frontend
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 37s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 18s
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 37s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 18s
Phase A: PostgreSQL State Store (sdk_states Tabelle, InMemory-Fallback) Phase B: Modules dynamisch vom Backend, Scope DB-Persistenz, Source Policy State Phase C: UCCA Frontend (3 Seiten, Wizard, RiskScoreGauge), Obligations Live-Daten Phase D: Document Import (PDF/LLM/Gap-Analyse), System Screening (SBOM/OSV.dev) Phase E: Company Profile CRUD mit Audit-Logging Phase F: Tests (Python + TypeScript), flow-data.ts DB-Tabellen aktualisiert Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { Pool } from 'pg'
|
||||
|
||||
/**
|
||||
* SDK State Management API
|
||||
@@ -11,7 +12,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
* - Versioning for optimistic locking
|
||||
* - Last-Modified headers
|
||||
* - ETag support for caching
|
||||
* - Prepared for PostgreSQL migration
|
||||
* - PostgreSQL persistence (with InMemory fallback)
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
@@ -27,27 +28,9 @@ interface StoredState {
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STORAGE LAYER (Abstract - Easy to swap to PostgreSQL)
|
||||
// STORAGE LAYER
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* In-memory storage for development
|
||||
* TODO: Replace with PostgreSQL implementation
|
||||
*
|
||||
* PostgreSQL Schema:
|
||||
* CREATE TABLE sdk_states (
|
||||
* id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
* tenant_id VARCHAR(255) NOT NULL UNIQUE,
|
||||
* user_id VARCHAR(255),
|
||||
* state JSONB NOT NULL,
|
||||
* version INTEGER DEFAULT 1,
|
||||
* created_at TIMESTAMP DEFAULT NOW(),
|
||||
* updated_at TIMESTAMP DEFAULT NOW()
|
||||
* );
|
||||
*
|
||||
* CREATE INDEX idx_sdk_states_tenant ON sdk_states(tenant_id);
|
||||
*/
|
||||
|
||||
interface StateStore {
|
||||
get(tenantId: string): Promise<StoredState | null>
|
||||
save(tenantId: string, state: unknown, userId?: string, expectedVersion?: number): Promise<StoredState>
|
||||
@@ -69,7 +52,6 @@ class InMemoryStateStore implements StateStore {
|
||||
): Promise<StoredState> {
|
||||
const existing = this.store.get(tenantId)
|
||||
|
||||
// Optimistic locking check
|
||||
if (expectedVersion !== undefined && existing && existing.version !== expectedVersion) {
|
||||
const error = new Error('Version conflict') as Error & { status: number }
|
||||
error.status = 409
|
||||
@@ -99,68 +81,94 @@ class InMemoryStateStore implements StateStore {
|
||||
}
|
||||
}
|
||||
|
||||
// Future PostgreSQL implementation would look like:
|
||||
// class PostgreSQLStateStore implements StateStore {
|
||||
// private db: Pool
|
||||
//
|
||||
// constructor(connectionString: string) {
|
||||
// this.db = new Pool({ connectionString })
|
||||
// }
|
||||
//
|
||||
// async get(tenantId: string): Promise<StoredState | null> {
|
||||
// const result = await this.db.query(
|
||||
// 'SELECT state, version, user_id, created_at, updated_at FROM sdk_states WHERE tenant_id = $1',
|
||||
// [tenantId]
|
||||
// )
|
||||
// if (result.rows.length === 0) return null
|
||||
// const row = result.rows[0]
|
||||
// return {
|
||||
// state: row.state,
|
||||
// version: row.version,
|
||||
// userId: row.user_id,
|
||||
// createdAt: row.created_at,
|
||||
// updatedAt: row.updated_at,
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// async save(tenantId: string, state: unknown, userId?: string, expectedVersion?: number): Promise<StoredState> {
|
||||
// // Use UPSERT with version check
|
||||
// const result = await this.db.query(`
|
||||
// INSERT INTO sdk_states (tenant_id, user_id, state, version)
|
||||
// VALUES ($1, $2, $3, 1)
|
||||
// ON CONFLICT (tenant_id) DO UPDATE SET
|
||||
// state = $3,
|
||||
// user_id = COALESCE($2, sdk_states.user_id),
|
||||
// version = sdk_states.version + 1,
|
||||
// updated_at = NOW()
|
||||
// WHERE ($4::int IS NULL OR sdk_states.version = $4)
|
||||
// RETURNING version, created_at, updated_at
|
||||
// `, [tenantId, userId, JSON.stringify(state), expectedVersion])
|
||||
//
|
||||
// if (result.rows.length === 0) {
|
||||
// throw new Error('Version conflict')
|
||||
// }
|
||||
//
|
||||
// return {
|
||||
// state,
|
||||
// version: result.rows[0].version,
|
||||
// userId,
|
||||
// createdAt: result.rows[0].created_at,
|
||||
// updatedAt: result.rows[0].updated_at,
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// async delete(tenantId: string): Promise<boolean> {
|
||||
// const result = await this.db.query(
|
||||
// 'DELETE FROM sdk_states WHERE tenant_id = $1',
|
||||
// [tenantId]
|
||||
// )
|
||||
// return result.rowCount > 0
|
||||
// }
|
||||
// }
|
||||
class PostgreSQLStateStore implements StateStore {
|
||||
private pool: Pool
|
||||
|
||||
// Use in-memory store for now
|
||||
const stateStore: StateStore = new InMemoryStateStore()
|
||||
constructor(connectionString: string) {
|
||||
this.pool = new Pool({
|
||||
connectionString,
|
||||
max: 5,
|
||||
// Set search_path for compliance schema
|
||||
options: '-c search_path=compliance,core,public',
|
||||
})
|
||||
}
|
||||
|
||||
async get(tenantId: string): Promise<StoredState | null> {
|
||||
const result = await this.pool.query(
|
||||
'SELECT state, version, user_id, created_at, updated_at FROM sdk_states WHERE tenant_id = $1',
|
||||
[tenantId]
|
||||
)
|
||||
if (result.rows.length === 0) return null
|
||||
const row = result.rows[0]
|
||||
return {
|
||||
state: row.state,
|
||||
version: row.version,
|
||||
userId: row.user_id,
|
||||
createdAt: row.created_at instanceof Date ? row.created_at.toISOString() : row.created_at,
|
||||
updatedAt: row.updated_at instanceof Date ? row.updated_at.toISOString() : row.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
async save(tenantId: string, state: unknown, userId?: string, expectedVersion?: number): Promise<StoredState> {
|
||||
const now = new Date().toISOString()
|
||||
const stateWithTimestamp = {
|
||||
...(state as object),
|
||||
lastModified: now,
|
||||
}
|
||||
|
||||
// Use UPSERT with version check
|
||||
const result = await this.pool.query(`
|
||||
INSERT INTO sdk_states (tenant_id, user_id, state, version, created_at, updated_at)
|
||||
VALUES ($1, $2, $3::jsonb, 1, NOW(), NOW())
|
||||
ON CONFLICT (tenant_id) DO UPDATE SET
|
||||
state = $3::jsonb,
|
||||
user_id = COALESCE($2, sdk_states.user_id),
|
||||
version = sdk_states.version + 1,
|
||||
updated_at = NOW()
|
||||
WHERE ($4::int IS NULL OR sdk_states.version = $4)
|
||||
RETURNING version, user_id, created_at, updated_at
|
||||
`, [tenantId, userId, JSON.stringify(stateWithTimestamp), expectedVersion ?? null])
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
const error = new Error('Version conflict') as Error & { status: number }
|
||||
error.status = 409
|
||||
throw error
|
||||
}
|
||||
|
||||
const row = result.rows[0]
|
||||
return {
|
||||
state: stateWithTimestamp,
|
||||
version: row.version,
|
||||
userId: row.user_id,
|
||||
createdAt: row.created_at instanceof Date ? row.created_at.toISOString() : row.created_at,
|
||||
updatedAt: row.updated_at instanceof Date ? row.updated_at.toISOString() : row.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
async delete(tenantId: string): Promise<boolean> {
|
||||
const result = await this.pool.query(
|
||||
'DELETE FROM sdk_states WHERE tenant_id = $1',
|
||||
[tenantId]
|
||||
)
|
||||
return (result.rowCount ?? 0) > 0
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STORE INITIALIZATION
|
||||
// =============================================================================
|
||||
|
||||
function createStateStore(): StateStore {
|
||||
const databaseUrl = process.env.DATABASE_URL
|
||||
if (databaseUrl) {
|
||||
console.log('[SDK State] Using PostgreSQL state store')
|
||||
return new PostgreSQLStateStore(databaseUrl)
|
||||
}
|
||||
console.log('[SDK State] Using in-memory state store (no DATABASE_URL)')
|
||||
return new InMemoryStateStore()
|
||||
}
|
||||
|
||||
const stateStore: StateStore = createStateStore()
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
|
||||
Reference in New Issue
Block a user