/** * IT Glue Sync Service — Part 1 of 2 * Syncs all IT Glue data to local itg_* PostgreSQL tables */ import postgresClient from './postgres-client'; import { getITGlueClient } from './itglue-client'; export interface ITGlueSyncEntityResult { entity: string; success: boolean; recordsUpserted: number; duration: number; error?: string; } export interface ITGlueSyncResult { syncId: number; syncType: 'full'; status: 'completed' | 'failed'; startedAt: Date; completedAt: Date; duration: number; entities: ITGlueSyncEntityResult[]; totalUpserted: number; errors: string[]; } export class ITGlueSyncService { private isSyncing = false; isSyncInProgress(): boolean { return this.isSyncing; } async fullSync(triggeredBy = 'system'): Promise { if (this.isSyncing) throw new Error('IT Glue sync already in progress'); this.isSyncing = true; const startedAt = new Date(); const entities: ITGlueSyncEntityResult[] = []; const errors: string[] = []; const { rows } = await postgresClient.query( `INSERT INTO itg_sync_history (sync_type, status, triggered_by, started_at) VALUES ('full','running',$1,NOW()) RETURNING id`, [triggeredBy] ); const syncId = rows[0].id; const run = async (name: string, fn: () => Promise) => { const t = Date.now(); try { const count = await fn(); entities.push({ entity: name, success: true, recordsUpserted: count, duration: Date.now() - t }); console.log(`[ITGlue] ${name}: ${count} records`); } catch (err: any) { errors.push(`${name}: ${err.message}`); entities.push({ entity: name, success: false, recordsUpserted: 0, duration: Date.now() - t, error: err.message }); console.error(`[ITGlue] ${name} FAILED:`, err.message); } }; try { await run('organization_types', () => this.syncSimpleTable('/organization_types', 'itg_organization_types')); await run('organization_statuses', () => this.syncSimpleTable('/organization_statuses','itg_organization_statuses')); await run('configuration_types', () => this.syncSimpleTable('/configuration_types', 'itg_configuration_types')); await run('configuration_statuses', () => this.syncSimpleTable('/configuration_statuses','itg_configuration_statuses')); await run('contact_types', () => this.syncSimpleTable('/contact_types', 'itg_contact_types')); await run('password_categories', () => this.syncSimpleTable('/password_categories', 'itg_password_categories')); await run('manufacturers', () => this.syncSimpleTable('/manufacturers', 'itg_manufacturers')); await run('operating_systems', () => this.syncSimpleTable('/operating_systems', 'itg_operating_systems')); await run('platforms', () => this.syncSimpleTable('/platforms', 'itg_platforms')); await run('countries', () => this.syncCountries()); await run('models', () => this.syncModels()); await run('flexible_asset_types', () => this.syncFlexibleAssetTypes()); await run('flexible_asset_fields', () => this.syncFlexibleAssetFields()); await run('organizations', () => this.syncOrganizations()); await run('locations', () => this.syncLocations()); await run('contacts', () => this.syncContacts()); await run('configurations', () => this.syncConfigurations()); // configuration_interfaces skipped — no flat API endpoint; per-config calls are too slow for 14k+ configs await run('flexible_assets', () => this.syncFlexibleAssets()); await run('password_folders', () => this.syncPasswordFolders()); await run('passwords', () => this.syncPasswords()); await run('documents', () => this.syncDocuments()); await run('domains', () => this.syncDomains()); await run('expirations', () => this.syncExpirations()); } finally { this.isSyncing = false; } const completedAt = new Date(); const duration = completedAt.getTime() - startedAt.getTime(); const totalUpserted = entities.reduce((s, e) => s + e.recordsUpserted, 0); const status = errors.length === 0 ? 'completed' : 'failed'; await postgresClient.query( `UPDATE itg_sync_history SET status=$1, completed_at=NOW(), duration_ms=$2, entities=$3, error=$4, total_upserted=$5 WHERE id=$6`, [status, duration, JSON.stringify(entities), errors.join('\n') || null, totalUpserted, syncId] ); return { syncId, syncType: 'full', status, startedAt, completedAt, duration, entities, totalUpserted, errors }; } // ─── Generic simple-table upsert (id, name, created_at, updated_at) ────────── private async syncSimpleTable(path: string, table: string): Promise { const client = getITGlueClient(); const items = await client.getRawAllPages(path); let count = 0; for (const item of items) { const a = item.attributes; await postgresClient.query( `INSERT INTO ${table} (id, name, created_at, updated_at, synced_at) VALUES ($1,$2,$3,$4,NOW()) ON CONFLICT (id) DO UPDATE SET name=$2, updated_at=$4, synced_at=NOW()`, [item.id, a.name, a['created-at'] || null, a['updated-at'] || null] ); count++; } return count; } private async syncCountries(): Promise { const client = getITGlueClient(); const items = await client.getRawAllPages('/countries'); let count = 0; for (const item of items) { const a = item.attributes; await postgresClient.query( `INSERT INTO itg_countries (id, name, iso_code, created_at, updated_at, synced_at) VALUES ($1,$2,$3,$4,$5,NOW()) ON CONFLICT (id) DO UPDATE SET name=$2, iso_code=$3, updated_at=$5, synced_at=NOW()`, [item.id, a.name, a['iso-code'] || null, a['created-at'] || null, a['updated-at'] || null] ); count++; } return count; } private async syncModels(): Promise { const client = getITGlueClient(); const mfrs = await client.getRawAllPages('/manufacturers'); let count = 0; for (const mfr of mfrs) { const models = await client.getRawAllPages(`/manufacturers/${mfr.id}/relationships/models`); for (const item of models) { const a = item.attributes; await postgresClient.query( `INSERT INTO itg_models (id, manufacturer_id, name, created_at, updated_at, synced_at) VALUES ($1,$2,$3,$4,$5,NOW()) ON CONFLICT (id) DO UPDATE SET manufacturer_id=$2, name=$3, updated_at=$5, synced_at=NOW()`, [item.id, mfr.id, a.name, a['created-at'] || null, a['updated-at'] || null] ); count++; } } return count; } private async syncFlexibleAssetTypes(): Promise { const client = getITGlueClient(); const items = await client.getRawAllPages('/flexible_asset_types'); let count = 0; for (const item of items) { const a = item.attributes; await postgresClient.query( `INSERT INTO itg_flexible_asset_types (id, name, description, icon, enabled, builtin, created_at, updated_at, synced_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NOW()) ON CONFLICT (id) DO UPDATE SET name=$2, description=$3, icon=$4, enabled=$5, builtin=$6, updated_at=$8, synced_at=NOW()`, [item.id, a.name, a.description || null, a.icon || null, a.enabled ?? true, a.builtin ?? false, a['created-at'] || null, a['updated-at'] || null] ); count++; } return count; } private async syncFlexibleAssetFields(): Promise { const client = getITGlueClient(); const types = await client.getRawAllPages('/flexible_asset_types'); let count = 0; for (const type of types) { const fields = await client.getRawAllPages( `/flexible_asset_types/${type.id}/relationships/flexible_asset_fields` ); for (const item of fields) { const a = item.attributes; await postgresClient.query( `INSERT INTO itg_flexible_asset_fields (id, flexible_asset_type_id, name, kind, hint, decimals, tag_type, required, use_for_title, expiration, show_in_list, created_at, updated_at, synced_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,NOW()) ON CONFLICT (id) DO UPDATE SET flexible_asset_type_id=$2, name=$3, kind=$4, hint=$5, decimals=$6, tag_type=$7, required=$8, use_for_title=$9, expiration=$10, show_in_list=$11, updated_at=$13, synced_at=NOW()`, [item.id, type.id, a.name, a.kind || null, a.hint || null, a.decimals || 0, a['tag-type'] || null, a.required ?? false, a['use-for-title'] ?? false, a.expiration ?? false, a['show-in-list'] ?? false, a['created-at'] || null, a['updated-at'] || null] ); count++; } } return count; } private async syncOrganizations(): Promise { const client = getITGlueClient(); const items = await client.getRawAllPages('/organizations'); let count = 0; for (const item of items) { const a = item.attributes; await postgresClient.query( `INSERT INTO itg_organizations (id, name, short_name, organization_type_id, organization_type_name, organization_status_id, organization_status_name, psa_integration, psa_id, sync_active, primary_org, quick_notes, description, alert, parent_id, created_at, updated_at, synced_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,NOW()) ON CONFLICT (id) DO UPDATE SET name=$2, short_name=$3, organization_type_id=$4, organization_type_name=$5, organization_status_id=$6, organization_status_name=$7, psa_integration=$8, psa_id=$9, sync_active=$10, primary_org=$11, quick_notes=$12, description=$13, alert=$14, parent_id=$15, updated_at=$17, synced_at=NOW()`, [item.id, a.name, a['short-name'] || null, a['organization-type-id'] || null, a['organization-type-name'] || null, a['organization-status-id'] || null, a['organization-status-name'] || null, a['psa-integration'] || null, a['psa-id'] || null, a['sync-active'] ?? false, a.primary ?? false, a['quick-notes'] || null, a.description || null, a.alert || null, a['parent-id'] || null, a['created-at'] || null, a['updated-at'] || null] ); count++; } return count; } private async syncLocations(): Promise { const client = getITGlueClient(); const items = await client.getRawAllPages('/locations'); let count = 0; for (const item of items) { const a = item.attributes; await postgresClient.query( `INSERT INTO itg_locations (id, organization_id, organization_name, name, primary_location, address_1, address_2, city, region_name, postal_code, country_name, phone, fax, notes, created_at, updated_at, synced_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,NOW()) ON CONFLICT (id) DO UPDATE SET organization_id=$2, organization_name=$3, name=$4, primary_location=$5, address_1=$6, address_2=$7, city=$8, region_name=$9, postal_code=$10, country_name=$11, phone=$12, fax=$13, notes=$14, updated_at=$16, synced_at=NOW()`, [item.id, a['organization-id'], a['organization-name'] || null, a.name, a['primary-location'] ?? false, a['address-1'] || null, a['address-2'] || null, a.city || null, a['region-name'] || null, a['postal-code'] || null, a['country-name'] || null, a.phone || null, a.fax || null, a.notes || null, a['created-at'] || null, a['updated-at'] || null] ); count++; } return count; } private async syncContacts(): Promise { const client = getITGlueClient(); const items = await client.getRawAllPages('/contacts'); let count = 0; for (const item of items) { const a = item.attributes; await postgresClient.query( `INSERT INTO itg_contacts (id, organization_id, organization_name, first_name, last_name, name, title, contact_type_id, contact_type_name, location_id, important, notes, emails, phones, created_at, updated_at, synced_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,NOW()) ON CONFLICT (id) DO UPDATE SET organization_id=$2, organization_name=$3, first_name=$4, last_name=$5, name=$6, title=$7, contact_type_id=$8, contact_type_name=$9, location_id=$10, important=$11, notes=$12, emails=$13, phones=$14, updated_at=$16, synced_at=NOW()`, [item.id, a['organization-id'], a['organization-name'] || null, a['first-name'] || null, a['last-name'] || null, a.name || null, a.title || null, a['contact-type-id'] || null, a['contact-type-name'] || null, a['location-id'] || null, a.important ?? false, a.notes || null, JSON.stringify(a['contact-emails'] || []), JSON.stringify(a['contact-phones'] || []), a['created-at'] || null, a['updated-at'] || null] ); count++; } return count; } private async syncConfigurations(): Promise { const client = getITGlueClient(); const items = await client.getRawAllPages('/configurations'); let count = 0; for (const item of items) { const a = item.attributes; await postgresClient.query( `INSERT INTO itg_configurations (id, organization_id, organization_name, name, hostname, primary_ip, mac_address, serial_number, asset_tag, position, installed_by, purchased_by, notes, operating_system_notes, warranty_expires_at, installed_at, purchased_at, end_of_life_at, configuration_type_id, configuration_type_name, configuration_status_id, configuration_status_name, manufacturer_id, manufacturer_name, model_id, model_name, operating_system_id, operating_system_name, location_id, contact_id, rmm_id, rmm_integration_type, created_at, updated_at, synced_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33,$34,NOW()) ON CONFLICT (id) DO UPDATE SET organization_id=$2, organization_name=$3, name=$4, hostname=$5, primary_ip=$6, mac_address=$7, serial_number=$8, asset_tag=$9, position=$10, installed_by=$11, purchased_by=$12, notes=$13, operating_system_notes=$14, warranty_expires_at=$15, installed_at=$16, purchased_at=$17, end_of_life_at=$18, configuration_type_id=$19, configuration_type_name=$20, configuration_status_id=$21, configuration_status_name=$22, manufacturer_id=$23, manufacturer_name=$24, model_id=$25, model_name=$26, operating_system_id=$27, operating_system_name=$28, location_id=$29, contact_id=$30, rmm_id=$31, rmm_integration_type=$32, updated_at=$34, synced_at=NOW()`, [item.id, a['organization-id'], a['organization-name'] || null, a.name, a.hostname || null, a['primary-ip'] || null, a['mac-address'] || null, a['serial-number'] || null, a['asset-tag'] || null, a.position || null, a['installed-by'] || null, a['purchased-by'] || null, a.notes || null, a['operating-system-notes'] || null, a['warranty-expires-at'] || null, a['installed-at'] || null, a['purchased-at'] || null, a['end-of-life-at'] || null, a['configuration-type-id'] || null, a['configuration-type-name'] || null, a['configuration-status-id'] || null, a['configuration-status-name'] || null, a['manufacturer-id'] || null, a['manufacturer-name'] || null, a['model-id'] || null, a['model-name'] || null, a['operating-system-id'] || null, a['operating-system-name'] || null, a['location-id'] || null, a['contact-id'] || null, a['rmm-id'] || null, a['rmm-integration-type'] || null, a['created-at'] || null, a['updated-at'] || null] ); count++; } return count; } private async syncConfigurationInterfaces(): Promise { const client = getITGlueClient(); // No flat endpoint — must iterate per configuration const configs = await client.getRawAllPages('/configurations'); let count = 0; for (const cfg of configs) { const items = await client.getRawAllPages( `/configurations/${cfg.id}/relationships/configuration_interfaces` ); for (const item of items) { const a = item.attributes; await postgresClient.query( `INSERT INTO itg_configuration_interfaces (id, configuration_id, organization_id, name, ip_address, mac_address, primary_interface, notes, created_at, updated_at, synced_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,NOW()) ON CONFLICT (id) DO UPDATE SET configuration_id=$2, organization_id=$3, name=$4, ip_address=$5, mac_address=$6, primary_interface=$7, notes=$8, updated_at=$10, synced_at=NOW()`, [item.id, cfg.id, a['organization-id'] || null, a.name || null, a['ip-address'] || null, a['mac-address'] || null, a.primary ?? false, a.notes || null, a['created-at'] || null, a['updated-at'] || null] ); count++; } } return count; } private async syncFlexibleAssets(): Promise { const client = getITGlueClient(); // API requires filter[flexible-asset-type-id] — iterate per type const types = await client.getRawAllPages('/flexible_asset_types'); let count = 0; for (const type of types) { const items = await client.getRawAllPages('/flexible_assets', { 'filter[flexible-asset-type-id]': type.id, }); for (const item of items) { const a = item.attributes; await postgresClient.query( `INSERT INTO itg_flexible_assets (id, organization_id, organization_name, flexible_asset_type_id, flexible_asset_type_name, name, traits, archived, created_at, updated_at, synced_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,NOW()) ON CONFLICT (id) DO UPDATE SET organization_id=$2, organization_name=$3, flexible_asset_type_id=$4, flexible_asset_type_name=$5, name=$6, traits=$7, archived=$8, updated_at=$10, synced_at=NOW()`, [item.id, a['organization-id'], a['organization-name'] || null, a['flexible-asset-type-id'], a['flexible-asset-type-name'] || null, a.name || null, JSON.stringify(a.traits || {}), a.archived ?? false, a['created-at'] || null, a['updated-at'] || null] ); count++; } } return count; } private async syncPasswordFolders(): Promise { const client = getITGlueClient(); // No flat endpoint — must iterate per organization const orgs = await postgresClient.query('SELECT id FROM itg_organizations'); let count = 0; for (const org of orgs.rows) { const items = await client.getRawAllPages( `/organizations/${org.id}/relationships/password_folders` ); for (const item of items) { const a = item.attributes; await postgresClient.query( `INSERT INTO itg_password_folders (id, organization_id, organization_name, name, inherited, created_at, updated_at, synced_at) VALUES ($1,$2,$3,$4,$5,$6,$7,NOW()) ON CONFLICT (id) DO UPDATE SET organization_id=$2, organization_name=$3, name=$4, inherited=$5, updated_at=$7, synced_at=NOW()`, [item.id, org.id, a['organization-name'] || null, a.name, a.inherited ?? false, a['created-at'] || null, a['updated-at'] || null] ); count++; } } return count; } private async syncPasswords(): Promise { const client = getITGlueClient(); const items = await client.getRawAllPages('/passwords'); let count = 0; for (const item of items) { const a = item.attributes; await postgresClient.query( `INSERT INTO itg_passwords (id, organization_id, organization_name, name, username, password, url, notes, password_category_id, password_category_name, password_folder_id, autofill_selectors, otp_enabled, archived, created_at, updated_at, synced_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,NOW()) ON CONFLICT (id) DO UPDATE SET organization_id=$2, organization_name=$3, name=$4, username=$5, password=$6, url=$7, notes=$8, password_category_id=$9, password_category_name=$10, password_folder_id=$11, autofill_selectors=$12, otp_enabled=$13, archived=$14, updated_at=$16, synced_at=NOW()`, [item.id, a['organization-id'], a['organization-name'] || null, a.name, a.username || null, a.password || null, a.url || null, a.notes || null, a['password-category-id'] || null, a['password-category-name'] || null, a['password-folder-id'] || null, a['autofill-selectors'] || null, a['otp-enabled'] ?? false, a.archived ?? false, a['created-at'] || null, a['updated-at'] || null] ); count++; } return count; } private async syncDocuments(): Promise { const client = getITGlueClient(); // No flat endpoint — must iterate per organization const orgs = await postgresClient.query('SELECT id FROM itg_organizations'); let count = 0; for (const org of orgs.rows) { const items = await client.getRawAllPages( `/organizations/${org.id}/relationships/documents` ); for (const item of items) { const a = item.attributes; await postgresClient.query( `INSERT INTO itg_documents (id, organization_id, organization_name, name, content, draft, archived, created_at, updated_at, synced_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,NOW()) ON CONFLICT (id) DO UPDATE SET organization_id=$2, organization_name=$3, name=$4, content=$5, draft=$6, archived=$7, updated_at=$9, synced_at=NOW()`, [item.id, org.id, a['organization-name'] || null, a.name, a.content || null, a.draft ?? false, a.archived ?? false, a['created-at'] || null, a['updated-at'] || null] ); count++; } } return count; } private async syncDomains(): Promise { const client = getITGlueClient(); const items = await client.getRawAllPages('/domains'); let count = 0; for (const item of items) { const a = item.attributes; await postgresClient.query( `INSERT INTO itg_domains (id, organization_id, organization_name, name, screenshot, whois_updated_at, expires_at, registrar_name, notes, created_at, updated_at, synced_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,NOW()) ON CONFLICT (id) DO UPDATE SET organization_id=$2, organization_name=$3, name=$4, screenshot=$5, whois_updated_at=$6, expires_at=$7, registrar_name=$8, notes=$9, updated_at=$11, synced_at=NOW()`, [item.id, a['organization-id'], a['organization-name'] || null, a.name, a.screenshot || null, a['whois-updated-at'] || null, a['expires-at'] || null, a['registrar-name'] || null, a.notes || null, a['created-at'] || null, a['updated-at'] || null] ); count++; } return count; } private async syncExpirations(): Promise { const client = getITGlueClient(); // No flat endpoint — must iterate per organization const orgs = await postgresClient.query('SELECT id FROM itg_organizations'); let count = 0; for (const org of orgs.rows) { const items = await client.getRawAllPages( `/organizations/${org.id}/relationships/expirations` ); for (const item of items) { const a = item.attributes; await postgresClient.query( `INSERT INTO itg_expirations (id, organization_id, organization_name, resource_id, resource_type, resource_name, expiration_type, description, expiration_date, notify, created_at, updated_at, synced_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,NOW()) ON CONFLICT (id) DO UPDATE SET organization_id=$2, organization_name=$3, resource_id=$4, resource_type=$5, resource_name=$6, expiration_type=$7, description=$8, expiration_date=$9, notify=$10, updated_at=$12, synced_at=NOW()`, [item.id, org.id, a['organization-name'] || null, a['resource-id'] || null, a['resource-type'] || null, a['resource-name'] || null, a['expiration-type'] || null, a.description || null, a['expiration-date'] || null, a.notify ?? false, a['created-at'] || null, a['updated-at'] || null] ); count++; } } return count; } /** * Re-fetch a single configuration from IT Glue and upsert the mirror row. * Mirrors the bulk syncConfigurations upsert. Used after a write so the * UI sees the new value immediately. */ async refreshConfigurationById(id: string | number): Promise { const client = getITGlueClient(); const item = await client.getRawSingle(`/configurations/${id}`); if (!item) { throw new Error(`IT Glue refreshConfigurationById ${id}: empty response`); } const a = item.attributes; await postgresClient.query( `INSERT INTO itg_configurations (id, organization_id, organization_name, name, hostname, primary_ip, mac_address, serial_number, asset_tag, position, installed_by, purchased_by, notes, operating_system_notes, warranty_expires_at, installed_at, purchased_at, end_of_life_at, configuration_type_id, configuration_type_name, configuration_status_id, configuration_status_name, manufacturer_id, manufacturer_name, model_id, model_name, operating_system_id, operating_system_name, location_id, contact_id, rmm_id, rmm_integration_type, created_at, updated_at, synced_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33,$34,NOW()) ON CONFLICT (id) DO UPDATE SET organization_id=$2, organization_name=$3, name=$4, hostname=$5, primary_ip=$6, mac_address=$7, serial_number=$8, asset_tag=$9, position=$10, installed_by=$11, purchased_by=$12, notes=$13, operating_system_notes=$14, warranty_expires_at=$15, installed_at=$16, purchased_at=$17, end_of_life_at=$18, configuration_type_id=$19, configuration_type_name=$20, configuration_status_id=$21, configuration_status_name=$22, manufacturer_id=$23, manufacturer_name=$24, model_id=$25, model_name=$26, operating_system_id=$27, operating_system_name=$28, location_id=$29, contact_id=$30, rmm_id=$31, rmm_integration_type=$32, updated_at=$34, synced_at=NOW()`, [ item.id, a['organization-id'], a['organization-name'] || null, a.name, a.hostname || null, a['primary-ip'] || null, a['mac-address'] || null, a['serial-number'] || null, a['asset-tag'] || null, a.position || null, a['installed-by'] || null, a['purchased-by'] || null, a.notes || null, a['operating-system-notes'] || null, a['warranty-expires-at'] || null, a['installed-at'] || null, a['purchased-at'] || null, a['end-of-life-at'] || null, a['configuration-type-id'] || null, a['configuration-type-name'] || null, a['configuration-status-id'] || null, a['configuration-status-name'] || null, a['manufacturer-id'] || null, a['manufacturer-name'] || null, a['model-id'] || null, a['model-name'] || null, a['operating-system-id'] || null, a['operating-system-name'] || null, a['location-id'] || null, a['contact-id'] || null, a['rmm-id'] || null, a['rmm-integration-type'] || null, a['created-at'] || null, a['updated-at'] || null, ] ); } /** * Re-fetch a single flexible asset from IT Glue and upsert the mirror row. * Used after a write to keep itg_flexible_assets in sync without running * the full 27-entity sync. */ async refreshFlexibleAssetById(id: string | number): Promise { const client = getITGlueClient(); const item = await client.getRawSingle(`/flexible_assets/${id}`); if (!item) { throw new Error(`IT Glue refreshFlexibleAssetById ${id}: empty response`); } const a = item.attributes; await postgresClient.query( `INSERT INTO itg_flexible_assets (id, organization_id, organization_name, flexible_asset_type_id, flexible_asset_type_name, name, traits, archived, created_at, updated_at, synced_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,NOW()) ON CONFLICT (id) DO UPDATE SET organization_id=$2, organization_name=$3, flexible_asset_type_id=$4, flexible_asset_type_name=$5, name=$6, traits=$7, archived=$8, updated_at=$10, synced_at=NOW()`, [ item.id, a['organization-id'], a['organization-name'] || null, a['flexible-asset-type-id'], a['flexible-asset-type-name'] || null, a.name || null, JSON.stringify(a.traits || {}), a.archived ?? false, a['created-at'] || null, a['updated-at'] || null, ] ); } } let _instance: ITGlueSyncService | null = null; export function getITGlueSyncService(): ITGlueSyncService { if (!_instance) _instance = new ITGlueSyncService(); return _instance; }