- Remove unused variables in ApplicationInfo, ArchitectureDebugPage - Fix type errors in Dashboard, GovernanceAnalysis, GovernanceModelHelper (PageHeader description prop) - Add null checks and explicit types in DataValidationDashboard - Fix ObjectDetailModal type errors for _jiraCreatedAt and Date constructor - Remove unused imports and variables in SchemaConfigurationSettings - Update PageHeader to accept string | ReactNode for description prop
877 lines
37 KiB
TypeScript
877 lines
37 KiB
TypeScript
/**
|
|
* 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 [, 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<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');
|
|
}
|
|
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>
|
|
);
|
|
}
|