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
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
116 lines
3.8 KiB
TypeScript
116 lines
3.8 KiB
TypeScript
import type { Knex } from 'knex';
|
|
import type { NormalizedRmmAlertEvent, RmmAlertRuleActions } from './contracts';
|
|
import { rmmAlertRuleActionsSchema } from './contracts';
|
|
import { createTicketForAlert, type CreatedAlertTicket } from './ticketCreator';
|
|
|
|
export interface CreateTicketForAlertIdArgs {
|
|
tenantId: string;
|
|
alertId: string;
|
|
overrides?: Partial<
|
|
Pick<RmmAlertRuleActions, 'boardId' | 'priorityOverride' | 'assignToUserId' | 'ticketTemplate'>
|
|
>;
|
|
}
|
|
|
|
/**
|
|
* Creates a ticket for an existing rmm_alerts row (manual button, workflow
|
|
* action). Resolves the client from the asset, falling back to the
|
|
* organization mapping, and links the alert to the created ticket.
|
|
*/
|
|
export async function createTicketForAlertId(
|
|
knex: Knex,
|
|
args: CreateTicketForAlertIdArgs
|
|
): Promise<CreatedAlertTicket> {
|
|
const { tenantId, alertId } = args;
|
|
|
|
const alert = await knex('rmm_alerts as a')
|
|
.join('rmm_integrations as i', function joinIntegrations() {
|
|
this.on('i.tenant', 'a.tenant').andOn('i.integration_id', 'a.integration_id');
|
|
})
|
|
.where('a.tenant', tenantId)
|
|
.andWhere('a.alert_id', alertId)
|
|
.first('a.*', 'i.provider');
|
|
if (!alert) {
|
|
throw new Error('Alert not found');
|
|
}
|
|
if (alert.ticket_id) {
|
|
throw new Error('Alert already has a linked ticket');
|
|
}
|
|
|
|
let clientId: string | null = null;
|
|
let organizationName: string | null = null;
|
|
if (alert.asset_id) {
|
|
const asset = await knex('assets')
|
|
.where({ tenant: tenantId, asset_id: alert.asset_id })
|
|
.first('client_id');
|
|
clientId = asset?.client_id ?? null;
|
|
}
|
|
|
|
const rawMetadata = typeof alert.metadata === 'string' ? safeParse(alert.metadata) : alert.metadata;
|
|
const externalOrgId =
|
|
rawMetadata && typeof rawMetadata === 'object' && 'organizationId' in rawMetadata
|
|
? String((rawMetadata as Record<string, unknown>).organizationId)
|
|
: null;
|
|
if (externalOrgId) {
|
|
const orgMapping = await knex('rmm_organization_mappings')
|
|
.where({
|
|
tenant: tenantId,
|
|
integration_id: alert.integration_id,
|
|
external_organization_id: externalOrgId,
|
|
})
|
|
.first('client_id', 'external_organization_name');
|
|
organizationName = orgMapping?.external_organization_name ?? null;
|
|
if (!clientId) clientId = orgMapping?.client_id ?? null;
|
|
}
|
|
if (!clientId) {
|
|
throw new Error('No client resolvable for this alert (unmapped asset and organization)');
|
|
}
|
|
|
|
const event: NormalizedRmmAlertEvent = {
|
|
tenantId,
|
|
integrationId: alert.integration_id,
|
|
provider: alert.provider,
|
|
kind: 'triggered',
|
|
externalAlertId: alert.external_alert_id,
|
|
externalDeviceId: alert.external_device_id,
|
|
activityType: alert.activity_type,
|
|
alertClass: alert.alert_class,
|
|
sourceType: alert.source_type,
|
|
severity: alert.severity,
|
|
message: alert.message,
|
|
deviceName: alert.device_name,
|
|
externalOrganizationId: externalOrgId,
|
|
occurredAt: toIso(alert.triggered_at) ?? new Date().toISOString(),
|
|
raw: (rawMetadata as Record<string, unknown>) ?? {},
|
|
};
|
|
const actions = rmmAlertRuleActionsSchema.parse({ createTicket: true, ...(args.overrides ?? {}) });
|
|
|
|
return knex.transaction(async (trx) => {
|
|
const created = await createTicketForAlert(trx, {
|
|
event,
|
|
actions,
|
|
clientId: clientId!,
|
|
assetId: alert.asset_id,
|
|
organizationName,
|
|
});
|
|
await trx('rmm_alerts')
|
|
.where({ tenant: tenantId, alert_id: alertId })
|
|
.update({ ticket_id: created.ticket_id, updated_at: new Date().toISOString() });
|
|
return created;
|
|
});
|
|
}
|
|
|
|
function safeParse(value: string): unknown {
|
|
try {
|
|
return JSON.parse(value);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function toIso(value: unknown): string | null {
|
|
if (!value) return null;
|
|
if (value instanceof Date) return value.toISOString();
|
|
const parsed = new Date(String(value));
|
|
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
|
|
}
|