fix: configuration_items sync — correct camelCase field mapping, store all UDFs in udfs JSONB

- Fix mapConfigurationItem() to use Autotask camelCase field names (was using
  snake_case keys that never matched, leaving costs/Datto/RMM fields empty)
- Map all 60+ fields explicitly: costs, Datto device fields, RMM audit fields,
  location, vendor, SNMP, backup dates, source cost, etc.
- Store entire userDefinedFields array as udfs JSONB (keyed by UDF name)
- Retain backup_type_udf VARCHAR column for backward compat
- Migration 052: add udfs JSONB column + GIN index to configuration_items
This commit is contained in:
lorentz 2026-03-17 15:39:42 -04:00
parent 0f1083f5b6
commit cb293ea843
2 changed files with 107 additions and 25 deletions

View file

@ -383,7 +383,6 @@ function mapResource(data: any): Record<string, any> {
* Map Configuration Item entity * Map Configuration Item entity
*/ */
function mapConfigurationItem(data: any): Record<string, any> { function mapConfigurationItem(data: any): Record<string, any> {
// Configuration items have many fields, map all of them
const mapped: Record<string, any> = { const mapped: Record<string, any> = {
id: data.id, id: data.id,
company_id: data.companyID, company_id: data.companyID,
@ -394,42 +393,109 @@ function mapConfigurationItem(data: any): Record<string, any> {
install_date: data.installDate, install_date: data.installDate,
warranty_expiration_date: data.warrantyExpirationDate, warranty_expiration_date: data.warrantyExpirationDate,
is_active: data.isActive !== undefined ? data.isActive : true, is_active: data.isActive !== undefined ? data.isActive : true,
daily_cost: data.dailyCost,
hourly_cost: data.hourlyCost,
monthly_cost: data.monthlyCost,
per_use_cost: data.perUseCost,
setup_fee: data.setupFee,
contact_id: data.contactID, contact_id: data.contactID,
location_id: data.locationID,
vendor_id: data.vendorID,
installed_by_id: data.installedByID,
installed_by_contact_id: data.installedByContactID,
parent_configuration_item_id: data.parentConfigurationItemID,
notes: data.notes,
create_date: data.createDate,
created_by_person_id: data.createdByPersonID,
last_modified_time: data.lastModifiedTime,
last_activity_person_type: data.lastActivityPersonType,
impersonator_creator_resource_id: data.impersonatorCreatorResourceID,
configuration_item_category_id: data.configurationItemCategoryID,
configuration_item_type: data.type, configuration_item_type: data.type,
synced_at: data.synced_at, device_type: data.deviceType,
is_deleted: data.is_deleted || false, rmm_device_uid: data.rmmDeviceUID,
api_vendor_id: data.apiVendorID,
// Datto fields
datto_availability: data.dattoAvailability,
datto_device_memory_megabytes: data.dattoDeviceMemoryMegabytes,
datto_drives_errors: data.dattoDrivesErrors,
datto_hostname: data.dattoHostname,
datto_internal_ip: data.dattoInternalIP,
datto_kernel_version_id: data.dattoKernelVersionID,
datto_last_check_in_date_time: data.dattoLastCheckInDateTime,
datto_nic_speed_kilobits_per_second: data.dattoNicSpeedKilobitsPerSecond,
datto_number_of_agents: data.dattoNumberOfAgents,
datto_number_of_drives: data.dattoNumberOfDrives,
datto_number_of_logical_volumes: data.dattoNumberOfLogicalVolumes,
datto_number_of_volumes: data.dattoNumberOfVolumes,
datto_off_site_storage_used_bytes: data.dattoOffSiteStorageUsedBytes,
datto_os_version_id: data.dattoOsVersionID,
datto_percentage_used: data.dattoPercentageUsed,
datto_protected_kilobytes: data.dattoProtectedKilobytes,
datto_remote_ip: data.dattoRemoteIP,
datto_serial_number: data.dattoSerialNumber,
datto_uptime_seconds: data.dattoUptimeSeconds,
datto_used_kilobytes: data.dattoUsedKilobytes,
datto_z_pool_percentage: data.dattoZPoolPercentage,
device_networking_id: data.deviceNetworkingID,
last_backup_date: data.lastBackupDateTime || data.lastBackupDate,
last_backup_status: data.lastBackupStatus,
os_version_id: data.osVersionID,
service_id: data.serviceID,
service_bundle_id: data.serviceBundleID,
snmp_location: data.snmpLocation,
snmp_name: data.snmpName,
snmp_contact: data.snmpContact,
// RMM audit fields
rmm_device_audit_architecture_id: data.rmmDeviceAuditArchitectureID,
rmm_device_audit_display_adaptor_id: data.rmmDeviceAuditDisplayAdaptorID,
rmm_device_audit_domain_id: data.rmmDeviceAuditDomainID,
rmm_device_audit_external_ip_address: data.rmmDeviceAuditExternalIPAddress,
rmm_device_audit_hostname: data.rmmDeviceAuditHostname,
rmm_device_audit_ip_address: data.rmmDeviceAuditIPAddress,
rmm_device_audit_mac_address: data.rmmDeviceAuditMacAddress,
rmm_device_audit_manufacturer_id: data.rmmDeviceAuditManufacturerID,
rmm_device_audit_missing_patch_count: data.rmmDeviceAuditMissingPatchCount,
rmm_device_audit_mobile_network_operator_id: data.rmmDeviceAuditMobileNetworkOperatorID,
rmm_device_audit_mobile_number: data.rmmDeviceAuditMobileNumber,
rmm_device_audit_model_id: data.rmmDeviceAuditModelID,
rmm_device_audit_motherboard_id: data.rmmDeviceAuditMotherboardID,
rmm_device_audit_operating_system_id: data.rmmDeviceAuditOperatingSystemID,
rmm_device_audit_processor_id: data.rmmDeviceAuditProcessorID,
rmm_device_audit_service_pack_id: data.rmmDeviceAuditServicePackID,
rmm_device_audit_snmp_contact: data.rmmDeviceAuditSNMPContact,
rmm_device_audit_snmp_location: data.rmmDeviceAuditSNMPLocation,
rmm_device_audit_snmp_name: data.rmmDeviceAuditSNMPName,
rmm_device_audit_software_status_id: data.rmmDeviceAuditSoftwareStatusID,
rmm_device_audit_storage_bytes: data.rmmDeviceAuditStorageBytes,
rmm_open_alert_count: data.rmmOpenAlertCount,
rmm_device_audit_description: data.rmmDeviceAuditDescription,
rmm_device_audit_device_type_id: data.rmmDeviceAuditDeviceTypeID,
rmm_device_audit_last_user: data.rmmDeviceAuditLastUser,
rmm_device_audit_memory_bytes: data.rmmDeviceAuditMemoryBytes,
source_cost_id: data.sourceCostID,
source_cost_type: data.sourceCostType,
}; };
// Extract backup-type UDF (ID 29693319) from userDefinedFields // Store all UDFs as JSONB
if (data.userDefinedFields && Array.isArray(data.userDefinedFields)) { if (data.userDefinedFields && Array.isArray(data.userDefinedFields)) {
const udfMap: Record<string, string> = {};
for (const udf of data.userDefinedFields) {
if (udf.name != null) {
udfMap[String(udf.name)] = udf.value ?? null;
}
}
mapped.udfs = Object.keys(udfMap).length > 0 ? udfMap : null;
// Also extract the named backup_type_udf for backward compat
const backupUdf = data.userDefinedFields.find( const backupUdf = data.userDefinedFields.find(
(udf: any) => udf.name === 'Backup Type' || String(udf.name) === '29693319' (udf: any) => udf.name === 'Backup Type' || String(udf.name) === '29693319'
); );
if (backupUdf && backupUdf.value) { if (backupUdf?.value) {
mapped.backup_type_udf = backupUdf.value; mapped.backup_type_udf = backupUdf.value;
} }
} }
// Add all other fields dynamically
const fieldsToInclude = [
'daily_cost', 'hourly_cost', 'monthly_cost', 'per_use_cost', 'setup_fee',
'location_id', 'vendor_id', 'installed_by_id', 'installed_by_contact_id',
'parent_configuration_item_id', 'notes', 'create_date', 'created_by_person_id',
'last_modified_time', 'last_activity_person_type', 'impersonator_creator_resource_id',
'configuration_item_category_id', 'configuration_item_type', 'device_type',
'rmm_device_uid', 'api_vendor_id',
];
// Add Datto RMM fields
const dattoFields = Object.keys(data).filter(key => key.startsWith('datto_') || key.startsWith('rmm_'));
fieldsToInclude.push(...dattoFields);
fieldsToInclude.forEach(field => {
if (data[field] !== undefined) {
mapped[field] = data[field];
}
});
return mapped; return mapped;
} }

View file

@ -0,0 +1,16 @@
-- ============================================================================
-- Add udfs JSONB column to configuration_items to store all Autotask UDFs
-- ============================================================================
-- Generic JSONB store for all UDFs returned from Autotask userDefinedFields
ALTER TABLE configuration_items
ADD COLUMN IF NOT EXISTS udfs JSONB;
-- Index for UDF queries (GIN for JSONB containment/key lookups)
CREATE INDEX IF NOT EXISTS idx_configuration_items_udfs
ON configuration_items USING GIN (udfs);
-- Note: all other schema columns (costs, Datto fields, RMM audit fields, etc.)
-- already existed from 001_initial_schema.sql. The mapper was writing them with
-- snake_case keys instead of reading from the camelCase Autotask response — that
-- was a code-only bug fixed in lib/utils/entity-mapper.ts mapConfigurationItem().