Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 47 additions & 2 deletions lib/src/services/yust_database_service_dart.dart
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,11 @@ class YustDatabaseService implements IYustDatabaseService {
getQuery(
docSetup,
filters: filters,
orderBy: orderBy,
orderBy: getOrderBy(
filters: filters,
orderBy: orderBy,
startAfterDocument: startAfterDocument,
),
limit: limit,
startAfterDocument: startAfterDocument,
),
Expand Down Expand Up @@ -1269,6 +1273,46 @@ class YustDatabaseService implements IYustDatabaseService {
return _transformDoc(docSetup, document as Document);
}

/// Returns the orderBy list to send to Firestore for a `getListFromDB`
/// call. When [startAfterDocument] is `null` the caller's [orderBy] is
/// returned unchanged — behavior for non-paginated queries is
/// preserved.
///
/// When [startAfterDocument] is set the orderBy must be non-empty:
/// Firestore's REST `runQuery` builds the `startAt` cursor by mapping
/// the query's `orderBy` fields onto the document's field values, and
/// an empty cursor makes Firestore return zero results. Mirrors the
/// pattern used in the lazy-chunked path:
/// - Seed orderBy from inequality-filter fields when the caller didn't
/// provide one (Firestore requires an inequality field to be the
/// first orderBy anyway).
/// - Append `__name__` as the stable tiebreaker so the cursor always
/// has a document reference to anchor on.
static List<YustOrderBy>? getOrderBy<T extends YustDoc>({
required List<YustFilter>? filters,
required List<YustOrderBy>? orderBy,
required T? startAfterDocument,
}) {
if (startAfterDocument == null) return orderBy;

final unequalFilters = (filters ?? [])
.whereNot(
(filter) =>
YustFilterComparator.equalityFilters.contains(filter.comparator),
)
.toSet()
.toList();

final resolved = <YustOrderBy>[
if ((orderBy == null || orderBy.isEmpty) && unequalFilters.isNotEmpty)
...unequalFilters.map((e) => YustOrderBy(field: e.field)).toSet(),
...?orderBy,
];

if (resolved.any((o) => o.field == '__name__')) return resolved;
return [...resolved, YustOrderBy(field: '__name__')];
}

String _getDatabasePath() => 'projects/${Yust.projectId}/databases/(default)';

String _getParentPath(YustDocSetup docSetup) {
Expand Down Expand Up @@ -1607,7 +1651,8 @@ class YustDatabaseService implements IYustDatabaseService {
// the `nullValue` branch below, not here.
return map?.map(
(key, childValue) => MapEntry(key, _dbValueToValue(childValue)),
) ?? {};
) ??
{};
} else if (dbValue.booleanValue != null) {
return dbValue.booleanValue;
} else if (dbValue.integerValue != null) {
Expand Down
161 changes: 161 additions & 0 deletions test/yust_database_service_dart_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import 'package:test/test.dart';
import 'package:yust/src/models/yust_doc.dart';
import 'package:yust/src/models/yust_filter.dart';
import 'package:yust/src/models/yust_order_by.dart';
import 'package:yust/src/services/yust_database_service_dart.dart';

class _FakeDoc extends YustDoc {
_FakeDoc({required String id}) {
this.id = id;
}

@override
Map<String, dynamic> toJson() => {'id': id};

YustDoc fromJson(Map<String, dynamic> json) => _FakeDoc(id: json['id']);
}

void main() {
final anchor = _FakeDoc(id: 'anchor');

group('YustDatabaseService.getOrderBy — without startAfterDocument', () {
test('returns caller orderBy unchanged (null stays null)', () {
expect(
YustDatabaseService.getOrderBy<YustDoc>(
filters: null,
orderBy: null,
startAfterDocument: null,
),
isNull,
);
});

test('returns caller orderBy unchanged (explicit list preserved)', () {
final orderBy = [YustOrderBy(field: 'createdAt', descending: true)];
final result = YustDatabaseService.getOrderBy<YustDoc>(
filters: [
YustFilter(
field: 'modifiedAt',
comparator: YustFilterComparator.greaterThanEqual,
value: 'x',
),
],
orderBy: orderBy,
startAfterDocument: null,
);
expect(result, same(orderBy));
});
});

group('YustDatabaseService.getOrderBy — with startAfterDocument', () {
test('null orderBy + no inequality filter → [__name__]', () {
final result = YustDatabaseService.getOrderBy(
filters: null,
orderBy: null,
startAfterDocument: anchor,
);

expect(result, isNotNull);
expect(result!.map((o) => o.field), ['__name__']);
});

test('null orderBy + only equality filter → [__name__]', () {
final result = YustDatabaseService.getOrderBy(
filters: [
YustFilter(
field: 'status',
comparator: YustFilterComparator.equal,
value: 'open',
),
],
orderBy: null,
startAfterDocument: anchor,
);

expect(result!.map((o) => o.field), ['__name__']);
});

test('null orderBy + inequality filter → [<field>, __name__]', () {
final result = YustDatabaseService.getOrderBy(
filters: [
YustFilter(
field: 'modifiedAt',
comparator: YustFilterComparator.greaterThanEqual,
value: '2025-07-01T00:00:00Z',
),
],
orderBy: null,
startAfterDocument: anchor,
);

expect(result!.map((o) => o.field), ['modifiedAt', '__name__']);
expect(result.first.descending, isFalse);
});

test('null orderBy + range on same field (>= and <=) → '
'single field + __name__', () {
final result = YustDatabaseService.getOrderBy(
filters: [
YustFilter(
field: 'modifiedAt',
comparator: YustFilterComparator.greaterThanEqual,
value: 'a',
),
YustFilter(
field: 'modifiedAt',
comparator: YustFilterComparator.lessThanEqual,
value: 'b',
),
],
orderBy: null,
startAfterDocument: anchor,
);

expect(result!.map((o) => o.field), ['modifiedAt', '__name__']);
});

test('explicit orderBy + no filter → orderBy + __name__ tiebreaker', () {
final result = YustDatabaseService.getOrderBy(
filters: null,
orderBy: [YustOrderBy(field: 'createdAt', descending: true)],
startAfterDocument: anchor,
);

expect(result!.map((o) => o.field), ['createdAt', '__name__']);
expect(result.first.descending, isTrue);
});

test('explicit orderBy already contains __name__ → not appended twice', () {
final result = YustDatabaseService.getOrderBy(
filters: null,
orderBy: [
YustOrderBy(field: 'createdAt'),
YustOrderBy(field: '__name__'),
],
startAfterDocument: anchor,
);

expect(result!.map((o) => o.field), ['createdAt', '__name__']);
});

test('explicit orderBy present with inequality filter → keep caller '
'order, append __name__', () {
// Caller-supplied orderBy takes precedence; we do not add the
// inequality field because Firestore requires the caller to have
// already put it first (this is validated elsewhere).
final result = YustDatabaseService.getOrderBy(
filters: [
YustFilter(
field: 'modifiedAt',
comparator: YustFilterComparator.greaterThanEqual,
value: 'a',
),
],
orderBy: [YustOrderBy(field: 'modifiedAt')],
startAfterDocument: anchor,
);

expect(result!.map((o) => o.field), ['modifiedAt', '__name__']);
});
});
}