Files
Hariom BalharaGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
b74dd3227c perf: Optimize DB calls and avoid N+1 queries in BookingAuditViewer (#26544)
* Integrate creation/rescheduling booking audit

* fix: add missing hostUserUuid to booking audit test data

Co-Authored-By: [email protected] <[email protected]>

* fix-ci

* feat: enhance booking audit with seat reference

- Added support for seat reference in booking audit actions.
- Updated localization for booking creation to include seat information.
- Modified relevant services to pass attendee seat ID during booking creation.

* fix: update test data to match schema requirements

- Add seatReferenceUid: null to default mock audit log data
- Add seatReferenceUid: null to multiple audit logs test case
- Convert SEAT_RESCHEDULED test data to use numeric timestamps instead of ISO strings

Co-Authored-By: [email protected] <[email protected]>

* Allow nullish seatReferenceUid

* feat: enhance booking audit to support rescheduledBy information

- Updated booking audit actions to include rescheduledBy details, allowing tracking of who rescheduled a booking.
- Refactored related services to accommodate the new rescheduledBy parameter in booking events.
- Adjusted type definitions and function signatures to reflect the changes in the booking audit context.

* Avoid possible run time issue

* Fix imoport path

* fix failing test due to merge from main\

* Pass useruuid

* Refactor getDisplayTitle method signatures to use _params for consistency across audit action services. Update IAuditActionService to include dbStore in GetDisplayTitleParams for improved data handling during title retrieval.

* fix: extend bulk-fetching optimization to actor and impersonator enrichment

- Collect actor userUuids (for USER type actors) in collectDataRequirements
- Collect impersonator UUIDs from log.context.impersonatedBy
- Update enrichActorInformation to use dbStore.getUserByUuid() for USER actors
- Update enrichImpersonator to use dbStore.getUserByUuid()
- Update tests to verify findByUuids is called with correct aggregated UUIDs

Co-Authored-By: [email protected] <[email protected]>

* feat: Add infrastructure for no-show audit integration

- Add Prisma migrations for SYSTEM source and NO_SHOW_UPDATED audit action
- Add NoShowUpdatedAuditActionService with array-based attendeesNoShow schema
- Update BookingAuditActionServiceRegistry to include NO_SHOW_UPDATED
- Update BookingAuditTaskConsumer and BookingAuditViewerService
- Add AttendeeRepository methods for no-show queries
- Update IAuditActionService interface with values array support
- Update locales with no-show audit translation keys

Co-Authored-By: [email protected] <[email protected]>

* fix: Add NO_SHOW_UPDATED to BookingAuditAction and SYSTEM to ActionSource types

Co-Authored-By: [email protected] <[email protected]>

* fix: Remove HOST_NO_SHOW_UPDATED and ATTENDEE_NO_SHOW_UPDATED from BookingAuditAction type to match Prisma schema

Co-Authored-By: [email protected] <[email protected]>

* fix: Update BookingAuditActionSchema to use NO_SHOW_UPDATED instead of HOST_NO_SHOW_UPDATED and ATTENDEE_NO_SHOW_UPDATED

Co-Authored-By: [email protected] <[email protected]>

* refactor: Remove deprecated no-show audit services and unify to NoShowUpdatedAuditActionService

- Delete HostNoShowUpdatedAuditActionService and AttendeeNoShowUpdatedAuditActionService
- Update BookingAuditProducerService.interface.ts to use queueNoShowUpdatedAudit
- Update BookingAuditTaskerProducerService.ts to use queueNoShowUpdatedAudit
- Update BookingEventHandlerService.ts to use onNoShowUpdated
- Add integration tests for NoShowUpdatedAuditActionService

Co-Authored-By: [email protected] <[email protected]>

* fix: Add data migration step for deprecated no-show enum values

Addresses Cubic AI review feedback (confidence 9/10): The migration now
includes an UPDATE statement to convert existing records using the
deprecated 'host_no_show_updated' or 'attendee_no_show_updated' enum
values to the new unified 'no_show_updated' value before the type cast.
This prevents migration failures if any existing data uses the old values.

Co-Authored-By: unknown <>

* fix: Use CASE expression in USING clause for enum migration

Fixes PostgreSQL error 'unsafe use of new value of enum type' by avoiding
the ADD VALUE statement and instead using a CASE expression in the ALTER
TABLE USING clause to convert deprecated enum values (host_no_show_updated,
attendee_no_show_updated) to the new unified value (no_show_updated) during
the type conversion.

Co-Authored-By: unknown <>

* fix: Remove dbStore usage from audit action services after merge

- Update CreatedAuditActionService to use userRepository.findByUuid directly
- Remove unused dbStore parameter from RescheduledAuditActionService
- Sync test file with prepare-for-no-show-audit branch

Co-Authored-By: [email protected] <[email protected]>

* fix: Restore dbStore bulk-fetching optimization pattern

- Add dbStore parameter and getDataRequirements method to IAuditActionService interface
- Update CreatedAuditActionService to use dbStore.getUserByUuid() for bulk-fetched data
- Update RescheduledAuditActionService to accept dbStore parameter
- Update BookingAuditViewerService to:
  - Collect data requirements from all audit logs
  - Bulk-fetch users with findByUuids() before enrichment
  - Pass dbStore to action services and enrichment methods
- Update tests to verify findByUuids is called for bulk-fetching optimization

Co-Authored-By: [email protected] <[email protected]>

* fix: Remove duplicate fireBookingEvents method in RecurringBookingService

Co-Authored-By: [email protected] <[email protected]>

* revert: Remove formatting-only changes in audit action services

Co-Authored-By: [email protected] <[email protected]>

* feat: Make getDataRequirements required and fetch rescheduled logs early

- Make getDataRequirements a required method in IAuditActionService interface
- Add getDataRequirements to all action services that didn't have it
- Fetch rescheduled logs early to include their data requirements in bulk fetch
- Update ReassignmentAuditActionService to use dbStore pattern instead of repository
- Update NoShowUpdatedAuditActionService to use dbStore pattern
- Update tests to use new dbStore pattern
- Handle invalid data gracefully in collectDataRequirements

Co-Authored-By: [email protected] <[email protected]>

* Remve unncessary variable

* feat: Implement Actor Strategy pattern for bulk-fetching attendees and credentials

- Add ActorStrategies.ts with strategy pattern for all 5 actor types
- Extend EnrichmentDataStore with StoredAttendee and StoredCredential types
- Add findByIds method to CredentialRepository for bulk-fetching
- Update BookingAuditViewerService to use strategy pattern:
  - Replace switch statement in enrichActorInformation with strategy dispatch
  - Update collectDataRequirements to collect attendeeIds and credentialIds
  - Update buildEnrichmentDataStore to bulk-fetch in parallel
- Update tests to use findByIds mock and reflect graceful handling of missing data

Co-Authored-By: [email protected] <[email protected]>

* fix: Restore error-throwing for actors with missing required IDs

- USER actor now throws error when userUuid is missing
- ATTENDEE actor now throws error when attendeeId is missing
- Updated tests to expect hasError: true for fallback logs

Co-Authored-By: [email protected] <[email protected]>

* fix: Restore original test assertions, only update mocks for bulk methods

Co-Authored-By: [email protected] <[email protected]>

* test: Add contract verification tests for getDataRequirements

Co-Authored-By: [email protected] <[email protected]>

* refactor: Clean up imports and improve type definitions in BookingAuditTaskConsumer and BookingAuditViewerService

- Consolidated import statements for better readability
- Enhanced type definitions for clarity and consistency
- Updated method signatures to use destructured parameters for improved maintainability

Co-Authored-By: [email protected] <[email protected]>

* test: Split contract verification tests into separate files per action service

Co-Authored-By: [email protected] <[email protected]>

* test: Rename contract test files to .test.ts and merge into existing test file

Co-Authored-By: [email protected] <[email protected]>

* feat: Add runtime validation to EnrichmentDataStore for undeclared data access

Co-Authored-By: [email protected] <[email protected]>

* refactor: Make declaredRequirements required and add ensureFetched helper

Co-Authored-By: [email protected] <[email protected]>

* refactor: Simplify EnrichmentDataStore with constructor + fetch() pattern

- Remove unused userIds and bookingUids from DataRequirements
- Remove unused getUserById and getBookingByUid methods
- Refactor constructor to accept requirements and repositories
- Add fetch() method that fetches data and populates maps
- Use Map keys as source of truth for declared requirements
- Update getters to throw when accessing undeclared data
- Add comprehensive unit tests for EnrichmentDataStore
- Update BookingAuditViewerService to use new API
- Update contractVerification.ts to remove unused tracking

Co-Authored-By: [email protected] <[email protected]>

* refactor: Clean up imports and improve type definitions in BookingAuditTaskConsumer and BookingAuditViewerService

- Consolidated import statements for better readability
- Enhanced type definitions for clarity and consistency
- Updated method signatures to use destructured parameters for improved maintainability

Co-Authored-By: [email protected] <[email protected]>

* fix: Include contextRequirements in mergeDataRequirements and add accessedData assertions

- Fix bug where contextRequirements (impersonator UUIDs) were not merged
- Add accessedData assertions to CreatedAuditActionService.test.ts
- Add accessedData assertions to ReassignmentAuditActionService.test.ts

Co-Authored-By: [email protected] <[email protected]>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-06 16:08:38 +05:30

191 lines
6.2 KiB
TypeScript

import type { IAttendeeRepository } from "@calcom/features/bookings/repositories/IAttendeeRepository";
import type { CredentialRepository } from "@calcom/features/credentials/repositories/CredentialRepository";
import type { UserRepository } from "@calcom/features/users/repositories/UserRepository";
/**
* Entity types stored in the enrichment data store
* These represent the minimal data needed for audit log enrichment
*/
export type StoredUser = {
id: number;
uuid: string;
name: string | null;
email: string;
avatarUrl: string | null;
};
export type StoredAttendee = {
id: number;
name: string;
email: string;
};
export type StoredCredential = {
id: number;
appId: string | null;
};
/**
* Data requirements that action services can declare
* Used to collect all identifiers needed before bulk fetching
*/
export type DataRequirements = {
userUuids?: string[];
attendeeIds?: number[];
credentialIds?: number[];
};
/**
* Mapper functions to ensure only defined properties are stored avoiding unnecessary memory usage
* These functions extract only the properties defined in StoredUser, StoredAttendee, and StoredCredential
* Accepts broader types that extend the stored types (allowing additional properties)
*/
function mapToStoredUser<T extends StoredUser>(user: T): StoredUser {
return {
id: user.id,
uuid: user.uuid,
name: user.name,
email: user.email,
avatarUrl: user.avatarUrl,
};
}
function mapToStoredAttendee<T extends StoredAttendee>(attendee: T): StoredAttendee {
return {
id: attendee.id,
name: attendee.name,
email: attendee.email,
};
}
function mapToStoredCredential<T extends StoredCredential>(credential: T): StoredCredential {
return {
id: credential.id,
appId: credential.appId,
};
}
/**
* Repository dependencies for fetching data
*/
export interface EnrichmentDataStoreRepositories {
userRepository: UserRepository;
attendeeRepository: IAttendeeRepository;
credentialRepository: CredentialRepository;
}
/**
* EnrichmentDataStore
*
* Holds pre-fetched data for audit log enrichment.
* Action services use this store instead of making individual DB queries,
* eliminating N+1 query problems.
*
* The store validates that accessed data was declared in requirements - throws an error
* if code tries to access data that wasn't declared (catches bugs at runtime).
*
* Usage:
* ```
* const store = new EnrichmentDataStore(requirements, repositories);
* await store.fetch();
* const user = store.getUserByUuid("uuid"); // throws if "uuid" wasn't declared
* ```
*/
export class EnrichmentDataStore {
private usersByUuid: Map<string, StoredUser | null> = new Map();
private attendeesById: Map<number, StoredAttendee | null> = new Map();
private credentialsById: Map<number, StoredCredential | null> = new Map();
constructor(
private requirements: DataRequirements,
private repositories: EnrichmentDataStoreRepositories
) {
// Pre-populate maps with null for all declared IDs
// This marks them as "declared" - the key exists, but data not yet fetched
for (const uuid of requirements.userUuids ?? []) {
this.usersByUuid.set(uuid, null);
}
for (const id of requirements.attendeeIds ?? []) {
this.attendeesById.set(id, null);
}
for (const id of requirements.credentialIds ?? []) {
this.credentialsById.set(id, null);
}
}
/**
* Fetch all declared data from the database
* Must be called before using any getter methods
*/
async fetch(): Promise<void> {
const [users, attendees, credentials] = await Promise.all([
this.requirements.userUuids?.length
? this.repositories.userRepository.findByUuids({ uuids: this.requirements.userUuids })
: [],
this.requirements.attendeeIds?.length
? this.repositories.attendeeRepository.findByIds({ ids: this.requirements.attendeeIds })
: [],
this.requirements.credentialIds?.length
? this.repositories.credentialRepository.findByIds({ ids: this.requirements.credentialIds })
: [],
]);
// Overwrite nulls with actual data from DB, using mappers to ensure only defined properties are stored
for (const user of users) {
this.usersByUuid.set(user.uuid, mapToStoredUser(user));
}
for (const attendee of attendees) {
this.attendeesById.set(attendee.id, mapToStoredAttendee(attendee));
}
for (const credential of credentials) {
this.credentialsById.set(credential.id, mapToStoredCredential(credential));
}
}
/**
* Get user by UUID
* Throws error if UUID was not declared in requirements (bug in getDataRequirements)
* Returns null if user was declared but doesn't exist in database
*/
getUserByUuid(uuid: string): StoredUser | null {
if (!this.usersByUuid.has(uuid)) {
throw new Error(
`EnrichmentDataStore: getUserByUuid("${uuid}") called but was not declared in getDataRequirements. ` +
`This is a bug - ensure the action service declares all required userUuids.`
);
}
return this.usersByUuid.get(uuid) ?? null;
}
/**
* Get attendee by ID
* Throws error if ID was not declared in requirements (bug in getDataRequirements)
* Returns null if attendee was declared but doesn't exist in database
*/
getAttendeeById(id: number): StoredAttendee | null {
if (!this.attendeesById.has(id)) {
throw new Error(
`EnrichmentDataStore: getAttendeeById(${id}) called but was not declared in getDataRequirements. ` +
`This is a bug - ensure the action service declares all required attendeeIds.`
);
}
return this.attendeesById.get(id) ?? null;
}
/**
* Get credential by ID
* Throws error if ID was not declared in requirements (bug in getDataRequirements)
* Returns null if credential was declared but doesn't exist in database
*/
getCredentialById(id: number): StoredCredential | null {
if (!this.credentialsById.has(id)) {
throw new Error(
`EnrichmentDataStore: getCredentialById(${id}) called but was not declared in getDataRequirements. ` +
`This is a bug - ensure the action service declares all required credentialIds.`
);
}
return this.credentialsById.get(id) ?? null;
}
}