diff --git a/src/models/userEntry.ts b/src/models/userEntry.ts index be9a61cff..5f2be6360 100644 --- a/src/models/userEntry.ts +++ b/src/models/userEntry.ts @@ -4,6 +4,8 @@ export class UserEntry extends Entry { email: string; disabled = false; deleted = false; + creationDate: Date = null; + deletionDate: Date = null; get displayName(): string { if (this.email == null) { @@ -12,4 +14,18 @@ export class UserEntry extends Entry { return this.email; } + + get relevantDate(): Date { + if (this.deleted) { + return this.deletionDate; + } + return this.creationDate; + } + + newerThan(other: UserEntry): boolean { + if (this.relevantDate != null && other.relevantDate != null) { + return this.relevantDate > other.relevantDate; + } + return false; + } } diff --git a/src/services/gsuite-directory.service.ts b/src/services/gsuite-directory.service.ts index 895c657b1..cc2bdae62 100644 --- a/src/services/gsuite-directory.service.ts +++ b/src/services/gsuite-directory.service.ts @@ -143,6 +143,8 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir entry.email = user.primaryEmail != null ? user.primaryEmail.trim().toLowerCase() : null; entry.disabled = user.suspended || false; entry.deleted = deleted; + entry.creationDate = user.creationTime != null ? new Date(user.creationTime) : null; + entry.deletionDate = user.deletionTime != null ? new Date(user.deletionTime) : null; return entry; } diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index e9128cde3..4b1962574 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -138,33 +138,37 @@ export class SyncService { return null; } + const userMap = new Map(); const uniqueUsers = new Array(); - const processedActiveUsers = new Map(); - const processedDeletedUsers = new Map(); const duplicateEmails = new Array(); - // UserEntrys with the same email are ignored if their properties are the same - // UserEntrys with the same email but different properties will throw an error, unless they are all in a deleted state. + // Map users by email address users.forEach((u) => { - if (processedActiveUsers.has(u.email)) { - if (processedActiveUsers.get(u.email) !== JSON.stringify(u)) { - duplicateEmails.push(u.email); - } - } else { - if (!u.deleted) { - // Check that active UserEntry does not conflict with a deleted UserEntry - if (processedDeletedUsers.has(u.email)) { + userMap.set(u.email, userMap.get(u.email) || []); + userMap.get(u.email).push(u); + }); + + // We only care about the most recent entry. If there are multiple entries for the same email, all except the most recent must be either + // deleted, or have identical properties. + userMap.forEach((us) => { + // If there are multiple entries, we want to process the newest one first. + us = us.sort((a, b) => { return a.newerThan(b) ? -1 : 1; }); + const [head, ...tail] = us; + uniqueUsers.push(head); + const comparison = JSON.stringify(head); + tail.forEach((u) => { + if (head.deleted) { + // If the latest entry is deleted, all other entries also must be deleted + if (!u.deleted) { duplicateEmails.push(u.email); - } else { - processedActiveUsers.set(u.email, JSON.stringify(u)); - uniqueUsers.push(u); } } else { - // UserEntrys with duplicate email will not throw an error if they are all deleted. They will be synced. - processedDeletedUsers.set(u.email, JSON.stringify(u)); - uniqueUsers.push(u); + // If the latest entry is active, all other entries must be deleted, or identical. + if (!u.deleted && comparison !== JSON.stringify(u)) { + duplicateEmails.push(u.email); + } } - } + }); }); if (duplicateEmails.length > 0) {