Skip to content

Repository Data Cleaning

Problem

You need to clean and validate data from GraphQL responses before providing it to the UI, handling missing fields and providing consistent defaults.

The repository layer handles data cleaning, validation, and exception handling:

class HotelBookingsRepository
with
PaginationRepository<
PaginatedHotelBookingsEntity,
HotelBookingFilterCriteria,
HotelBookingSortCriteria
>
implements
QueryableRepository<
PaginatedHotelBookingsEntity,
HotelBookingsEntity,
HotelBookingFilterCriteria,
HotelBookingSortCriteria
>,
WritableRepository<HotelBookingsEntity, HotelBookingsUpsertVars> {
const HotelBookingsRepository({required this.datasource});
final HotelBookingsDatasource datasource;
@override
Stream<PaginatedHotelBookingsEntity> queryList({
int? skip,
int? take,
HotelBookingFilterCriteria? filter,
HotelBookingSortCriteria? sort,
Object? context,
}) {
return datasource
.queryList(
skip: skip,
take: take,
filter: filter?.toGql,
sort: sort?.toGql,
)
.map((response) => response.data?.hotelbookings)
.map(convertToPaginatedHotelBookings);
}
@override
Stream<HotelBookingsEntity?> queryOne({
required HotelBookingFilterCriteria filter,
Object? context,
}) {
return datasource
.queryOne(filter: filter.toGql)
.map((response) => response.data?.hotelbookings)
.map(convertToHotelBooking);
}
}

Implement robust data conversion with proper error handling:

HotelBookingsEntity? _convertToHotelBookingEntity(
GQueryHotelBookingsData_hotelbookings_hotelbookingsItems? booking,
) {
if (booking == null) return null;
try {
final bookingNumber = booking.bookingNumber;
final roomNumber = booking.roomNumber;
final roomCapacity = booking.roomCapacity;
final validRoomCapacity = (roomCapacity == null || roomCapacity.isEmpty)
? '1'
: roomCapacity;
final bookingStatus = booking.bookingStatus?.firstOrNull;
final bookingStatusEntity =
bookingStatus?.toEntity() ??
HotelBookingStatusEntity(
id: 'unknown',
title: 'Unknown Status',
status: HotelBookingStatusEnum.requested,
);
final roomType = booking.roomType?.firstOrNull;
final roomTypeEntity =
roomType?.toEntity() ??
RoomTypeEntity(id: 'unknown', title: 'Unknown Room Type');
final hotel = booking.hotel?.firstOrNull;
final hotelEntity =
hotel?.toEntity() ??
HotelEntity(id: 'unknown', title: 'Unknown Hotel', roomTypes: []);
final billingInstruction = booking.billingInstruction?.firstOrNull;
final billingInstructionEntity =
billingInstruction?.toEntity() ??
BillingInstructionsEntity(
id: 'unknown',
title: 'Unknown Billing Instruction',
);
final bookingFiles = booking.bookingFiles?.toList() ?? [];
final bookingFilesEntities = bookingFiles
.map(
(file) => AttachementEntity(
id: file?.id,
mediaUrl: file?.mediaUrl,
title: file?.title,
),
)
.toList();
return HotelBookingsEntity(
id: booking.id,
title: booking.title,
bookingNumber: bookingNumber,
roomNumber: roomNumber,
bookingStatus: bookingStatusEntity,
roomType: roomTypeEntity,
guests: booking.guests!
.map((guest) => guest!.toEntity())
.whereType<GuestEntity>()
.toList(),
roomCapacity: validRoomCapacity,
checkIn: booking.checkIn?.toDto,
checkOut: booking.checkOut?.toDto,
billingInstruction: billingInstructionEntity,
hotel: hotelEntity,
bookingFiles: bookingFilesEntities,
nights: booking.checkIn?.toDto?.nights(booking.checkOut?.toDto) ?? 0,
guestsToRoommCapacityRatio:
booking.guests!.length ~/ (int.tryParse(validRoomCapacity) ?? 1),
);
} catch (e) {
print('Error converting hotel booking: $e');
return null;
}
}

Handle mutation responses with proper validation:

HotelBookingsEntity? convertFromMutationToHotelBooking(
GMutateHotelBookingsSaveData_hotelbookingsSave? response,
) {
if (response == null) return null;
final id = response.id;
if (id == null) return null;
try {
final roomCapacity = response.roomCapacity;
final validRoomCapacity = (roomCapacity == null || roomCapacity.isEmpty)
? '1'
: roomCapacity;
final bookingStatus = response.bookingStatus?.firstOrNull;
final roomType = response.roomType?.firstOrNull;
final hotel = response.hotel?.firstOrNull;
final billingInstruction = response.billingInstruction?.firstOrNull;
final guests = response.guests?.toList() ?? [];
final bookingFiles = response.bookingFiles?.toList() ?? [];
final bookingFilesEntities = bookingFiles
.map(
(file) => AttachementEntity(
id: file?.id,
mediaUrl: file?.mediaUrl,
title: file?.title,
),
)
.toList();
return HotelBookingsEntity(
id: id,
title: response.title,
bookingNumber: response.bookingNumber,
roomNumber: response.roomNumber,
bookingFiles: bookingFilesEntities,
bookingStatus:
bookingStatus != null &&
bookingStatus.id != null &&
bookingStatus.title != null
? HotelBookingStatusEntity(
id: bookingStatus.id!,
title: bookingStatus.title!,
status: _mapTitleToStatusEnum(bookingStatus.title!),
)
: HotelBookingStatusEntity(
id: 'unknown',
title: 'Unknown Status',
status: HotelBookingStatusEnum.requested,
),
roomType:
roomType != null && roomType.id != null && roomType.title != null
? RoomTypeEntity(id: roomType.id!, title: roomType.title!)
: RoomTypeEntity(id: 'unknown', title: 'Unknown Room Type'),
guests: guests
.map(
(guest) => guest != null
? GuestEntity(
id: guest.id!,
fullName: '${guest.firstName} ${guest.lastName}',
firstName: guest.firstName!,
lastName: guest.lastName!,
jobTitle: guest.jobTitle ?? '',
organization: guest.organizationName ?? '',
)
: null,
)
.whereType<GuestEntity>()
.toList(),
roomCapacity: validRoomCapacity,
checkIn: response.checkIn?.toDto,
checkOut: response.checkOut?.toDto,
billingInstruction:
billingInstruction != null &&
billingInstruction.id != null &&
billingInstruction.title != null
? BillingInstructionsEntity(
id: billingInstruction.id!,
title: billingInstruction.title!,
)
: BillingInstructionsEntity(
id: 'unknown',
title: 'Unknown Billing Instruction',
),
hotel: hotel != null && hotel.id != null && hotel.title != null
? HotelEntity(
id: hotel.id!,
title: hotel.title!,
roomTypes:
hotel.roomTypes
?.map(
(rt) => RoomTypeEntity(id: rt!.id!, title: rt.title!),
)
.toList() ??
[],
)
: HotelEntity(id: 'unknown', title: 'Unknown Hotel', roomTypes: []),
nights: response.checkIn?.toDto?.nights(response.checkOut?.toDto) ?? 0,
guestsToRoommCapacityRatio:
guests.length ~/ (int.tryParse(validRoomCapacity) ?? 1),
notes: response.notes,
);
} catch (e) {
print('Error converting hotel booking from mutation: $e');
return null;
}
}

Use extensions for consistent data cleaning:

extension StringExtensions on String? {
String get NA {
if (this == null || this!.trim().isEmpty) return 'N/A';
return this!;
}
String get emptyIfNull {
return this ?? '';
}
bool get isNullOrEmpty {
return this == null || this!.trim().isEmpty;
}
}
extension DateTimeExtensions on DateTime? {
int nights(DateTime? endDate) {
if (this == null || endDate == null) return 0;
return endDate.difference(this!).inDays;
}
String get formattedDate {
if (this == null) return 'N/A';
return DateFormat('MMM dd, yyyy').format(this!);
}
}
extension ListExtensions on List? {
bool get isNullOrEmpty => this == null || this!.isEmpty;
int get safeLength => this?.length ?? 0;
}

Implement proper exception handling in repository methods:

@override
Stream<HotelBookingsEntity> upsert({
required HotelBookingsUpsertVars vars,
Object? context,
}) {
return datasource.upsert(vars: vars.toMutationVars).map((response) {
final result = convertFromMutationToHotelBooking(
response.data?.hotelbookingsSave,
);
if (result == null) {
throw HotelBookingUpsertException(
code: CRUDExceptionCode.NO_DATA,
stackTrace: StackTrace.current.toString(),
originalException: Exception(
'No data received from server $vars, $result',
),
);
}
return result;
});
}

Common validation patterns for data cleaning:

class DataValidator {
static String validateAndCleanString(String? value, String defaultValue) {
if (value == null || value.trim().isEmpty) {
return defaultValue;
}
return value.trim();
}
static int validatePositiveInt(int? value, int defaultValue) {
if (value == null || value <= 0) {
return defaultValue;
}
return value;
}
static List<T> validateList<T>(List<T>? list) {
return list?.where((item) => item != null).toList() ?? [];
}
}