diff --git a/lib/utils/entity-mapper.ts b/lib/utils/entity-mapper.ts index c0934f4..0a91dd9 100644 --- a/lib/utils/entity-mapper.ts +++ b/lib/utils/entity-mapper.ts @@ -383,7 +383,6 @@ function mapResource(data: any): Record { * Map Configuration Item entity */ function mapConfigurationItem(data: any): Record { - // Configuration items have many fields, map all of them const mapped: Record = { id: data.id, company_id: data.companyID, @@ -394,42 +393,109 @@ function mapConfigurationItem(data: any): Record { install_date: data.installDate, warranty_expiration_date: data.warrantyExpirationDate, 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, + 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, - synced_at: data.synced_at, - is_deleted: data.is_deleted || false, + device_type: data.deviceType, + 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)) { + const udfMap: Record = {}; + 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( (udf: any) => udf.name === 'Backup Type' || String(udf.name) === '29693319' ); - if (backupUdf && backupUdf.value) { + if (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; } diff --git a/migrations/052_add_configuration_items_udfs_and_missing_fields.sql b/migrations/052_add_configuration_items_udfs_and_missing_fields.sql new file mode 100644 index 0000000..e154670 --- /dev/null +++ b/migrations/052_add_configuration_items_udfs_and_missing_fields.sql @@ -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().