feat(sdk): Multi-Projekt-Architektur — mehrere Projekte pro Tenant
Some checks failed
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) Failing after 33s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 19s
Some checks failed
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) Failing after 33s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 19s
Jeder Tenant kann jetzt mehrere Compliance-Projekte anlegen (z.B. verschiedene Produkte, Tochterunternehmen). CompanyProfile ist pro Projekt kopierbar und danach unabhaengig editierbar. Multi-Tab-Support via separater BroadcastChannel und localStorage Keys pro Projekt. - Migration 039: compliance_projects Tabelle, sdk_states.project_id - Backend: FastAPI CRUD-Routes fuer Projekte mit Tenant-Isolation - Frontend: ProjectSelector UI, SDKProvider mit projectId, URL ?project= - State API: UPSERT auf (tenant_id, project_id) mit Abwaertskompatibilitaet - Tests: pytest fuer Model-Validierung, Row-Konvertierung, Tenant-Isolation - Docs: MKDocs Seite, CLAUDE.md, Backend README Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
* retry logic, and optimistic locking support.
|
||||
*/
|
||||
|
||||
import { SDKState, CheckpointStatus } from './types'
|
||||
import { SDKState, CheckpointStatus, ProjectInfo } from './types'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
@@ -73,16 +73,19 @@ const RETRY_DELAYS = [1000, 2000, 4000] // Exponential backoff
|
||||
export class SDKApiClient {
|
||||
private baseUrl: string
|
||||
private tenantId: string
|
||||
private projectId: string | undefined
|
||||
private timeout: number
|
||||
private abortControllers: Map<string, AbortController> = new Map()
|
||||
|
||||
constructor(options: {
|
||||
baseUrl?: string
|
||||
tenantId: string
|
||||
projectId?: string
|
||||
timeout?: number
|
||||
}) {
|
||||
this.baseUrl = options.baseUrl || DEFAULT_BASE_URL
|
||||
this.tenantId = options.tenantId
|
||||
this.projectId = options.projectId
|
||||
this.timeout = options.timeout || DEFAULT_TIMEOUT
|
||||
}
|
||||
|
||||
@@ -188,8 +191,10 @@ export class SDKApiClient {
|
||||
*/
|
||||
async getState(): Promise<StateResponse | null> {
|
||||
try {
|
||||
const params = new URLSearchParams({ tenantId: this.tenantId })
|
||||
if (this.projectId) params.set('projectId', this.projectId)
|
||||
const response = await this.fetchWithRetry<APIResponse<StateResponse>>(
|
||||
`${this.baseUrl}/state?tenantId=${encodeURIComponent(this.tenantId)}`,
|
||||
`${this.baseUrl}/state?${params.toString()}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@@ -228,6 +233,7 @@ export class SDKApiClient {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tenantId: this.tenantId,
|
||||
projectId: this.projectId,
|
||||
state,
|
||||
version,
|
||||
}),
|
||||
@@ -245,8 +251,10 @@ export class SDKApiClient {
|
||||
* Delete SDK state for the current tenant
|
||||
*/
|
||||
async deleteState(): Promise<void> {
|
||||
const params = new URLSearchParams({ tenantId: this.tenantId })
|
||||
if (this.projectId) params.set('projectId', this.projectId)
|
||||
await this.fetchWithRetry<APIResponse<void>>(
|
||||
`${this.baseUrl}/state?tenantId=${encodeURIComponent(this.tenantId)}`,
|
||||
`${this.baseUrl}/state?${params.toString()}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
@@ -571,6 +579,107 @@ export class SDKApiClient {
|
||||
return this.tenantId
|
||||
}
|
||||
|
||||
/**
|
||||
* Set project ID for multi-project support
|
||||
*/
|
||||
setProjectId(projectId: string | undefined): void {
|
||||
this.projectId = projectId
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current project ID
|
||||
*/
|
||||
getProjectId(): string | undefined {
|
||||
return this.projectId
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Methods - Project Management
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* List all projects for the current tenant
|
||||
*/
|
||||
async listProjects(): Promise<{ projects: ProjectInfo[]; total: number }> {
|
||||
const response = await this.fetchWithRetry<{ projects: ProjectInfo[]; total: number }>(
|
||||
`${this.baseUrl}/projects?tenant_id=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new project
|
||||
*/
|
||||
async createProject(data: {
|
||||
name: string
|
||||
description?: string
|
||||
customer_type?: string
|
||||
copy_from_project_id?: string
|
||||
}): Promise<ProjectInfo> {
|
||||
const response = await this.fetchWithRetry<ProjectInfo>(
|
||||
`${this.baseUrl}/projects?tenant_id=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...data,
|
||||
tenant_id: this.tenantId,
|
||||
}),
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing project
|
||||
*/
|
||||
async updateProject(projectId: string, data: {
|
||||
name?: string
|
||||
description?: string
|
||||
}): Promise<ProjectInfo> {
|
||||
const response = await this.fetchWithRetry<ProjectInfo>(
|
||||
`${this.baseUrl}/projects/${projectId}?tenant_id=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...data,
|
||||
tenant_id: this.tenantId,
|
||||
}),
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive (soft-delete) a project
|
||||
*/
|
||||
async archiveProject(projectId: string): Promise<void> {
|
||||
await this.fetchWithRetry<{ success: boolean }>(
|
||||
`${this.baseUrl}/projects/${projectId}?tenant_id=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check
|
||||
*/
|
||||
@@ -594,19 +703,23 @@ export class SDKApiClient {
|
||||
|
||||
let clientInstance: SDKApiClient | null = null
|
||||
|
||||
export function getSDKApiClient(tenantId?: string): SDKApiClient {
|
||||
export function getSDKApiClient(tenantId?: string, projectId?: string): SDKApiClient {
|
||||
if (!clientInstance && !tenantId) {
|
||||
throw new Error('SDKApiClient not initialized. Provide tenantId on first call.')
|
||||
}
|
||||
|
||||
if (!clientInstance && tenantId) {
|
||||
clientInstance = new SDKApiClient({ tenantId })
|
||||
clientInstance = new SDKApiClient({ tenantId, projectId })
|
||||
}
|
||||
|
||||
if (tenantId && clientInstance && clientInstance.getTenantId() !== tenantId) {
|
||||
clientInstance.setTenantId(tenantId)
|
||||
}
|
||||
|
||||
if (clientInstance) {
|
||||
clientInstance.setProjectId(projectId)
|
||||
}
|
||||
|
||||
return clientInstance!
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user