Map Domain Models to GraphQL Types
Problem
You need to convert between domain models and GraphQL types while maintaining clean separation between domain and datasource packages.
Team Input Needed
Conversion extensions need better organization patterns to prevent missing fields in filters and GQL type mappings. Team discussion required for standardization.
Conversion Extension Pattern
Section titled âConversion Extension PatternâCreate extension methods to handle conversions between domain models and GraphQL types:
Generic Filter Extensions (Datasource Package)
Section titled âGeneric Filter Extensions (Datasource Package)âThese generic filter extensions are essential for all projects and exist in the datasource package:
String Filter Extensions
Section titled âString Filter Extensionsâextension GStringFiltersBuilderToGql on GStringFiltersBuilder { GStringFiltersBuilder? construct({ String? contains, String? equals, String? startsWith, String? endsWith, String? nullOrEmpty, }) { if (contains == null && equals == null && startsWith == null && endsWith == null && nullOrEmpty == null) { return null; } return GStringFilters( (b) => b ..G_contains = contains ..G_eq = equals ..G_startswith = startsWith ..G_endswith = endsWith ..G_null_or_empty = nullOrEmpty, ).toBuilder(); }}Related Filters Extensions
Section titled âRelated Filters Extensionsâextension GRelatedFiltersDtoExtension on GRelatedFiltersBuilder { GRelatedFiltersBuilder? construct({ List<String?>? anyIds, List<String?>? allIds, List<String?>? anyTitles, List<String?>? allTitles, }) { final filteredAnyIds = anyIds?.nonNulls.toList(); final filteredAnyTitles = anyTitles?.nonNulls.toList(); final filteredAllIds = allIds?.nonNulls.toList(); final filteredAllTitles = allTitles?.nonNulls.toList();
if ((filteredAnyIds?.isNotEmpty != true) && (filteredAllIds?.isNotEmpty != true) && (filteredAnyTitles?.isNotEmpty != true) && (filteredAllTitles?.isNotEmpty != true)) { return null; }
final builder = GRelatedFiltersBuilder();
if (filteredAnyIds?.isNotEmpty == true || filteredAnyTitles?.isNotEmpty == true) { builder.G_any = ListBuilder([ ...?filteredAnyIds?.map((e) => GrelatedFilter((b) => b.id = e)), ...?filteredAnyTitles?.map((e) => GrelatedFilter((b) => b.title = e)), ]); }
if (filteredAllIds?.isNotEmpty == true || filteredAllTitles?.isNotEmpty == true) { builder.G_all = ListBuilder([ ...?filteredAllIds?.map((e) => GrelatedFilter((b) => b.id = e)), ...?filteredAllTitles?.map((e) => GrelatedFilter((b) => b.title = e)), ]); }
return builder; }}Using Generic Extensions in Practice
Section titled âUsing Generic Extensions in PracticeâFilter Conversions with Generic Extensions
Section titled âFilter Conversions with Generic Extensionsâextension HotelBookingFilterCriteriaExtensions on HotelBookingFilterCriteria { GhotelbookingsFilters get toGql { return GhotelbookingsFilters( (b) => b ..id = id ..systemParentId = conferenceId ..bookingNumber = GStringFiltersBuilder().construct( contains: bookingNumberContains, equals: bookingNumber, startsWith: bookingNumberStartsWith, ) ..roomNumber = GStringFiltersBuilder().construct( contains: roomNumberContains, equals: roomNumber, ) ..bookingStatus = GRelatedFiltersBuilder().construct( anyIds: bookingStatusIds, anyTitles: bookingStatusTitles, ) ..hotel = GRelatedFiltersBuilder().construct( anyIds: hotelIds, anyTitles: hotelTitles, ), ); }}Sort Criteria Conversions
Section titled âSort Criteria Conversionsâextension HotelBookingSortCriteriaExtensions on HotelBookingSortCriteria { GhotelbookingsOrder get toGql { return GhotelbookingsOrder( (b) => b ..createdAt = createdAt?.toGql ..updatedAt = updatedAt?.toGql ..bookingNumber = bookingNumber?.toGql ..checkIn = checkIn?.toGql ..checkOut = checkOut?.toGql, ); }}
extension SortDirectionExtensions on SortDirection { GSortEnumType get toGql { switch (this) { case SortDirection.asc: return GSortEnumType.ASC; case SortDirection.desc: return GSortEnumType.DESC; } }}Mutation Variables Conversions
Section titled âMutation Variables Conversionsâextension HotelBookingsUpsertVarsExtensions on HotelBookingsUpsertVars { GMutateHotelBookingsSaveVars get toMutationVars { return GMutateHotelBookingsSaveVars( (b) => b ..hotelBookingsArgs = GhotelBookingsArgs( (args) => args ..id = id ..title = title ..bookingNumber = bookingNumber ..roomNumber = roomNumber ..roomCapacity = roomCapacity ..checkIn = checkIn?.toGqlDateTime ..checkOut = checkOut?.toGqlDateTime ..notes = notes ..bookingStatus = bookingStatusId != null ? GRelatedArgs( (rb) => rb ..connect.replace([ GrelatedArg((fb) => fb..id = bookingStatusId!), ]), ).toBuilder() : null ..roomType = roomTypeId != null ? GRelatedArgs( (rb) => rb ..connect.replace([ GrelatedArg((fb) => fb..id = roomTypeId!), ]), ).toBuilder() : null ..hotel = hotelId != null ? GRelatedArgs( (rb) => rb ..connect.replace([ GrelatedArg((fb) => fb..id = hotelId!), ]), ).toBuilder() : null, ).toBuilder(), ); }}Entity Conversions from GraphQL
Section titled âEntity Conversions from GraphQLâextension HotelBookingStatusGqlExtensions on GQueryHotelBookingsData_hotelbookings_hotelbookingsItems_bookingStatus { HotelBookingStatusEntity toEntity() { return HotelBookingStatusEntity( id: id!, title: title!, status: _mapTitleToStatusEnum(title!), ); }
HotelBookingStatusEnum _mapTitleToStatusEnum(String title) { switch (title.toLowerCase()) { case 'canceled': return HotelBookingStatusEnum.canceled; case 'completed': return HotelBookingStatusEnum.completed; case 'requested': return HotelBookingStatusEnum.requested; case 'approved': return HotelBookingStatusEnum.approved; default: return HotelBookingStatusEnum.requested; } }}
extension GuestGqlExtensions on GQueryHotelBookingsData_hotelbookings_hotelbookingsItems_guests { GuestEntity toEntity() { return GuestEntity( id: id!, fullName: '${firstName} ${lastName}', firstName: firstName!, lastName: lastName!, jobTitle: jobTitle ?? '', organization: organizationName ?? '', ); }}DateTime Conversions
Section titled âDateTime Conversionsâextension DateTimeExtensions on DateTime { GDateTime get toGqlDateTime { return GDateTime(toIso8601String()); }}
extension GDateTimeExtensions on GDateTime { DateTime get toDto { return DateTime.parse(value); }}Domain Model Extensions (Domain Package)
Section titled âDomain Model Extensions (Domain Package)âThese essential extensions exist in the domain package and provide consistent data handling across all projects:
Date and Time Formatters
Section titled âDate and Time Formattersâenum DateFormatter { shortSlash('dd/MM/yyyy', '01/12/2023'), shortDash('dd-MM-yyyy', '01-12-2023'), shortDot('dd.MM.yyyy', '01.12.2023'), medium('MMM d, yyyy', 'Dec 1, 2023'), long('MMMM d, yyyy', 'December 1, 2023'), full('EEEE, MMMM d, yyyy', 'Friday, December 1, 2023'), yearMonth('MMM yyyy', 'Dec 2023'), monthDay('MMM d', 'Dec 1'), iso('yyyy-MM-dd', '2023-12-01');
const DateFormatter(this.pattern, this.example); final String pattern; final String example;}
enum TimeFormatter { hour24('HH:mm', '14:30'), hour24WithSeconds('HH:mm:ss', '14:30:45'), hour12('h:mm a', '2:30 PM'), hour12WithSeconds('h:mm:ss a', '2:30:45 PM'), hour12Short('h:mma', '2:30p'), hourOnly24('HH', '14'), hourOnly12('h a', '2 PM');
const TimeFormatter(this.pattern, this.example); final String pattern; final String example;}DateTime Extensions
Section titled âDateTime Extensionsâextension DateTimeExtensions on DateTime? { int days(DateTime? other) { if (this == null || other == null) return 0; final current = this!; final days = other.difference(current).inDays.abs(); return days; }
int nights(DateTime? other) { return days(other); }
String formatDate(DateFormatter formatter, Locale locale) { if (this == null) return ''; final localDateTime = this!.toLocal(); return DateFormat( formatter.pattern, locale.languageCode, ).format(localDateTime); }
String formatTime(TimeFormatter formatter, Locale locale) { if (this == null) return ''; final localDateTime = this!.toLocal(); return DateFormat( formatter.pattern, locale.languageCode, ).format(localDateTime); }}String Extensions for Data Cleaning
Section titled âString Extensions for 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; }}Organization Best Practices
Section titled âOrganization Best Practicesâ- Use Generic Extensions - Always leverage the generic filter and domain extensions from datasource/domain packages
- Group by Feature - Keep conversion extensions in feature-specific files
- Separate by Direction - Create separate extensions for to/from conversions
- Consistent Naming - Use
toGql,toEntity,toDtonaming conventions - Null Safety - Always handle null values appropriately
- Default Values - Provide sensible defaults for missing data
- Package Separation - Domain and datasource are separate packages where datasource depends on domain
Common Patterns
Section titled âCommon Patternsâextension ListExtensions<T> on List<T>? { List<T> get orEmpty => this ?? [];
T? get firstOrNull => (this?.isNotEmpty ?? false) ? this!.first : null;}
extension RelatedFieldExtensions on List<GQueryData_field>? { String? get firstTitle => firstOrNull?.title; String? get firstId => firstOrNull?.id;}Next Steps
Section titled âNext Stepsâ- Learn how to merge pagination data
- Implement optimistic updates
- Understand cache handling strategies