/** * ArchitectureDebugPage - Debug page for testing refactored architecture * * Tests: * A - Level2 recursion: referencedObject with attributes is stored * B - Disabled type: NOT full sync, but reference-only caching works * C - Attribute wipe rule: shallow referencedObject doesn't delete existing values * D - QueryService reconstruction: DB → TS object is correct * E - Write-through: update to Jira + immediate DB update */ import { useState, useEffect } from 'react'; import { getConfiguredObjectTypes } from '../services/api'; import PageHeader from './PageHeader'; interface TestResult { test: string; status: 'pending' | 'running' | 'success' | 'error'; message?: string; data?: any; sqlQueries?: Array<{ sql: string; result: any }>; timestamp?: string; } export default function ArchitectureDebugPage() { const [testResults, setTestResults] = useState>({}); const [loading, setLoading] = useState>({}); const [enabledTypes, setEnabledTypes] = useState([]); const [loadingEnabledTypes, setLoadingEnabledTypes] = useState(true); const [inputs, setInputs] = useState({ typeName: 'ApplicationComponent', objectKey: '', referencedObjectKey: '', disabledTypeName: 'HostingType', updateField: 'description', updateValue: 'Updated via Debug Page', }); // Load enabled object types on mount useEffect(() => { const loadEnabledTypes = async () => { try { setLoadingEnabledTypes(true); const config = await getConfiguredObjectTypes(); const enabled = config.schemas.flatMap(s => s.objectTypes.filter(ot => ot.enabled).map(ot => ot.objectTypeName) ); setEnabledTypes(enabled); } catch (error) { console.error('Failed to load enabled types:', error); } finally { setLoadingEnabledTypes(false); } }; loadEnabledTypes(); }, []); const updateTestResult = (testId: string, updates: Partial) => { setTestResults(prev => ({ ...prev, [testId]: { ...prev[testId], ...updates, timestamp: new Date().toISOString(), } as TestResult, })); }; const executeSqlQuery = async (sql: string, params: any[] = []): Promise => { try { const response = await fetch('/api/v2/debug/query', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ sql, params }), }); if (!response.ok) { const errorData = await response.json().catch(() => ({ error: 'Unknown error' })); throw new Error(errorData.error || `HTTP ${response.status}`); } const data = await response.json(); return data.result || []; } catch (error) { console.error('SQL query failed:', error); throw error; } }; // Test A: Level2 recursion const runTestA = async () => { const testId = 'testA'; setLoading(prev => ({ ...prev, [testId]: true })); updateTestResult(testId, { test: 'Test A: Level2 Recursion', status: 'running' }); try { // Step 1: Sync object type updateTestResult(testId, { message: `Syncing object type "${inputs.typeName}"...` }); const syncResponse = await fetch(`/api/v2/sync/objects/${inputs.typeName}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', }); if (!syncResponse.ok) { const errorData = await syncResponse.json().catch(() => ({ error: `HTTP ${syncResponse.status}: Sync failed` })); const errorMessage = errorData.error || `HTTP ${syncResponse.status}: Sync failed`; // Extract enabled types from error details if available let enabledTypesHint = ''; if (errorData.details?.enabledTypeNames && errorData.details.enabledTypeNames.length > 0) { enabledTypesHint = ` Currently enabled: ${errorData.details.enabledTypeNames.join(', ')}.`; } // Provide helpful hints for common errors let hint = ''; if (errorMessage.includes('not enabled')) { hint = ` Tip: Go to Schema Configuration settings and enable "${inputs.typeName}".${enabledTypesHint}`; } else if (errorMessage.includes('token')) { hint = ' Tip: Check if JIRA_SERVICE_ACCOUNT_TOKEN is configured in .env or if you have a Personal Access Token configured.'; } else if (errorMessage.includes('404') || errorMessage.includes('not found')) { hint = ' Tip: The object type may not exist in your Jira Assets instance. Check the type name spelling.'; } throw new Error(errorMessage + hint); } const syncResult = await syncResponse.json(); // Check for errors in sync result if (syncResult.hasErrors && syncResult.errors && syncResult.errors.length > 0) { const errorMessages = syncResult.errors.map((e: any) => `${e.objectId}: ${e.error}`).join('; '); console.warn(`Sync completed with errors: ${errorMessages}`); } // Check if sync had errors (even if not marked as hasErrors) if (syncResult.errors && syncResult.errors.length > 0) { const errorMessages = syncResult.errors.map((e: any) => e.error || e.message || 'Unknown error').join('; '); // Check if it's a network/Jira API error if (errorMessages.includes('Failed to fetch') || errorMessages.includes('fetch failed') || errorMessages.includes('network')) { throw new Error(`Network error while syncing: ${errorMessages}. This usually means: - Jira host URL is incorrect or unreachable - Network connectivity issues - Jira API authentication failed. Check your Jira PAT token in Profile Settings and verify the JIRA_HOST environment variable.`); } throw new Error(`Sync failed: ${errorMessages}. Check backend logs for details.`); } if (syncResult.objectsCached === 0 && syncResult.objectsProcessed === 0) { throw new Error('Sync completed but no objects were processed. This could mean:\n- The object type has no objects in Jira Assets\n- The IQL search returned no results\n- Check if the object type name is correct'); } // Step 2: Get a sample object to check updateTestResult(testId, { message: 'Fetching sample objects...' }); const objectsResponse = await fetch(`/api/v2/objects/${inputs.typeName}?limit=10`, { credentials: 'include', }); if (!objectsResponse.ok) { const errorData = await objectsResponse.json().catch(() => ({ error: `HTTP ${objectsResponse.status}: Failed to fetch objects` })); const errorMessage = errorData.error || `HTTP ${objectsResponse.status}: Failed to fetch objects`; throw new Error(`${errorMessage}. Sync processed ${syncResult.objectsProcessed || 0} objects. If objects were synced, they may not be queryable yet.`); } const objectsData = await objectsResponse.json(); const sampleObject = objectsData.objects?.[0]; if (!sampleObject) { throw new Error(`No objects found after sync. Sync processed ${syncResult.objectsProcessed || 0} objects, cached ${syncResult.objectsCached || 0}. The objects may not be queryable yet or the object type has no objects in Jira Assets.`); } // Step 3: Find a referenced object from the sample updateTestResult(testId, { message: 'Looking for referenced objects in sample...' }); const referencedObjectKey = inputs.referencedObjectKey || (() => { // Try to find a reference in the object const references: string[] = []; for (const [, value] of Object.entries(sampleObject)) { if (value && typeof value === 'object' && ('objectKey' in value || 'key' in value)) { const objKey = (value as any).objectKey || (value as any).key; if (objKey) references.push(objKey); } if (Array.isArray(value) && value.length > 0) { for (const item of value) { if (item && typeof item === 'object' && ('objectKey' in item || 'key' in item)) { const objKey = item.objectKey || item.key; if (objKey) references.push(objKey); } } } } return references.length > 0 ? references[0] : null; })(); if (!referencedObjectKey) { const sampleObjectKeys = Object.keys(sampleObject).join(', '); throw new Error(`No referenced object found in sample object. Object fields: ${sampleObjectKeys}. Please provide a referencedObjectKey manually if the object has references.`); } updateTestResult(testId, { message: `Found referenced object: ${referencedObjectKey}` }); // Step 4: Run SQL checks updateTestResult(testId, { message: 'Running database checks...' }); const sqlQueries: Array<{ sql: string; result: any }> = []; // Check 1: Both objects exist const sampleObjectKey = sampleObject.objectKey || sampleObject.key || sampleObject.id; if (!sampleObjectKey) { throw new Error(`Sample object has no objectKey. Object structure: ${JSON.stringify(sampleObject)}`); } const check1Sql = `SELECT id, object_key, object_type_name, label FROM objects WHERE object_key IN (?, ?)`; const check1Result = await executeSqlQuery(check1Sql, [sampleObjectKey, referencedObjectKey]); sqlQueries.push({ sql: check1Sql, result: check1Result }); // Check 2: Referenced object has attribute values // Handle both snake_case and camelCase column names const refObj = check1Result.find((o: any) => (o.object_key || o.objectKey) === referencedObjectKey ); if (!refObj) { const foundKeys = check1Result.map((o: any) => (o.object_key || o.objectKey)).filter(Boolean); throw new Error(`Referenced object "${referencedObjectKey}" not found in database. Found objects: ${foundKeys.join(', ') || 'none'}. The referenced object may not have been cached during sync.`); } const check2Sql = `SELECT COUNT(*) as count FROM attribute_values WHERE object_id = ?`; const check2Result = await executeSqlQuery(check2Sql, [refObj.id]); sqlQueries.push({ sql: check2Sql, result: check2Result }); const attrCount = check2Result[0]?.count || (check2Result[0]?.count === 0 ? 0 : null); // Check 3: Get sample attribute values to verify they exist const check3Sql = `SELECT av.*, a.field_name, a.attr_type FROM attribute_values av JOIN attributes a ON a.id = av.attribute_id WHERE av.object_id = ? LIMIT 5`; const check3Result = await executeSqlQuery(check3Sql, [refObj.id]); sqlQueries.push({ sql: check3Sql, result: check3Result }); updateTestResult(testId, { status: 'success', message: `✅ Sync completed successfully. Found ${syncResult.objectsCached || 0} objects. Referenced object "${referencedObjectKey}" exists with ${attrCount || 0} attribute values.`, data: { syncResult: { objectsProcessed: syncResult.objectsProcessed, objectsCached: syncResult.objectsCached, relationsExtracted: syncResult.relationsExtracted, }, sampleObject: { objectKey: sampleObjectKey, label: sampleObject.label || sampleObject.name }, referencedObject: { objectKey: referencedObjectKey, id: refObj.id, typeName: refObj.object_type_name || refObj.objectTypeName, label: refObj.label, attributeValueCount: attrCount, }, }, sqlQueries, }); } catch (error) { updateTestResult(testId, { status: 'error', message: error instanceof Error ? error.message : 'Test failed', }); } finally { setLoading(prev => ({ ...prev, [testId]: false })); } }; // Test B: Disabled type reference-only caching const runTestB = async () => { const testId = 'testB'; setLoading(prev => ({ ...prev, [testId]: true })); updateTestResult(testId, { test: 'Test B: Disabled Type Reference-Only', status: 'running' }); try { const disabledType = inputs.disabledTypeName; // Step 1: Check initial count updateTestResult(testId, { message: 'Checking initial object count...' }); const initialCountResult = await executeSqlQuery( `SELECT COUNT(*) as count FROM objects WHERE object_type_name = ?`, [disabledType] ); const initialCount = initialCountResult[0]?.count || 0; // Step 2: Sync an enabled type that references the disabled type updateTestResult(testId, { message: `Syncing ${inputs.typeName} (should cache ${disabledType} references)...` }); const syncResponse = await fetch(`/api/v2/sync/objects/${inputs.typeName}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', }); if (!syncResponse.ok) throw new Error('Sync failed'); await syncResponse.json(); // Step 3: Check final count (should not have increased much) updateTestResult(testId, { message: 'Checking final object count...' }); const finalCountResult = await executeSqlQuery( `SELECT COUNT(*) as count FROM objects WHERE object_type_name = ?`, [disabledType] ); const finalCount = finalCountResult[0]?.count || 0; // Step 4: Check relations const relationsResult = await executeSqlQuery( `SELECT COUNT(*) as count FROM object_relations r JOIN objects t ON t.id = r.target_id WHERE t.object_type_name = ?`, [disabledType] ); const relationCount = relationsResult[0]?.count || 0; const sqlQueries = [ { sql: 'Initial count query', result: initialCountResult }, { sql: 'Final count query', result: finalCountResult }, { sql: 'Relations query', result: relationsResult }, ]; updateTestResult(testId, { status: 'success', message: `Disabled type ${disabledType}: ${initialCount} → ${finalCount} objects. ${relationCount} relations found.`, data: { initialCount, finalCount, relationCount, delta: finalCount - initialCount, }, sqlQueries, }); } catch (error) { updateTestResult(testId, { status: 'error', message: error instanceof Error ? error.message : 'Test failed', }); } finally { setLoading(prev => ({ ...prev, [testId]: false })); } }; // Test C: Attribute wipe rule const runTestC = async () => { const testId = 'testC'; setLoading(prev => ({ ...prev, [testId]: true })); updateTestResult(testId, { test: 'Test C: Attribute Wipe Rule', status: 'running' }); try { if (!inputs.referencedObjectKey) { throw new Error('Please provide a referencedObjectKey to test'); } // Step 1: Get initial attribute count updateTestResult(testId, { message: 'Checking initial attribute values...' }); const objInfoResponse = await fetch(`/api/v2/debug/objects?objectKey=${inputs.referencedObjectKey}`, { credentials: 'include', }); if (!objInfoResponse.ok) throw new Error('Failed to get object info'); const objInfo = await objInfoResponse.json(); const initialAttrCount = objInfo.attributeValueCount || 0; if (initialAttrCount === 0) { throw new Error('Object has no attributes - sync it first with level2 expansion'); } // Step 2: Sync a parent object that references this one (should come as shallow) updateTestResult(testId, { message: 'Syncing parent object (referenced object may come as shallow)...' }); const syncResponse = await fetch(`/api/v2/sync/objects/${inputs.typeName}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', }); if (!syncResponse.ok) throw new Error('Sync failed'); // Step 3: Check final attribute count (should be same or greater) updateTestResult(testId, { message: 'Checking final attribute values...' }); const finalObjInfoResponse = await fetch(`/api/v2/debug/objects?objectKey=${inputs.referencedObjectKey}`, { credentials: 'include', }); if (!finalObjInfoResponse.ok) throw new Error('Failed to get final object info'); const finalObjInfo = await finalObjInfoResponse.json(); const finalAttrCount = finalObjInfo.attributeValueCount || 0; const sqlQueries = [ { sql: 'Initial attribute count', result: [{ count: initialAttrCount }] }, { sql: 'Final attribute count', result: [{ count: finalAttrCount }] }, ]; const success = finalAttrCount >= initialAttrCount; updateTestResult(testId, { status: success ? 'success' : 'error', message: success ? `✅ Attributes preserved: ${initialAttrCount} → ${finalAttrCount}` : `❌ Attributes were wiped: ${initialAttrCount} → ${finalAttrCount}`, data: { initialAttrCount, finalAttrCount, preserved: success, }, sqlQueries, }); } catch (error) { updateTestResult(testId, { status: 'error', message: error instanceof Error ? error.message : 'Test failed', }); } finally { setLoading(prev => ({ ...prev, [testId]: false })); } }; // Test D: QueryService reconstruction const runTestD = async () => { const testId = 'testD'; setLoading(prev => ({ ...prev, [testId]: true })); updateTestResult(testId, { test: 'Test D: QueryService Reconstruction', status: 'running' }); try { if (!inputs.objectKey) { throw new Error('Please provide an objectKey to test'); } // Step 1: Get object from DB via QueryService updateTestResult(testId, { message: 'Fetching object via QueryService...' }); const objectResponse = await fetch(`/api/v2/objects/${inputs.typeName}/${inputs.objectKey}`, { credentials: 'include', }); if (!objectResponse.ok) throw new Error('Failed to fetch object'); const object = await objectResponse.json(); // Step 2: Get raw DB data for comparison updateTestResult(testId, { message: 'Fetching raw DB data...' }); const objInfoResponse = await fetch(`/api/v2/debug/objects?objectKey=${inputs.objectKey}`, { credentials: 'include', }); if (!objInfoResponse.ok) throw new Error('Failed to get object info'); const objInfo = await objInfoResponse.json(); // Step 3: Get attribute values from DB const attrValuesResult = await executeSqlQuery( `SELECT av.*, a.field_name, a.attr_type, a.is_multiple FROM attribute_values av JOIN attributes a ON a.id = av.attribute_id WHERE av.object_id = ? ORDER BY a.field_name, av.array_index`, [objInfo.object.id] ); // Step 4: Verify reconstruction const fieldCount = new Set(attrValuesResult.map((av: any) => av.field_name)).size; const objectFieldCount = Object.keys(object).filter(k => !k.startsWith('_') && k !== 'id' && k !== 'objectKey' && k !== 'label').length; const sqlQueries = [ { sql: 'Attribute values query', result: attrValuesResult }, ]; updateTestResult(testId, { status: 'success', message: `Object reconstructed: ${fieldCount} fields from DB, ${objectFieldCount} fields in object`, data: { object, dbFields: fieldCount, objectFields: objectFieldCount, attributeValues: attrValuesResult.length, }, sqlQueries, }); } catch (error) { updateTestResult(testId, { status: 'error', message: error instanceof Error ? error.message : 'Test failed', }); } finally { setLoading(prev => ({ ...prev, [testId]: false })); } }; // Test E: Write-through const runTestE = async () => { const testId = 'testE'; setLoading(prev => ({ ...prev, [testId]: true })); updateTestResult(testId, { test: 'Test E: Write-Through Update', status: 'running' }); try { if (!inputs.objectKey) { throw new Error('Please provide an objectKey to test'); } // Step 1: Get initial object state updateTestResult(testId, { message: 'Fetching initial object state...' }); const initialResponse = await fetch(`/api/v2/objects/${inputs.typeName}/${inputs.objectKey}`, { credentials: 'include', }); if (!initialResponse.ok) throw new Error('Failed to fetch initial object'); const initialObject = await initialResponse.json(); // Step 2: Update via write-through updateTestResult(testId, { message: 'Updating object via write-through...' }); const updatePayload: Record = {}; updatePayload[inputs.updateField] = inputs.updateValue; const updateResponse = await fetch(`/api/v2/objects/${inputs.typeName}/${inputs.objectKey}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(updatePayload), }); if (!updateResponse.ok) { const error = await updateResponse.json(); throw new Error(error.error || 'Update failed'); } await updateResponse.json(); // Step 3: Immediately check DB (without refresh) updateTestResult(testId, { message: 'Checking DB state immediately...' }); const dbCheckResponse = await fetch(`/api/v2/objects/${inputs.typeName}/${inputs.objectKey}`, { credentials: 'include', }); if (!dbCheckResponse.ok) throw new Error('Failed to fetch updated object'); const dbObject = await dbCheckResponse.json(); // Step 4: Check attribute value in DB const attrCheckResult = await executeSqlQuery( `SELECT av.*, a.field_name FROM attribute_values av JOIN attributes a ON a.id = av.attribute_id JOIN objects o ON o.id = av.object_id WHERE o.object_key = ? AND a.field_name = ?`, [inputs.objectKey, inputs.updateField] ); const sqlQueries = [ { sql: 'Attribute value check', result: attrCheckResult }, ]; const dbValue = dbObject[inputs.updateField]; const updated = dbValue === inputs.updateValue || (typeof dbValue === 'object' && dbValue?.label === inputs.updateValue); updateTestResult(testId, { status: updated ? 'success' : 'error', message: updated ? `✅ Write-through successful: DB updated immediately` : `❌ Write-through failed: DB value doesn't match`, data: { initialValue: initialObject[inputs.updateField], expectedValue: inputs.updateValue, dbValue, updated, }, sqlQueries, }); } catch (error) { updateTestResult(testId, { status: 'error', message: error instanceof Error ? error.message : 'Test failed', }); } finally { setLoading(prev => ({ ...prev, [testId]: false })); } }; return (
} /> {/* Input Form */}

Test Parameters

setInputs({ ...inputs, typeName: e.target.value })} className="flex-1 border border-gray-300 rounded-md px-3 py-2" placeholder="ApplicationComponent" list="enabled-types-list" /> {enabledTypes.map(type => (
{!loadingEnabledTypes && enabledTypes.length > 0 && (

Enabled types: {enabledTypes.join(', ')} {!enabledTypes.includes(inputs.typeName) && inputs.typeName && ( ⚠ "{inputs.typeName}" is not enabled )}

)} {!loadingEnabledTypes && enabledTypes.length === 0 && (

⚠ No object types are currently enabled. Please enable at least one in Schema Configuration.

)}
setInputs({ ...inputs, objectKey: e.target.value })} className="w-full border border-gray-300 rounded-md px-3 py-2" placeholder="APP-123" />
setInputs({ ...inputs, referencedObjectKey: e.target.value })} className="w-full border border-gray-300 rounded-md px-3 py-2" placeholder="HOST-123" />
setInputs({ ...inputs, disabledTypeName: e.target.value })} className="w-full border border-gray-300 rounded-md px-3 py-2" placeholder="HostingType" />
setInputs({ ...inputs, updateField: e.target.value })} className="w-full border border-gray-300 rounded-md px-3 py-2" placeholder="description" />
setInputs({ ...inputs, updateValue: e.target.value })} className="w-full border border-gray-300 rounded-md px-3 py-2" placeholder="Updated value" />
{/* Utility Actions */}

Utility Actions

{/* Test Buttons */}
{/* Test Results */}
{Object.entries(testResults).map(([testId, result]) => (

{result.test}

{result.status}
{result.message && (

{result.message}

)} {result.data && (

Data:

                  {JSON.stringify(result.data, null, 2)}
                
)} {result.sqlQueries && result.sqlQueries.length > 0 && (

SQL Queries & Results:

{result.sqlQueries.map((query, idx) => (
{query.sql}
                      {JSON.stringify(query.result, null, 2)}
                    
))}
)} {result.timestamp && (

Ran at: {new Date(result.timestamp).toLocaleString()}

)}
))}
{Object.keys(testResults).length === 0 && (
No tests run yet. Use the buttons above to start testing.
)}
); }