UI styling improvements: dashboard headers and navigation
- Restore blue PageHeader on Dashboard (/app-components) - Update homepage (/) with subtle header design without blue bar - Add uniform PageHeader styling to application edit page - Fix Rapporten link on homepage to point to /reports overview - Improve header descriptions spacing for better readability
This commit is contained in:
876
frontend/src/components/ArchitectureDebugPage.tsx
Normal file
876
frontend/src/components/ArchitectureDebugPage.tsx
Normal file
@@ -0,0 +1,876 @@
|
||||
/**
|
||||
* 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<Record<string, TestResult>>({});
|
||||
const [loading, setLoading] = useState<Record<string, boolean>>({});
|
||||
const [enabledTypes, setEnabledTypes] = useState<string[]>([]);
|
||||
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<TestResult>) => {
|
||||
setTestResults(prev => ({
|
||||
...prev,
|
||||
[testId]: {
|
||||
...prev[testId],
|
||||
...updates,
|
||||
timestamp: new Date().toISOString(),
|
||||
} as TestResult,
|
||||
}));
|
||||
};
|
||||
|
||||
const executeSqlQuery = async (sql: string, params: any[] = []): Promise<any> => {
|
||||
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 [key, 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');
|
||||
const syncResult = 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<string, any> = {};
|
||||
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');
|
||||
}
|
||||
const updateResult = 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 (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<PageHeader
|
||||
title="Architecture Debug & Test Page"
|
||||
description="Test the refactored V2 API architecture. All tests require admin permissions."
|
||||
icon={
|
||||
<svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Input Form */}
|
||||
<div className="bg-white shadow rounded-lg p-6 mb-8">
|
||||
<h2 className="text-xl font-semibold mb-4">Test Parameters</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Object Type Name
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={inputs.typeName}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<datalist id="enabled-types-list">
|
||||
{enabledTypes.map(type => (
|
||||
<option key={type} value={type} />
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
{!loadingEnabledTypes && enabledTypes.length > 0 && (
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Enabled types: {enabledTypes.join(', ')}
|
||||
{!enabledTypes.includes(inputs.typeName) && inputs.typeName && (
|
||||
<span className="ml-2 text-orange-600">
|
||||
⚠ "{inputs.typeName}" is not enabled
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{!loadingEnabledTypes && enabledTypes.length === 0 && (
|
||||
<p className="mt-1 text-xs text-orange-600">
|
||||
⚠ No object types are currently enabled. Please enable at least one in Schema Configuration.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Object Key (for Tests C, D, E)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={inputs.objectKey}
|
||||
onChange={(e) => setInputs({ ...inputs, objectKey: e.target.value })}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2"
|
||||
placeholder="APP-123"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Referenced Object Key (for Tests A, C)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={inputs.referencedObjectKey}
|
||||
onChange={(e) => setInputs({ ...inputs, referencedObjectKey: e.target.value })}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2"
|
||||
placeholder="HOST-123"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Disabled Type Name (for Test B)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={inputs.disabledTypeName}
|
||||
onChange={(e) => setInputs({ ...inputs, disabledTypeName: e.target.value })}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2"
|
||||
placeholder="HostingType"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Update Field (for Test E)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={inputs.updateField}
|
||||
onChange={(e) => setInputs({ ...inputs, updateField: e.target.value })}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2"
|
||||
placeholder="description"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Update Value (for Test E)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={inputs.updateValue}
|
||||
onChange={(e) => setInputs({ ...inputs, updateValue: e.target.value })}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2"
|
||||
placeholder="Updated value"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Utility Actions */}
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-8">
|
||||
<h2 className="text-lg font-semibold mb-2 text-yellow-900">Utility Actions</h2>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
const response = await fetch('/api/v2/debug/fix-missing-type-names', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
alert(`Fixed ${result.fixed} object types with missing type_name. Errors: ${result.errors || 0}`);
|
||||
// Reload enabled types
|
||||
const config = await getConfiguredObjectTypes();
|
||||
const enabled = config.schemas.flatMap(s =>
|
||||
s.objectTypes.filter(ot => ot.enabled).map(ot => ot.objectTypeName)
|
||||
);
|
||||
setEnabledTypes(enabled);
|
||||
} else {
|
||||
alert(`Error: ${result.error || 'Failed to fix'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}}
|
||||
className="bg-yellow-600 text-white px-4 py-2 rounded-md hover:bg-yellow-700"
|
||||
>
|
||||
Fix Missing type_name
|
||||
</button>
|
||||
<button
|
||||
onClick={async () => {
|
||||
const testId = 'debug-all-types';
|
||||
setLoading(prev => ({ ...prev, [testId]: true }));
|
||||
updateTestResult(testId, { test: 'Debug: All Object Types', status: 'running', message: 'Fetching all object types...' });
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v2/debug/all-object-types', {
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// Check if it's a 404 - likely means V2 API is not enabled
|
||||
if (response.status === 404) {
|
||||
throw new Error('V2 API routes are not enabled. Please set USE_V2_API=true in backend environment variables and restart the server.');
|
||||
}
|
||||
|
||||
const errorData = await response.json().catch(() => ({ error: `HTTP ${response.status}: Failed to fetch` }));
|
||||
const errorMessage = errorData.error || errorData.details || `HTTP ${response.status}: Failed to fetch`;
|
||||
|
||||
// Provide more context for common errors
|
||||
if (response.status === 403) {
|
||||
throw new Error(`${errorMessage} (Admin permission required)`);
|
||||
}
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Format the data for display
|
||||
const enabledWithNullTypeName = result.allTypes.filter((t: any) => t.enabled && !t.hasTypeName);
|
||||
const enabledWithTypeName = result.allTypes.filter((t: any) => t.enabled && t.hasTypeName);
|
||||
|
||||
updateTestResult(testId, {
|
||||
test: 'Debug: All Object Types',
|
||||
status: 'success',
|
||||
message: `Found ${result.summary.total} total types. ${result.summary.enabled} enabled in DB (${result.summary.enabledWithTypeName} with type_name, ${enabledWithNullTypeName.length} missing type_name). ${result.summary.missingTypeName} total missing type_name.`,
|
||||
data: {
|
||||
summary: result.summary,
|
||||
enabledWithNullTypeName: enabledWithNullTypeName.map((t: any) => ({
|
||||
id: t.id,
|
||||
displayName: t.displayName,
|
||||
enabled: t.enabled,
|
||||
})),
|
||||
enabledWithTypeName: enabledWithTypeName.map((t: any) => ({
|
||||
id: t.id,
|
||||
typeName: t.typeName,
|
||||
displayName: t.displayName,
|
||||
})),
|
||||
allTypes: result.allTypes,
|
||||
enabledTypesFromService: result.enabledTypes,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
updateTestResult(testId, {
|
||||
test: 'Debug: All Object Types',
|
||||
status: 'error',
|
||||
message: error instanceof Error ? error.message : 'Failed to fetch',
|
||||
});
|
||||
} finally {
|
||||
setLoading(prev => ({ ...prev, [testId]: false }));
|
||||
}
|
||||
}}
|
||||
disabled={loading.debugAllTypes}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:bg-gray-400"
|
||||
>
|
||||
{loading.debugAllTypes ? 'Loading...' : 'Show All Object Types (Debug)'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Test Buttons */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
|
||||
<button
|
||||
onClick={runTestA}
|
||||
disabled={loading.testA}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:bg-gray-400"
|
||||
>
|
||||
{loading.testA ? 'Running...' : 'Test A: Level2 Recursion'}
|
||||
</button>
|
||||
<button
|
||||
onClick={runTestB}
|
||||
disabled={loading.testB}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:bg-gray-400"
|
||||
>
|
||||
{loading.testB ? 'Running...' : 'Test B: Disabled Type'}
|
||||
</button>
|
||||
<button
|
||||
onClick={runTestC}
|
||||
disabled={loading.testC}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:bg-gray-400"
|
||||
>
|
||||
{loading.testC ? 'Running...' : 'Test C: Attribute Wipe Rule'}
|
||||
</button>
|
||||
<button
|
||||
onClick={runTestD}
|
||||
disabled={loading.testD}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:bg-gray-400"
|
||||
>
|
||||
{loading.testD ? 'Running...' : 'Test D: QueryService Reconstruction'}
|
||||
</button>
|
||||
<button
|
||||
onClick={runTestE}
|
||||
disabled={loading.testE}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:bg-gray-400"
|
||||
>
|
||||
{loading.testE ? 'Running...' : 'Test E: Write-Through'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Test Results */}
|
||||
<div className="space-y-6">
|
||||
{Object.entries(testResults).map(([testId, result]) => (
|
||||
<div key={testId} className="bg-white shadow rounded-lg p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold">{result.test}</h3>
|
||||
<span className={`px-3 py-1 rounded-full text-sm ${
|
||||
result.status === 'success' ? 'bg-green-100 text-green-800' :
|
||||
result.status === 'error' ? 'bg-red-100 text-red-800' :
|
||||
result.status === 'running' ? 'bg-yellow-100 text-yellow-800' :
|
||||
'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
{result.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{result.message && (
|
||||
<p className="text-gray-700 mb-4">{result.message}</p>
|
||||
)}
|
||||
|
||||
{result.data && (
|
||||
<div className="mb-4">
|
||||
<h4 className="font-medium mb-2">Data:</h4>
|
||||
<pre className="bg-gray-50 p-4 rounded-md overflow-auto text-sm">
|
||||
{JSON.stringify(result.data, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result.sqlQueries && result.sqlQueries.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h4 className="font-medium mb-2">SQL Queries & Results:</h4>
|
||||
{result.sqlQueries.map((query, idx) => (
|
||||
<div key={idx} className="mb-4 border-l-4 border-blue-500 pl-4">
|
||||
<div className="font-mono text-sm bg-gray-50 p-2 rounded mb-2">
|
||||
{query.sql}
|
||||
</div>
|
||||
<pre className="bg-gray-50 p-4 rounded-md overflow-auto text-xs max-h-64">
|
||||
{JSON.stringify(query.result, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result.timestamp && (
|
||||
<p className="text-xs text-gray-500">Ran at: {new Date(result.timestamp).toLocaleString()}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{Object.keys(testResults).length === 0 && (
|
||||
<div className="bg-gray-50 rounded-lg p-8 text-center text-gray-500">
|
||||
No tests run yet. Use the buttons above to start testing.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user