- RMM Overshell (migration 077): admin page, dispatch UI, executor/worker, target resolver, script registry (AD/DHCP/DNS/event-log/services/software/network/loglift) - LogLift evidence pipeline (migration 078): upload webhook, B2 storage client, receiver/matcher, EventLogCollector PowerShell script - IT Glue audit + write-back (migrations 075, 076): asset-audit runner, ticket xrefs, applications/configurations browse pages + apply/revert/audit endpoints - Link-aware analyzer bundles (migration 073) + provider toggle (migration 074): link-discovery service, OpenRouter LLM provider, related-tickets/itglue-suggestion panels, analyze-bundle endpoint - Endpoint data model + device-link reconciliation (migrations 079, 080): conflicts admin page, reconciler service, resolve endpoints - Dashboard overhaul: integration-health service + alerts, overview/health endpoints - Permissions: add itglue + rmm scopes; middleware: public /api/rmm/loglift route Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
663 lines
30 KiB
TypeScript
663 lines
30 KiB
TypeScript
/**
|
|
* 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<ITGlueSyncResult> {
|
|
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<number>) => {
|
|
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<number> {
|
|
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<number> {
|
|
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<number> {
|
|
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<number> {
|
|
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<number> {
|
|
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<number> {
|
|
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<number> {
|
|
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<number> {
|
|
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<number> {
|
|
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<number> {
|
|
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<number> {
|
|
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<number> {
|
|
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<number> {
|
|
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<number> {
|
|
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<number> {
|
|
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<number> {
|
|
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<void> {
|
|
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<void> {
|
|
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;
|
|
}
|