wulf-pulse/lib/services/itglue-sync-service.ts

554 lines
25 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;
}
}
let _instance: ITGlueSyncService | null = null;
export function getITGlueSyncService(): ITGlueSyncService {
if (!_instance) _instance = new ITGlueSyncService();
return _instance;
}