PSA/shared/rmm/sharedAssetIngestionService.ts
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

383 lines
12 KiB
TypeScript

import type { Knex } from 'knex';
import { createTenantKnex } from '@alga-psa/db';
import type {
NormalizedRmmDeviceType,
NormalizedRmmExternalDeviceSnapshot,
NormalizedRmmIngestionResult,
} from './contracts';
type SupportedAssetType = Extract<NormalizedRmmDeviceType, 'workstation' | 'server' | 'network_device' | 'mobile_device' | 'unknown'>;
export interface IngestNormalizedRmmDeviceSnapshotInput {
tenant: string;
snapshot: NormalizedRmmExternalDeviceSnapshot;
resolvedClientId?: string | null;
knex?: Knex;
}
type ExternalMappingRow = {
id: string;
alga_entity_id: string;
external_realm_id?: string | null;
};
type ExistingAssetRow = {
asset_id: string;
asset_type: SupportedAssetType;
client_id?: string | null;
};
function parseIsoDate(value?: string | null): Date | null {
if (!value) return null;
const parsed = new Date(value);
return Number.isNaN(parsed.getTime()) ? null : parsed;
}
function normalizeAssetType(assetType: NormalizedRmmDeviceType): SupportedAssetType {
if (assetType === 'workstation' || assetType === 'server' || assetType === 'network_device' || assetType === 'mobile_device') {
return assetType;
}
return 'unknown';
}
function pickExtensionTable(assetType: SupportedAssetType): 'workstation_assets' | 'server_assets' | null {
if (assetType === 'server') return 'server_assets';
if (assetType === 'workstation') return 'workstation_assets';
return null;
}
async function resolveClientIdForScope(
trx: Knex.Transaction,
args: { tenant: string; integrationId: string; externalScopeId: string; resolvedClientId?: string | null; }
): Promise<string | null> {
if (args.resolvedClientId) return args.resolvedClientId;
const mapping = await trx('rmm_organization_mappings')
.where({
tenant: args.tenant,
integration_id: args.integrationId,
external_organization_id: args.externalScopeId,
})
.first<{ client_id?: string | null }>('client_id');
return mapping?.client_id ? String(mapping.client_id) : null;
}
async function upsertAssetExtension(
trx: Knex.Transaction,
args: { tenant: string; assetId: string; assetType: SupportedAssetType; snapshot: NormalizedRmmExternalDeviceSnapshot; }
): Promise<void> {
const table = pickExtensionTable(args.assetType);
if (!table) return;
const ext = args.snapshot.extension ?? {};
const patch: Record<string, unknown> = {
os_type: ext.osType ?? null,
os_version: ext.osVersion ?? null,
agent_version: ext.agentVersion ?? null,
current_user: ext.currentUser ?? null,
uptime_seconds: ext.uptimeSeconds ?? null,
lan_ip: ext.lanIp ?? null,
wan_ip: ext.wanIp ?? null,
antivirus_status: ext.antivirusStatus ?? null,
antivirus_product: ext.antivirusProduct ?? null,
last_reboot_at: parseIsoDate(ext.lastRebootAt),
pending_patches: ext.pendingPatches ?? null,
pending_os_patches: ext.pendingOsPatches ?? null,
pending_software_patches: ext.pendingSoftwarePatches ?? null,
failed_patches: ext.failedPatches ?? null,
last_patch_scan_at: parseIsoDate(ext.lastPatchScanAt),
system_info: ext.systemInfo ? JSON.stringify(ext.systemInfo) : null,
};
if (typeof ext.cpuModel !== 'undefined') {
patch.cpu_model = ext.cpuModel ?? null;
}
if (typeof ext.cpuCores !== 'undefined') {
patch.cpu_cores = ext.cpuCores ?? null;
}
if (typeof ext.ramGb !== 'undefined') {
patch.ram_gb = ext.ramGb ?? null;
}
if (typeof ext.diskUsage !== 'undefined') {
patch.disk_usage = ext.diskUsage ? JSON.stringify(ext.diskUsage) : null;
}
if (typeof ext.installedSoftware !== 'undefined') {
patch.installed_software = JSON.stringify(ext.installedSoftware ?? []);
}
await trx(table)
.insert({
tenant: args.tenant,
asset_id: trx.raw('?::uuid', [args.assetId]),
...patch,
})
.onConflict(['tenant', 'asset_id'])
.merge(patch);
}
async function markMappedAssetDeleted(
trx: Knex.Transaction,
args: {
tenant: string;
provider: string;
mappingId: string;
assetId: string;
externalDeviceId: string;
externalScopeId: string;
metadata?: Record<string, unknown>;
}
): Promise<NormalizedRmmIngestionResult> {
const now = new Date().toISOString();
await trx('assets')
.where({ tenant: args.tenant, asset_id: args.assetId })
.update({
status: 'inactive',
agent_status: 'offline',
last_rmm_sync_at: trx.fn.now(),
updated_at: now,
});
await trx('tenant_external_entity_mappings')
.where({ tenant: args.tenant, id: args.mappingId })
.update({
sync_status: 'error',
last_synced_at: trx.fn.now(),
metadata: {
...(args.metadata ?? {}),
deleted: true,
deletedAt: now,
},
updated_at: now,
});
return {
externalDeviceId: args.externalDeviceId,
assetId: args.assetId,
action: 'marked_deleted',
};
}
export async function ingestNormalizedRmmDeviceSnapshot(
input: IngestNormalizedRmmDeviceSnapshotInput
): Promise<NormalizedRmmIngestionResult> {
const { tenant, snapshot } = input;
const knex = input.knex ?? (await createTenantKnex()).knex;
const assetType = normalizeAssetType(snapshot.assetType);
return knex.transaction(async (trx) => {
const exactMapping = await trx('tenant_external_entity_mappings')
.where({
tenant,
integration_type: snapshot.provider,
alga_entity_type: 'asset',
external_entity_id: snapshot.externalDeviceId,
external_realm_id: snapshot.externalScopeId,
})
.first<ExternalMappingRow>('id', 'alga_entity_id', 'external_realm_id');
const anyRealmMapping = exactMapping
? null
: await trx('tenant_external_entity_mappings')
.where({
tenant,
integration_type: snapshot.provider,
alga_entity_type: 'asset',
external_entity_id: snapshot.externalDeviceId,
})
.first<ExternalMappingRow>('id', 'alga_entity_id', 'external_realm_id');
const existingMapping = exactMapping ?? anyRealmMapping;
if (snapshot.lifecycleState === 'deleted' || snapshot.lifecycleState === 'tombstoned') {
if (!existingMapping?.alga_entity_id) {
return {
externalDeviceId: snapshot.externalDeviceId,
action: 'skipped',
error: 'No existing mapping to mark as deleted',
};
}
return markMappedAssetDeleted(trx, {
tenant,
provider: snapshot.provider,
mappingId: existingMapping.id,
assetId: String(existingMapping.alga_entity_id),
externalDeviceId: snapshot.externalDeviceId,
externalScopeId: snapshot.externalScopeId,
metadata: snapshot.metadata,
});
}
let existingAsset: ExistingAssetRow | null = null;
let mappingId: string | null = null;
if (existingMapping?.alga_entity_id) {
existingAsset = await trx('assets')
.where({ tenant, asset_id: existingMapping.alga_entity_id })
.first<ExistingAssetRow>('asset_id', 'asset_type', 'client_id');
mappingId = existingMapping.id;
}
if (!existingAsset) {
existingAsset = await trx('assets')
.where({
tenant,
rmm_provider: snapshot.provider,
rmm_device_id: snapshot.externalDeviceId,
})
.first<ExistingAssetRow>('asset_id', 'asset_type', 'client_id');
}
if (!mappingId && existingAsset?.asset_id) {
const assetScopedMapping = await trx('tenant_external_entity_mappings')
.where({
tenant,
integration_type: snapshot.provider,
alga_entity_type: 'asset',
alga_entity_id: existingAsset.asset_id,
})
.first<ExternalMappingRow>('id', 'alga_entity_id', 'external_realm_id');
if (assetScopedMapping?.id) {
mappingId = assetScopedMapping.id;
}
}
const lastSeenAt = parseIsoDate(snapshot.lastSeenAt);
const assetStatus = snapshot.status || (snapshot.lifecycleState === 'offline' ? 'inactive' : 'active');
const agentStatus = snapshot.agentStatus ?? (snapshot.lifecycleState === 'offline' ? 'offline' : 'online');
const resolvedClientId = await resolveClientIdForScope(trx, {
tenant,
integrationId: snapshot.integrationId,
externalScopeId: snapshot.externalScopeId,
resolvedClientId: input.resolvedClientId,
});
if (existingAsset?.asset_id) {
const assetId = String(existingAsset.asset_id);
const assetPatch: Record<string, unknown> = {
asset_type: assetType,
name: snapshot.displayName,
serial_number: snapshot.serialNumber ?? '',
status: assetStatus,
location: snapshot.location ?? '',
rmm_provider: snapshot.provider,
rmm_device_id: snapshot.externalDeviceId,
rmm_organization_id: snapshot.externalScopeId,
agent_status: agentStatus,
last_seen_at: lastSeenAt,
last_rmm_sync_at: trx.fn.now(),
};
if (resolvedClientId) {
assetPatch.client_id = resolvedClientId;
}
await trx('assets')
.where({ tenant, asset_id: assetId })
.update(assetPatch);
await upsertAssetExtension(trx, {
tenant,
assetId,
assetType,
snapshot,
});
if (mappingId) {
await trx('tenant_external_entity_mappings')
.where({ tenant, id: mappingId })
.update({
external_realm_id: snapshot.externalScopeId,
sync_status: 'synced',
last_synced_at: trx.fn.now(),
metadata: snapshot.metadata ?? {},
});
} else {
await trx('tenant_external_entity_mappings').insert({
tenant,
integration_type: snapshot.provider,
alga_entity_type: 'asset',
alga_entity_id: assetId,
external_entity_id: snapshot.externalDeviceId,
external_realm_id: snapshot.externalScopeId,
sync_status: 'synced',
last_synced_at: trx.fn.now(),
metadata: snapshot.metadata ?? {},
});
}
return {
externalDeviceId: snapshot.externalDeviceId,
assetId,
action: 'updated',
};
}
if (!resolvedClientId) {
return {
externalDeviceId: snapshot.externalDeviceId,
action: 'skipped',
error: `No mapped client for external scope ${snapshot.externalScopeId}`,
};
}
const now = new Date().toISOString();
const [createdAsset] = await trx('assets')
.insert({
tenant,
asset_type: assetType,
client_id: resolvedClientId,
asset_tag: snapshot.assetTag || `${snapshot.provider}:${snapshot.externalDeviceId}`,
serial_number: snapshot.serialNumber ?? '',
name: snapshot.displayName,
status: assetStatus,
location: snapshot.location ?? '',
rmm_provider: snapshot.provider,
rmm_device_id: snapshot.externalDeviceId,
rmm_organization_id: snapshot.externalScopeId,
agent_status: agentStatus,
last_seen_at: lastSeenAt,
last_rmm_sync_at: now,
created_at: now,
updated_at: now,
})
.returning<{ asset_id: string }[]>('asset_id');
const createdAssetId = String(createdAsset.asset_id);
await upsertAssetExtension(trx, {
tenant,
assetId: createdAssetId,
assetType,
snapshot,
});
await trx('tenant_external_entity_mappings').insert({
tenant,
integration_type: snapshot.provider,
alga_entity_type: 'asset',
alga_entity_id: createdAssetId,
external_entity_id: snapshot.externalDeviceId,
external_realm_id: snapshot.externalScopeId,
sync_status: 'synced',
last_synced_at: trx.fn.now(),
metadata: snapshot.metadata ?? {},
created_at: now,
updated_at: now,
});
return {
externalDeviceId: snapshot.externalDeviceId,
assetId: createdAssetId,
action: 'created',
};
});
}