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:
parent
0f1083f5b6
commit
cb293ea843
2 changed files with 107 additions and 25 deletions
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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().
|
||||||
Loading…
Add table
Add a link
Reference in a new issue