Skip to content

Repository Context Usage

Problem

You need to pass context data to repository methods that isn’t part of the GraphQL request but is needed for internal filtering and business logic decisions.

Enhancement Needed

Repository context patterns need team input for standardization and better type safety mechanisms.

Repository context allows passing data needed for layer logic that isn’t tied to the GraphQL request:

typedef ChatContext = ({String currentUserId});
class ChatsRepository
implements
QueryableRepository<
PaginatedChatsModel,
ChatModel,
ChatFilterCriteria,
ChatSortCriteria
>,
WritableRepository<ChatModel, ChatUpsertVars>,
SubscribableRepository<ChatModel, Object?> {
ChatsRepository({required this.datasource});
final ChatDatasource datasource;
@override
Stream<PaginatedChatsModel> queryList({
int? skip,
int? take,
ChatFilterCriteria? filter,
ChatSortCriteria? sort,
Object? context,
}) {
if (context is! ChatContext) {
throw ArgumentError(
'ChatContext with currentUserId is required for chat normalization',
);
}
return datasource
.queryList(
skip: skip,
take: take,
filter: filter?.toGql,
sort: sort?.toGql,
)
.map((response) => response.data?.chat)
.map((data) => convertToPaginatedChat(data, context.currentUserId));
}
}

Use context for data normalization that requires external information:

ChatModel _buildNormalizedChatModel({
required String id,
required String? lastMessage,
required DateTime createdAt,
required DateTime updatedAt,
required AttendeeModel user1,
required AttendeeModel user2,
required String currentUserId,
required bool? isBlockedByUser1,
required bool? isBlockedByUser2,
int? childMessageCount,
}) {
final bool user1IsCurrent = user1.id == currentUserId;
if (!user1IsCurrent && user2.id != currentUserId) {
throw StateError('currentUserId does not belong to this chat');
}
return ChatModel(
id: id,
lastMessage: lastMessage,
createdAt: createdAt,
updatedAt: updatedAt,
currentUser: user1IsCurrent ? user1 : user2,
otherUser: user1IsCurrent ? user2 : user1,
childMessageCount: childMessageCount ?? 0,
isBlockedByCurrentUser: user1IsCurrent
? isBlockedByUser1 ?? false
: isBlockedByUser2 ?? false,
isBlockedByOtherUser: user1IsCurrent
? isBlockedByUser2 ?? false
: isBlockedByUser1 ?? false,
);
}

Implement proper context validation:

@override
Stream<ChatModel?> queryOne({
required ChatFilterCriteria filter,
Object? context,
}) {
if (context is! ChatContext) {
throw ArgumentError(
'ChatContext with currentUserId is required for chat normalization',
);
}
return datasource
.queryOne(filter: filter.toGql)
.map((response) => response.data?.chat)
.map((data) => convertToChat(data, context.currentUserId));
}

Use context for mutation operations that need additional data:

@override
Stream<ChatModel> upsert({required ChatUpsertVars vars, Object? context}) {
if (context is! ChatContext) {
throw ArgumentError(
'ChatContext with currentUserId is required for chat normalization',
);
}
return datasource.upsert(vars: vars.toGql).map((response) {
final result = convertFromMutationToChat(
response.data?.chatSave,
context.currentUserId,
);
if (result == null) {
throw ChatUpsertException(
code: CRUDExceptionCode.NO_DATA,
stackTrace: StackTrace.current.toString(),
originalException: Exception(
'No data received from server $vars, $result',
),
);
}
return result;
});
}

Pass context to subscription methods for filtering:

@override
Stream<ChatModel> subscribe(Object? context) {
if (context is! ChatContext) {
throw ArgumentError(
'ChatContext with currentUserId is required for chat normalization',
);
}
return datasource.subscribe().map((response) {
final result = convertFromSubscriptionToChat(
response.data?.chatSubscription,
context.currentUserId,
);
if (result == null) {
throw ChatUpsertException(
code: CRUDExceptionCode.NO_DATA,
stackTrace: StackTrace.current.toString(),
originalException: Exception('No data received from server, $result'),
);
}
return result;
});
}

Services provide context when calling repository methods:

class ChatService {
final ChatsRepository _repository;
final AuthService _authService;
Stream<PaginatedChatsModel> getChats({
int? skip,
int? take,
ChatFilterCriteria? filter,
ChatSortCriteria? sort,
}) {
final context = (currentUserId: _authService.currentUserId);
return _repository.queryList(
skip: skip,
take: take,
filter: filter,
sort: sort,
context: context,
);
}
Stream<ChatModel?> getChatById(String chatId) {
final context = (currentUserId: _authService.currentUserId);
return _repository.queryOne(
filter: ChatFilterCriteria(id: chatId),
context: context,
);
}
}

For complex scenarios, use structured context types:

@freezed
class HotelBookingContext with _$HotelBookingContext {
const factory HotelBookingContext({
required String userId,
required UserRole userRole,
String? organizationId,
List<String>? permissions,
Map<String, dynamic>? metadata,
}) = _HotelBookingContext;
}
class HotelBookingsRepository {
Stream<PaginatedHotelBookingsEntity> queryList({
int? skip,
int? take,
HotelBookingFilterCriteria? filter,
HotelBookingSortCriteria? sort,
Object? context,
}) {
final bookingContext = context as HotelBookingContext?;
return datasource
.queryList(
skip: skip,
take: take,
filter: _applyContextFilters(filter, bookingContext),
sort: sort?.toGql,
)
.map((response) => response.data?.hotelbookings)
.map((data) => convertToPaginatedHotelBookings(data, bookingContext));
}
HotelBookingFilterCriteria? _applyContextFilters(
HotelBookingFilterCriteria? filter,
HotelBookingContext? context,
) {
if (context == null) return filter;
return (filter ?? HotelBookingFilterCriteria()).copyWith(
userId: context.userRole == UserRole.admin ? null : context.userId,
organizationId: context.organizationId,
);
}
}

Common repository context scenarios:

  1. User Normalization - Determine current vs other user in relationships
  2. Permission Filtering - Filter data based on user permissions
  3. Organization Scoping - Scope data to user’s organization
  4. Feature Flags - Enable/disable features based on context
  5. Audit Information - Track who performed operations
  6. Localization - Format data based on user locale

Current Limitations:

  1. Type Safety - Context is Object? without compile-time validation
  2. Context Inheritance - No way to compose contexts
  3. Context Validation - Runtime validation only
  4. Context Documentation - No clear contract for required context

Proposed Improvements:

  1. Typed Context Interface - Define context contracts per repository
  2. Context Composition - Combine multiple context sources
  3. Context Middleware - Process context before repository methods
  4. Context Validation Framework - Compile-time context validation