fix: prevent sync from nullifying assigned_resource_id on tickets

- bulkUpsert now accepts preserveExistingOnNull column list, using
  COALESCE(EXCLUDED.col, table.col) so null incoming values never
  overwrite existing non-null DB values
- bulkUpsertRecords passes resource ID columns as preserve-on-null
  for TICKETS and TASKS entities
- getValidResourceIds now throws on DB error instead of returning
  empty set (which would nullify every resource reference)
- Fix mimecast mailbox-remediate fetch handlers to check res.ok and
  content-type before calling res.json(), preventing JSON parse crash
  on 502 Bad Gateway responses
This commit is contained in:
lorentz 2026-04-05 08:55:41 -04:00
parent 04a720f0d0
commit 0e8eb4871e
4 changed files with 40 additions and 26 deletions

View file

@ -253,14 +253,16 @@ export class EntitySyncService {
const validResourceIds = await this.getValidResourceIds();
// Filter tickets with invalid resource references
let skippedResourceCount = 0;
mappedRecords = mappedRecords.map(ticket => {
// Set invalid resource IDs to null instead of filtering out the entire ticket
// Set to null — bulkUpsert uses COALESCE for these columns so existing DB value is preserved
if (ticket.assigned_resource_id && !validResourceIds.has(ticket.assigned_resource_id)) {
entityLogger.debug(`Invalid assigned_resource_id, setting to null`, {
entityLogger.debug(`Unknown assigned_resource_id, will preserve existing DB value via COALESCE`, {
ticketId: ticket.id,
invalidResourceId: ticket.assigned_resource_id,
unknownResourceId: ticket.assigned_resource_id,
});
ticket.assigned_resource_id = null;
skippedResourceCount++;
}
if (ticket.first_response_assigned_resource_id && !validResourceIds.has(ticket.first_response_assigned_resource_id)) {
ticket.first_response_assigned_resource_id = null;
@ -271,9 +273,8 @@ export class EntitySyncService {
return ticket;
});
const nullifiedCount = initialCount - mappedRecords.filter(t => t.assigned_resource_id).length;
if (nullifiedCount > 0) {
entityLogger.warn('Nullified invalid resource references', { nullifiedCount });
if (skippedResourceCount > 0) {
entityLogger.warn('Skipped unknown resource references (existing DB values preserved via COALESCE)', { skippedResourceCount });
}
}
@ -595,7 +596,7 @@ export class EntitySyncService {
chunkLogger.info('Cached valid resource IDs', { cacheSize: this.cachedValidResourceIds.size });
}
// Nullify invalid resource references
// Null out unknown resource references — bulkUpsert uses COALESCE so existing DB value is preserved
mappedRecords = mappedRecords.map(ticket => {
if (ticket.assigned_resource_id && !this.cachedValidResourceIds!.has(ticket.assigned_resource_id)) {
ticket.assigned_resource_id = null;
@ -670,16 +671,9 @@ export class EntitySyncService {
* @returns Set of valid resource IDs
*/
private async getValidResourceIds(): Promise<Set<number>> {
try {
const query = 'SELECT id FROM resources WHERE is_deleted = false';
const result = await postgresClient.query<{ id: number }>(query);
return new Set(result.rows.map(row => row.id));
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
this.logger.error('Failed to fetch valid resource IDs', {}, err);
// Return empty set on error - will cause all resource IDs to be nullified
return new Set();
}
const query = 'SELECT id FROM resources WHERE is_deleted = false';
const result = await postgresClient.query<{ id: number }>(query);
return new Set(result.rows.map(row => Number(row.id)));
}
/**
@ -690,7 +684,7 @@ export class EntitySyncService {
try {
const query = 'SELECT id FROM contacts WHERE is_deleted = false';
const result = await postgresClient.query<{ id: number }>(query);
return new Set(result.rows.map(row => row.id));
return new Set(result.rows.map(row => Number(row.id)));
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
this.logger.error('Failed to fetch valid contact IDs', {}, err);
@ -708,7 +702,7 @@ export class EntitySyncService {
try {
const query = 'SELECT id FROM projects WHERE is_deleted = false';
const result = await postgresClient.query<{ id: number }>(query);
return new Set(result.rows.map(row => row.id));
return new Set(result.rows.map(row => Number(row.id)));
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
this.logger.error('Failed to fetch valid project IDs', {}, err);