335 lines
14 KiB
TypeScript
335 lines
14 KiB
TypeScript
|
|
/**
|
||
|
|
* Test script for entity-specific sync with Autotask
|
||
|
|
* Run with: npx tsx dev/test-entity-specific-sync.ts
|
||
|
|
*
|
||
|
|
* This script tests the entity-specific sync functionality by:
|
||
|
|
* 1. Syncing individual entities
|
||
|
|
* 2. Syncing multiple selected entities
|
||
|
|
* 3. Verifying dependency ordering
|
||
|
|
* 4. Testing different entity types
|
||
|
|
*/
|
||
|
|
|
||
|
|
import dotenv from 'dotenv';
|
||
|
|
import path from 'path';
|
||
|
|
|
||
|
|
// Load environment variables from .env.local FIRST
|
||
|
|
dotenv.config({ path: path.resolve(__dirname, '../.env.local') });
|
||
|
|
|
||
|
|
import { AutotaskClient } from '../lib/services/autotask-client';
|
||
|
|
import { createSyncService } from '../lib/services/sync-service';
|
||
|
|
import { EntityType, SyncType } from '../lib/types/sync';
|
||
|
|
import postgresClient from '../lib/services/postgres-client';
|
||
|
|
|
||
|
|
async function testEntitySpecificSync() {
|
||
|
|
console.log('🔍 Testing Entity-Specific Sync with Autotask\n');
|
||
|
|
|
||
|
|
// Validate required environment variables
|
||
|
|
if (!process.env.AUTOTASK_API_URL || !process.env.AUTOTASK_USERNAME ||
|
||
|
|
!process.env.AUTOTASK_SECRET || !process.env.AUTOTASK_API_INTEGRATION_CODE) {
|
||
|
|
console.error('❌ Missing required Autotask environment variables');
|
||
|
|
process.exit(1);
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
// Test 1: Database connection
|
||
|
|
console.log('Test 1: Verify database connection');
|
||
|
|
const dbConnected = await postgresClient.testConnection();
|
||
|
|
if (!dbConnected) {
|
||
|
|
console.error('❌ Database connection failed');
|
||
|
|
process.exit(1);
|
||
|
|
}
|
||
|
|
console.log('✅ Database connection successful\n');
|
||
|
|
|
||
|
|
// Test 2: Initialize clients
|
||
|
|
console.log('Test 2: Initialize Autotask client and sync service');
|
||
|
|
const autotaskClient = new AutotaskClient({
|
||
|
|
apiUrl: process.env.AUTOTASK_API_URL!,
|
||
|
|
username: process.env.AUTOTASK_USERNAME!,
|
||
|
|
password: process.env.AUTOTASK_SECRET!,
|
||
|
|
apiIntegrationCode: process.env.AUTOTASK_API_INTEGRATION_CODE!,
|
||
|
|
});
|
||
|
|
const syncService = createSyncService(autotaskClient);
|
||
|
|
console.log('✅ Clients initialized\n');
|
||
|
|
|
||
|
|
// Test 3: Sync a single entity (Companies)
|
||
|
|
console.log('Test 3: Sync single entity - Companies');
|
||
|
|
console.log('Testing entity-specific sync with one entity\n');
|
||
|
|
|
||
|
|
const singleEntityStart = Date.now();
|
||
|
|
const singleResult = await syncService.syncEntities(
|
||
|
|
[EntityType.COMPANIES],
|
||
|
|
SyncType.ENTITY_SPECIFIC,
|
||
|
|
'test-single-entity'
|
||
|
|
);
|
||
|
|
const singleDuration = Date.now() - singleEntityStart;
|
||
|
|
|
||
|
|
console.log('\n=== SINGLE ENTITY SYNC RESULTS ===');
|
||
|
|
console.log(`Sync ID: ${singleResult.syncId}`);
|
||
|
|
console.log(`Sync Type: ${singleResult.syncType}`);
|
||
|
|
console.log(`Status: ${singleResult.status}`);
|
||
|
|
console.log(`Duration: ${singleDuration}ms`);
|
||
|
|
console.log(`Entities synced: ${singleResult.entities.length}`);
|
||
|
|
|
||
|
|
singleResult.entities.forEach(entity => {
|
||
|
|
const status = entity.success ? '✅' : '❌';
|
||
|
|
console.log(`${status} ${entity.entityType}: +${entity.recordsAdded} ~${entity.recordsUpdated} -${entity.recordsDeleted}`);
|
||
|
|
if (entity.error) {
|
||
|
|
console.log(` Error: ${entity.error}`);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
if (singleResult.errors.length > 0) {
|
||
|
|
console.log('\nErrors:');
|
||
|
|
singleResult.errors.forEach(err => console.log(` - ${err}`));
|
||
|
|
}
|
||
|
|
console.log();
|
||
|
|
|
||
|
|
// Test 4: Verify sync history for single entity
|
||
|
|
console.log('Test 4: Verify sync history for single entity');
|
||
|
|
const companiesHistory = await syncService.getSyncHistory(1, EntityType.COMPANIES);
|
||
|
|
if (companiesHistory.length > 0) {
|
||
|
|
const latest = companiesHistory[0];
|
||
|
|
console.log(`✅ Latest companies sync:`);
|
||
|
|
console.log(` Sync Type: ${latest.sync_type}`);
|
||
|
|
console.log(` Status: ${latest.status}`);
|
||
|
|
|
||
|
|
if (latest.sync_type === 'entity-specific') {
|
||
|
|
console.log(` ✅ Confirmed: Sync type is entity-specific`);
|
||
|
|
} else {
|
||
|
|
console.log(` ⚠️ Expected entity-specific, got ${latest.sync_type}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
console.log();
|
||
|
|
|
||
|
|
// Test 5: Sync multiple entities
|
||
|
|
console.log('Test 5: Sync multiple entities - Companies, Resources, Statuses');
|
||
|
|
console.log('Testing entity-specific sync with multiple entities\n');
|
||
|
|
|
||
|
|
const multiEntityStart = Date.now();
|
||
|
|
const multiResult = await syncService.syncEntities(
|
||
|
|
[EntityType.COMPANIES, EntityType.RESOURCES, EntityType.STATUSES],
|
||
|
|
SyncType.ENTITY_SPECIFIC,
|
||
|
|
'test-multi-entity'
|
||
|
|
);
|
||
|
|
const multiDuration = Date.now() - multiEntityStart;
|
||
|
|
|
||
|
|
console.log('\n=== MULTIPLE ENTITY SYNC RESULTS ===');
|
||
|
|
console.log(`Sync ID: ${multiResult.syncId}`);
|
||
|
|
console.log(`Sync Type: ${multiResult.syncType}`);
|
||
|
|
console.log(`Status: ${multiResult.status}`);
|
||
|
|
console.log(`Duration: ${multiDuration}ms`);
|
||
|
|
console.log(`Entities synced: ${multiResult.entities.length}`);
|
||
|
|
console.log(`\nTotal Records:`);
|
||
|
|
console.log(` Added: ${multiResult.totalRecordsAdded}`);
|
||
|
|
console.log(` Updated: ${multiResult.totalRecordsUpdated}`);
|
||
|
|
console.log(` Deleted: ${multiResult.totalRecordsDeleted}`);
|
||
|
|
|
||
|
|
console.log('\n=== ENTITY DETAILS ===');
|
||
|
|
multiResult.entities.forEach(entity => {
|
||
|
|
const status = entity.success ? '✅' : '❌';
|
||
|
|
console.log(`${status} ${entity.entityType}:`);
|
||
|
|
console.log(` Added: ${entity.recordsAdded}, Updated: ${entity.recordsUpdated}, Deleted: ${entity.recordsDeleted}`);
|
||
|
|
console.log(` Duration: ${entity.duration}ms`);
|
||
|
|
if (entity.error) {
|
||
|
|
console.log(` Error: ${entity.error}`);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
if (multiResult.errors.length > 0) {
|
||
|
|
console.log('\nErrors:');
|
||
|
|
multiResult.errors.forEach(err => console.log(` - ${err}`));
|
||
|
|
}
|
||
|
|
console.log();
|
||
|
|
|
||
|
|
// Test 6: Verify entity ordering
|
||
|
|
console.log('Test 6: Verify entity sync ordering');
|
||
|
|
console.log('Entities should sync in dependency order (companies first, then dependent entities)');
|
||
|
|
|
||
|
|
const entityOrder = multiResult.entities.map(e => e.entityType);
|
||
|
|
console.log(`Actual sync order: ${entityOrder.join(' → ')}`);
|
||
|
|
|
||
|
|
// Companies should come before resources and statuses
|
||
|
|
const companiesIndex = entityOrder.indexOf(EntityType.COMPANIES);
|
||
|
|
const resourcesIndex = entityOrder.indexOf(EntityType.RESOURCES);
|
||
|
|
const statusesIndex = entityOrder.indexOf(EntityType.STATUSES);
|
||
|
|
|
||
|
|
if (companiesIndex !== -1 && resourcesIndex !== -1 && companiesIndex < resourcesIndex) {
|
||
|
|
console.log('✅ Companies synced before Resources (correct dependency order)');
|
||
|
|
} else {
|
||
|
|
console.log('⚠️ Dependency ordering may need verification');
|
||
|
|
}
|
||
|
|
console.log();
|
||
|
|
|
||
|
|
// Test 7: Test with picklist entities
|
||
|
|
console.log('Test 7: Sync picklist entities - Statuses, IssueTypes, WorkTypes');
|
||
|
|
console.log('Testing entity-specific sync with picklist/lookup entities\n');
|
||
|
|
|
||
|
|
const picklistStart = Date.now();
|
||
|
|
const picklistResult = await syncService.syncEntities(
|
||
|
|
[EntityType.STATUSES, EntityType.ISSUE_TYPES, EntityType.WORK_TYPES],
|
||
|
|
SyncType.ENTITY_SPECIFIC,
|
||
|
|
'test-picklist-entities'
|
||
|
|
);
|
||
|
|
const picklistDuration = Date.now() - picklistStart;
|
||
|
|
|
||
|
|
console.log('\n=== PICKLIST ENTITY SYNC RESULTS ===');
|
||
|
|
console.log(`Sync ID: ${picklistResult.syncId}`);
|
||
|
|
console.log(`Status: ${picklistResult.status}`);
|
||
|
|
console.log(`Duration: ${picklistDuration}ms`);
|
||
|
|
console.log(`Entities synced: ${picklistResult.entities.length}`);
|
||
|
|
|
||
|
|
picklistResult.entities.forEach(entity => {
|
||
|
|
const status = entity.success ? '✅' : '❌';
|
||
|
|
console.log(`${status} ${entity.entityType}: +${entity.recordsAdded} ~${entity.recordsUpdated}`);
|
||
|
|
if (entity.error) {
|
||
|
|
console.log(` Error: ${entity.error}`);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
console.log();
|
||
|
|
|
||
|
|
// Test 8: Verify sync history for multiple entities
|
||
|
|
console.log('Test 8: Verify sync history for all tested entities');
|
||
|
|
const entities = [EntityType.COMPANIES, EntityType.RESOURCES, EntityType.STATUSES];
|
||
|
|
|
||
|
|
for (const entity of entities) {
|
||
|
|
const history = await syncService.getSyncHistory(1, entity);
|
||
|
|
if (history.length > 0) {
|
||
|
|
const latest = history[0];
|
||
|
|
console.log(`✅ ${entity}: ${latest.status} (${latest.sync_type})`);
|
||
|
|
} else {
|
||
|
|
console.log(`⚠️ ${entity}: No sync history found`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
console.log();
|
||
|
|
|
||
|
|
// Test 9: Test entity-specific sync with full mode
|
||
|
|
console.log('Test 9: Entity-specific sync with FULL sync type');
|
||
|
|
console.log('Testing that entity-specific can use full sync mode\n');
|
||
|
|
|
||
|
|
const fullModeResult = await syncService.syncEntities(
|
||
|
|
[EntityType.STATUSES],
|
||
|
|
SyncType.FULL,
|
||
|
|
'test-entity-full-mode'
|
||
|
|
);
|
||
|
|
|
||
|
|
console.log(`✅ Entity-specific full sync completed: ${fullModeResult.status}`);
|
||
|
|
console.log(` Sync Type: ${fullModeResult.syncType}`);
|
||
|
|
console.log(` Entities: ${fullModeResult.entities.map(e => e.entityType).join(', ')}`);
|
||
|
|
console.log();
|
||
|
|
|
||
|
|
// Test 10: Test entity-specific sync with incremental mode
|
||
|
|
console.log('Test 10: Entity-specific sync with INCREMENTAL sync type');
|
||
|
|
console.log('Testing that entity-specific can use incremental sync mode\n');
|
||
|
|
|
||
|
|
const incrementalModeResult = await syncService.syncEntities(
|
||
|
|
[EntityType.COMPANIES],
|
||
|
|
SyncType.INCREMENTAL,
|
||
|
|
'test-entity-incremental-mode'
|
||
|
|
);
|
||
|
|
|
||
|
|
console.log(`✅ Entity-specific incremental sync completed: ${incrementalModeResult.status}`);
|
||
|
|
console.log(` Sync Type: ${incrementalModeResult.syncType}`);
|
||
|
|
console.log(` Entities: ${incrementalModeResult.entities.map(e => e.entityType).join(', ')}`);
|
||
|
|
console.log();
|
||
|
|
|
||
|
|
// Test 11: Count records in database for each entity
|
||
|
|
console.log('Test 11: Verify data in database for synced entities');
|
||
|
|
const entityTables = [
|
||
|
|
{ entity: EntityType.COMPANIES, table: 'companies' },
|
||
|
|
{ entity: EntityType.RESOURCES, table: 'resources' },
|
||
|
|
{ entity: EntityType.STATUSES, table: 'statuses' },
|
||
|
|
{ entity: EntityType.ISSUE_TYPES, table: 'issue_types' },
|
||
|
|
{ entity: EntityType.WORK_TYPES, table: 'work_types' },
|
||
|
|
];
|
||
|
|
|
||
|
|
for (const { entity, table } of entityTables) {
|
||
|
|
try {
|
||
|
|
const count = await postgresClient.count(table, { is_deleted: false });
|
||
|
|
console.log(`✅ ${entity}: ${count} records in database`);
|
||
|
|
} catch (error) {
|
||
|
|
console.log(`⚠️ ${entity}: Could not count records (${error instanceof Error ? error.message : 'unknown error'})`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
console.log();
|
||
|
|
|
||
|
|
// Test 12: Test error handling for invalid entity
|
||
|
|
console.log('Test 12: Test error handling with dependent entities');
|
||
|
|
console.log('Testing sync of entities that depend on others (e.g., Tickets depend on Companies)\n');
|
||
|
|
|
||
|
|
const dependentResult = await syncService.syncEntities(
|
||
|
|
[EntityType.TICKETS, EntityType.COMPANIES],
|
||
|
|
SyncType.ENTITY_SPECIFIC,
|
||
|
|
'test-dependent-entities'
|
||
|
|
);
|
||
|
|
|
||
|
|
console.log(`✅ Dependent entity sync completed: ${dependentResult.status}`);
|
||
|
|
console.log(` Entities synced in order: ${dependentResult.entities.map(e => e.entityType).join(' → ')}`);
|
||
|
|
|
||
|
|
// Verify companies came before tickets
|
||
|
|
const syncedOrder = dependentResult.entities.map(e => e.entityType);
|
||
|
|
const companiesIdx = syncedOrder.indexOf(EntityType.COMPANIES);
|
||
|
|
const ticketsIdx = syncedOrder.indexOf(EntityType.TICKETS);
|
||
|
|
|
||
|
|
if (companiesIdx !== -1 && ticketsIdx !== -1 && companiesIdx < ticketsIdx) {
|
||
|
|
console.log('✅ Dependency ordering respected: Companies synced before Tickets');
|
||
|
|
} else {
|
||
|
|
console.log('⚠️ Dependency ordering may need attention');
|
||
|
|
}
|
||
|
|
console.log();
|
||
|
|
|
||
|
|
// Summary
|
||
|
|
console.log('🎉 Entity-specific sync tests completed!');
|
||
|
|
console.log('\n=== SUMMARY ===');
|
||
|
|
console.log('✅ Single entity sync working');
|
||
|
|
console.log('✅ Multiple entity sync working');
|
||
|
|
console.log('✅ Picklist entity sync working');
|
||
|
|
console.log('✅ Entity-specific with FULL mode working');
|
||
|
|
console.log('✅ Entity-specific with INCREMENTAL mode working');
|
||
|
|
console.log('✅ Sync history tracking per entity');
|
||
|
|
console.log('✅ Dependency ordering verified');
|
||
|
|
console.log('✅ Database records verified');
|
||
|
|
|
||
|
|
// Test statistics
|
||
|
|
console.log('\n=== TEST STATISTICS ===');
|
||
|
|
const allResults = [singleResult, multiResult, picklistResult, fullModeResult, incrementalModeResult, dependentResult];
|
||
|
|
const totalSyncs = allResults.length;
|
||
|
|
const successfulSyncs = allResults.filter(r => r.status === 'completed').length;
|
||
|
|
const failedSyncs = allResults.filter(r => r.status === 'failed').length;
|
||
|
|
const totalEntitiesSynced = allResults.reduce((sum, r) => sum + r.entities.length, 0);
|
||
|
|
const successfulEntities = allResults.reduce((sum, r) => sum + r.entities.filter(e => e.success).length, 0);
|
||
|
|
const failedEntities = allResults.reduce((sum, r) => sum + r.entities.filter(e => !e.success).length, 0);
|
||
|
|
|
||
|
|
console.log(`Total sync operations: ${totalSyncs}`);
|
||
|
|
console.log(` Successful: ${successfulSyncs}`);
|
||
|
|
console.log(` Failed: ${failedSyncs}`);
|
||
|
|
console.log(`\nTotal entities processed: ${totalEntitiesSynced}`);
|
||
|
|
console.log(` Successful: ${successfulEntities}`);
|
||
|
|
console.log(` Failed: ${failedEntities}`);
|
||
|
|
|
||
|
|
const successRate = ((successfulEntities / totalEntitiesSynced) * 100).toFixed(1);
|
||
|
|
console.log(`\nEntity success rate: ${successRate}%`);
|
||
|
|
|
||
|
|
} catch (error) {
|
||
|
|
console.error('\n❌ Test failed with error:', error);
|
||
|
|
if (error instanceof Error) {
|
||
|
|
console.error('Stack trace:', error.stack);
|
||
|
|
}
|
||
|
|
process.exit(1);
|
||
|
|
} finally {
|
||
|
|
// Close database connection
|
||
|
|
await postgresClient.close();
|
||
|
|
console.log('\n🔌 Database connection closed');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Run the tests
|
||
|
|
testEntitySpecificSync()
|
||
|
|
.then(() => {
|
||
|
|
console.log('\n✅ Test script completed successfully');
|
||
|
|
process.exit(0);
|
||
|
|
})
|
||
|
|
.catch((error) => {
|
||
|
|
console.error('\n❌ Test script failed:', error);
|
||
|
|
process.exit(1);
|
||
|
|
});
|