fetchAndStitchRelations method
Future<void>
fetchAndStitchRelations(
- List<
T> entities, - List<
String> relations, - DataSource source,
- String? userId,
Implementation
Future<void> fetchAndStitchRelations(List<T> entities, List<String> relations, DataSource source, String? userId) async {
if (entities.isEmpty || entities.first is! RelationalDatumEntity) {
return;
}
registerRelationSchema(entities.first);
// A read without an explicit userId still concerns the parents' OWN data:
// fall back to their userId so stitching never attaches other users' rows
// whose foreign keys happen to collide.
userId ??= entities.first.userId;
// Support nested relations via dot notation, e.g. 'posts.comments'. Group
// paths by their top-level segment so each relation is fetched once, then
// recurse into the remaining sub-paths on the fetched related entities (#22).
final nested = <String, List<String>>{};
for (final path in relations) {
final dot = path.indexOf('.');
final head = dot == -1 ? path : path.substring(0, dot);
final tail = dot == -1 ? null : path.substring(dot + 1);
final subPaths = nested.putIfAbsent(head, () => <String>[]);
if (tail != null && tail.isNotEmpty) subPaths.add(tail);
}
for (final entry in nested.entries) {
final relationName = entry.key;
final subPaths = entry.value;
final firstEntity = entities.first as RelationalDatumEntity;
final relation = firstEntity.relations[relationName];
if (relation == null) {
_logger.warn('Relation "$relationName" not found on entity ${T.toString()}');
continue;
}
final relatedManager = relation.getRelatedManager();
var fetchedRelated = const <DatumEntityInterface>[];
if (relation is BelongsTo) {
final foreignKeyName = relation.foreignKey;
final foreignKeyValues = entities.map((e) => (e as RelationalDatumEntity).toDatumMap()[foreignKeyName]).nonNulls.toSet().toList();
if (foreignKeyValues.isNotEmpty) {
final relatedEntities = await relatedManager.query(
DatumQuery(filters: [Filter(relation.localKey, FilterOperator.isIn, foreignKeyValues)]),
source: source,
userId: userId,
);
final relatedEntitiesById = {for (final e in relatedEntities) e.id: e};
for (final entity in entities) {
final relationalEntity = entity as RelationalDatumEntity;
final foreignKeyValue = relationalEntity.toDatumMap()[foreignKeyName];
final relatedEntity = relatedEntitiesById[foreignKeyValue];
relationalEntity.relations[relationName]?.setRaw(relatedEntity);
}
fetchedRelated = relatedEntities;
}
} else if (relation is HasMany) {
final foreignKeyName = relation.foreignKey;
final localKeyValues = entities.map((e) => e.id).toSet().toList();
if (localKeyValues.isNotEmpty) {
final relatedEntities = await relatedManager.query(
DatumQuery(filters: [Filter(foreignKeyName, FilterOperator.isIn, localKeyValues)]),
source: source,
userId: userId,
);
final relatedEntitiesByParentId = <String, List<DatumEntityInterface>>{};
for (final relatedEntity in relatedEntities) {
// toDatumMap is on the base interface — related entities need not be relational.
final parentId = relatedEntity.toDatumMap()[foreignKeyName];
(relatedEntitiesByParentId[parentId] ??= []).add(relatedEntity);
}
for (final entity in entities) {
final related = relatedEntitiesByParentId[entity.id] ?? [];
(entity as RelationalDatumEntity).relations[relationName]?.setRaw(related);
}
fetchedRelated = relatedEntities;
}
} else if (relation is HasOne) {
// 1:1 where the related entity holds the foreign key.
final foreignKeyName = relation.foreignKey;
final localKeyName = relation.localKey;
final localKeyValues = entities.map((e) => e.toDatumMap()[localKeyName]).nonNulls.toSet().toList();
if (localKeyValues.isNotEmpty) {
final relatedEntities = await relatedManager.query(
DatumQuery(filters: [Filter(foreignKeyName, FilterOperator.isIn, localKeyValues)]),
source: source,
userId: userId,
);
final byForeignKey = <Object?, DatumEntityInterface>{};
for (final r in relatedEntities) {
final fk = r.toDatumMap()[foreignKeyName];
byForeignKey.putIfAbsent(fk, () => r); // first wins for a 1:1
}
for (final entity in entities) {
final lk = entity.toDatumMap()[localKeyName];
(entity as RelationalDatumEntity).relations[relationName]?.setRaw(byForeignKey[lk]);
}
fetchedRelated = relatedEntities;
}
} else if (relation is ManyToMany) {
// Traverse the pivot entity to resolve the many-to-many targets.
final pivotManager = Datum.managerByType(relation.pivotType);
final thisLocalKeyName = relation.thisLocalKey;
final localKeyValues = entities.map((e) => e.toDatumMap()[thisLocalKeyName]).nonNulls.toSet().toList();
if (localKeyValues.isNotEmpty) {
final pivotEntries = await pivotManager.query(
DatumQuery(filters: [Filter(relation.thisForeignKey, FilterOperator.isIn, localKeyValues)]),
source: source,
userId: userId,
);
final otherIdsByParent = <Object?, List<Object?>>{};
final allOtherIds = <Object?>{};
for (final pe in pivotEntries) {
final m = pe.toDatumMap();
final thisFk = m[relation.thisForeignKey];
final otherFk = m[relation.otherForeignKey];
if (otherFk != null) {
(otherIdsByParent[thisFk] ??= []).add(otherFk);
allOtherIds.add(otherFk);
}
}
var related = const <DatumEntityInterface>[];
if (allOtherIds.isNotEmpty) {
related = await relatedManager.query(
DatumQuery(filters: [Filter(relation.otherLocalKey, FilterOperator.isIn, allOtherIds.toList())]),
source: source,
userId: userId,
);
}
final relatedByOtherLocalKey = {for (final r in related) r.toDatumMap()[relation.otherLocalKey]: r};
for (final entity in entities) {
final lk = entity.toDatumMap()[thisLocalKeyName];
final ids = otherIdsByParent[lk] ?? const [];
(entity as RelationalDatumEntity).relations[relationName]?.setRaw(ids.map((id) => relatedByOtherLocalKey[id]).nonNulls.toList());
}
fetchedRelated = related;
}
}
// Recurse into nested relations on the fetched related entities.
if (subPaths.isNotEmpty && fetchedRelated.isNotEmpty) {
await recurseIntoRelated(relatedManager, fetchedRelated, subPaths, source, userId);
}
}
}