Implemented basics of extended queries

This commit is contained in:
Anton Stubenbord
2022-12-16 00:58:53 +01:00
parent 209f692cfd
commit f77ccf50c1
6 changed files with 308 additions and 102 deletions

View File

@@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart';
import 'package:paperless_api/src/constants.dart'; import 'package:paperless_api/src/constants.dart';
import 'package:paperless_api/src/models/query_parameters/asn_query.dart'; import 'package:paperless_api/src/models/query_parameters/asn_query.dart';
import 'package:paperless_api/src/models/query_parameters/correspondent_query.dart'; import 'package:paperless_api/src/models/query_parameters/correspondent_query.dart';
import 'package:paperless_api/src/models/query_parameters/date_range_query.dart';
import 'package:paperless_api/src/models/query_parameters/document_type_query.dart'; import 'package:paperless_api/src/models/query_parameters/document_type_query.dart';
import 'package:paperless_api/src/models/query_parameters/query_type.dart'; import 'package:paperless_api/src/models/query_parameters/query_type.dart';
import 'package:paperless_api/src/models/query_parameters/sort_field.dart'; import 'package:paperless_api/src/models/query_parameters/sort_field.dart';
@@ -29,16 +30,12 @@ class DocumentFilter extends Equatable {
final TagsQuery tags; final TagsQuery tags;
final SortField sortField; final SortField sortField;
final SortOrder sortOrder; final SortOrder sortOrder;
final DateTime? addedDateAfter; final DateRangeQuery added;
final DateTime? addedDateBefore; final DateRangeQuery created;
final DateTime? createdDateAfter;
final DateTime? createdDateBefore;
final QueryType queryType; final QueryType queryType;
final String? queryText; final String? queryText;
const DocumentFilter({ const DocumentFilter({
this.createdDateAfter,
this.createdDateBefore,
this.documentType = const DocumentTypeQuery.unset(), this.documentType = const DocumentTypeQuery.unset(),
this.correspondent = const CorrespondentQuery.unset(), this.correspondent = const CorrespondentQuery.unset(),
this.storagePath = const StoragePathQuery.unset(), this.storagePath = const StoragePathQuery.unset(),
@@ -48,53 +45,39 @@ class DocumentFilter extends Equatable {
this.sortOrder = SortOrder.descending, this.sortOrder = SortOrder.descending,
this.page = 1, this.page = 1,
this.pageSize = 25, this.pageSize = 25,
this.addedDateAfter,
this.addedDateBefore,
this.queryType = QueryType.titleAndContent, this.queryType = QueryType.titleAndContent,
this.queryText, this.queryText,
this.added = const UnsetDateRangeQuery(),
this.created = const UnsetDateRangeQuery(),
}); });
String toQueryString() { Map<String, String> toQueryParameters() {
final StringBuffer sb = StringBuffer("page=$page&page_size=$pageSize"); Map<String, String> params = {
sb.write(documentType.toQueryParameter()); 'page': page.toString(),
sb.write(correspondent.toQueryParameter()); 'page_size': pageSize.toString(),
sb.write(tags.toQueryParameter()); };
sb.write(storagePath.toQueryParameter());
sb.write(asnQuery.toQueryParameter());
params.addAll(documentType.toQueryParameter());
params.addAll(correspondent.toQueryParameter());
params.addAll(tags.toQueryParameter());
params.addAll(storagePath.toQueryParameter());
params.addAll(asnQuery.toQueryParameter());
params.addAll(added.toQueryParameter());
params.addAll(created.toQueryParameter());
//TODO: Rework when implementing extended queries.
if (queryText?.isNotEmpty ?? false) { if (queryText?.isNotEmpty ?? false) {
sb.write("&${queryType.queryParam}=$queryText"); params.putIfAbsent(queryType.queryParam, () => queryText!);
} }
// Reverse ordering can also be encoded using &reverse=1
params.putIfAbsent(
'ordering', () => '${sortOrder.queryString}${sortField.queryString}');
sb.write("&ordering=${sortOrder.queryString}${sortField.queryString}"); return params;
// Add/subtract one day in the following because paperless uses gt/lt not gte/lte
if (addedDateAfter != null) {
sb.write(
"&added__date__gt=${apiDateFormat.format(addedDateAfter!.subtract(_oneDay))}");
}
if (addedDateBefore != null) {
sb.write(
"&added__date__lt=${apiDateFormat.format(addedDateBefore!.add(_oneDay))}");
}
if (createdDateAfter != null) {
sb.write(
"&created__date__gt=${apiDateFormat.format(createdDateAfter!.subtract(_oneDay))}");
}
if (createdDateBefore != null) {
sb.write(
"&created__date__lt=${apiDateFormat.format(createdDateBefore!.add(_oneDay))}");
}
return sb.toString();
} }
@override @override
String toString() { String toString() {
return toQueryString(); return toQueryParameters().toString();
} }
DocumentFilter copyWith({ DocumentFilter copyWith({
@@ -108,10 +91,8 @@ class DocumentFilter extends Equatable {
TagsQuery? tags, TagsQuery? tags,
SortField? sortField, SortField? sortField,
SortOrder? sortOrder, SortOrder? sortOrder,
DateTime? addedDateAfter, DateRangeQuery? added,
DateTime? addedDateBefore, DateRangeQuery? created,
DateTime? createdDateBefore,
DateTime? createdDateAfter,
QueryType? queryType, QueryType? queryType,
String? queryText, String? queryText,
}) { }) {
@@ -124,13 +105,11 @@ class DocumentFilter extends Equatable {
tags: tags ?? this.tags, tags: tags ?? this.tags,
sortField: sortField ?? this.sortField, sortField: sortField ?? this.sortField,
sortOrder: sortOrder ?? this.sortOrder, sortOrder: sortOrder ?? this.sortOrder,
addedDateAfter: addedDateAfter ?? this.addedDateAfter, added: added ?? this.added,
addedDateBefore: addedDateBefore ?? this.addedDateBefore,
queryType: queryType ?? this.queryType, queryType: queryType ?? this.queryType,
queryText: queryText ?? this.queryText, queryText: queryText ?? this.queryText,
createdDateBefore: createdDateBefore ?? this.createdDateBefore,
createdDateAfter: createdDateAfter ?? this.createdDateAfter,
asnQuery: asnQuery ?? this.asnQuery, asnQuery: asnQuery ?? this.asnQuery,
created: created ?? this.created,
); );
} }
@@ -160,10 +139,8 @@ class DocumentFilter extends Equatable {
correspondent != initial.correspondent, correspondent != initial.correspondent,
storagePath != initial.storagePath, storagePath != initial.storagePath,
tags != initial.tags, tags != initial.tags,
(addedDateAfter != initial.addedDateAfter || (added != initial.added),
addedDateBefore != initial.addedDateBefore), (created != initial.created),
(createdDateAfter != initial.createdDateAfter ||
createdDateBefore != initial.createdDateBefore),
asnQuery != initial.asnQuery, asnQuery != initial.asnQuery,
(queryType != initial.queryType || queryText != initial.queryText), (queryType != initial.queryType || queryText != initial.queryText),
].fold(0, (previousValue, element) => previousValue += element ? 1 : 0); ].fold(0, (previousValue, element) => previousValue += element ? 1 : 0);
@@ -179,10 +156,8 @@ class DocumentFilter extends Equatable {
tags, tags,
sortField, sortField,
sortOrder, sortOrder,
addedDateAfter, added,
addedDateBefore, created,
createdDateAfter,
createdDateBefore,
queryType, queryType,
queryText, queryText,
]; ];

View File

@@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart';
import 'package:paperless_api/src/constants.dart'; import 'package:paperless_api/src/constants.dart';
import 'package:paperless_api/src/models/document_filter.dart'; import 'package:paperless_api/src/models/document_filter.dart';
import 'package:paperless_api/src/models/query_parameters/correspondent_query.dart'; import 'package:paperless_api/src/models/query_parameters/correspondent_query.dart';
import 'package:paperless_api/src/models/query_parameters/date_range_query.dart';
import 'package:paperless_api/src/models/query_parameters/document_type_query.dart'; import 'package:paperless_api/src/models/query_parameters/document_type_query.dart';
import 'package:paperless_api/src/models/query_parameters/query_type.dart'; import 'package:paperless_api/src/models/query_parameters/query_type.dart';
import 'package:paperless_api/src/models/query_parameters/storage_path_query.dart'; import 'package:paperless_api/src/models/query_parameters/storage_path_query.dart';
@@ -18,6 +19,8 @@ class FilterRule with EquatableMixin {
static const int createdAfterRule = 9; static const int createdAfterRule = 9;
static const int addedBeforeRule = 13; static const int addedBeforeRule = 13;
static const int addedAfterRule = 14; static const int addedAfterRule = 14;
static const int modifiedBeforeRule = 15;
static const int modifiedAfterRule = 16;
static const int excludeTagsRule = 17; static const int excludeTagsRule = 17;
static const int titleAndContentRule = 19; static const int titleAndContentRule = 19;
static const int extendedRule = 20; static const int extendedRule = 20;
@@ -28,14 +31,15 @@ class FilterRule with EquatableMixin {
static const int _createdYearIs = 10; static const int _createdYearIs = 10;
static const int _createdMonthIs = 11; static const int _createdMonthIs = 11;
static const int _createdDayIs = 12; static const int _createdDayIs = 12;
static const int _modifiedBefore = 15;
static const int _modifiedAfter = 16;
static const int _doesNotHaveAsn = 18; static const int _doesNotHaveAsn = 18;
static const int _moreLikeThis = 21; static const int _moreLikeThis = 21;
static const int _hasTagsIn = 22; static const int _hasTagsIn = 22;
static const int _asnGreaterThan = 23; static const int _asnGreaterThan = 23;
static const int _asnLessThan = 24; static const int _asnLessThan = 24;
static const String _lastNDateRangeQueryRegex =
r"(?<field>created|added|modified):\[(?<n>-?\d+) (?<unit>day|week|month|year) to now\]";
final int ruleType; final int ruleType;
final String? value; final String? value;
@@ -53,6 +57,9 @@ class FilterRule with EquatableMixin {
} }
DocumentFilter applyToFilter(final DocumentFilter filter) { DocumentFilter applyToFilter(final DocumentFilter filter) {
if (value == null) {
return filter;
}
//TODO: Check in profiling mode if this is inefficient enough to cause stutters... //TODO: Check in profiling mode if this is inefficient enough to cause stutters...
switch (ruleType) { switch (ruleType) {
case titleRule: case titleRule:
@@ -94,34 +101,107 @@ class FilterRule with EquatableMixin {
.withIdQueriesAdded([ExcludeTagIdQuery(int.parse(value!))]), .withIdQueriesAdded([ExcludeTagIdQuery(int.parse(value!))]),
); );
case createdBeforeRule: case createdBeforeRule:
return filter.copyWith( if (filter.created is FixedDateRangeQuery) {
createdDateBefore: value == null ? null : DateTime.parse(value!), return filter.copyWith(
); created: (filter.created as FixedDateRangeQuery)
.copyWith(before: DateTime.parse(value!)),
);
} else {
return filter.copyWith(
created:
FixedDateRangeQuery.created(before: DateTime.parse(value!)),
);
}
case createdAfterRule: case createdAfterRule:
return filter.copyWith( if (filter.created is FixedDateRangeQuery) {
createdDateAfter: value == null ? null : DateTime.parse(value!), return filter.copyWith(
); created: (filter.created as FixedDateRangeQuery)
.copyWith(after: DateTime.parse(value!)),
);
} else {
return filter.copyWith(
created: FixedDateRangeQuery.created(after: DateTime.parse(value!)),
);
}
case addedBeforeRule: case addedBeforeRule:
return filter.copyWith( if (filter.added is FixedDateRangeQuery) {
addedDateBefore: value == null ? null : DateTime.parse(value!), return filter.copyWith(
); added: (filter.added as FixedDateRangeQuery)
.copyWith(before: DateTime.parse(value!)),
);
} else {
return filter.copyWith(
added: FixedDateRangeQuery.added(before: DateTime.parse(value!)),
);
}
case addedAfterRule: case addedAfterRule:
return filter.copyWith( if (filter.added is FixedDateRangeQuery) {
addedDateAfter: value == null ? null : DateTime.parse(value!), return filter.copyWith(
); added: (filter.added as FixedDateRangeQuery)
.copyWith(after: DateTime.parse(value!)),
);
} else {
return filter.copyWith(
added: FixedDateRangeQuery.added(after: DateTime.parse(value!)),
);
}
case titleAndContentRule: case titleAndContentRule:
return filter.copyWith( return filter.copyWith(
queryText: value, queryText: value,
queryType: QueryType.titleAndContent, queryType: QueryType.titleAndContent,
); );
case extendedRule: case extendedRule:
_parseExtendedRule(filter);
return filter.copyWith(queryText: value, queryType: QueryType.extended); return filter.copyWith(queryText: value, queryType: QueryType.extended);
//TODO: Add currently unused rules
default: default:
return filter; return filter;
} }
} }
DocumentFilter _parseExtendedRule(final DocumentFilter filter) {
DocumentFilter newFilter = filter;
assert(value != null);
final dateRangeRegExp = RegExp(_lastNDateRangeQueryRegex);
if (dateRangeRegExp.hasMatch(value!)) {
final matches = dateRangeRegExp.allMatches(value!);
for (final match in matches) {
final field = match.namedGroup('field')!;
final n = int.parse(match.namedGroup('n')!);
final unit = match.namedGroup('unit')!;
switch (field) {
case 'created':
newFilter = newFilter.copyWith(
created: LastNDateRangeQuery.created(
n,
DateRangeUnit.values.byName(unit),
),
);
break;
case 'added':
newFilter = newFilter.copyWith(
created: LastNDateRangeQuery.added(
n,
DateRangeUnit.values.byName(unit),
),
);
break;
case 'modified':
newFilter = newFilter.copyWith(
created: LastNDateRangeQuery.modified(
n,
DateRangeUnit.values.byName(unit),
),
);
break;
}
}
return newFilter;
} else {
// Match other extended query types... currently not supported!
return filter;
}
}
/// ///
/// Converts a [DocumentFilter] to a list of [FilterRule]s. /// Converts a [DocumentFilter] to a list of [FilterRule]s.
/// ///
@@ -179,22 +259,65 @@ class FilterRule with EquatableMixin {
break; break;
} }
} }
if (filter.createdDateAfter != null) {
filterRules.add(FilterRule( // Parse created at
createdAfterRule, apiDateFormat.format(filter.createdDateAfter!))); final created = filter.created;
if (created is FixedDateRangeQuery) {
if (created.after != null) {
filterRules.add(
FilterRule(createdAfterRule, apiDateFormat.format(created.after!)),
);
}
if (created.before != null) {
filterRules.add(
FilterRule(createdBeforeRule, apiDateFormat.format(created.before!)),
);
}
} else if (created is LastNDateRangeQuery) {
filterRules.add(
FilterRule(extendedRule, created.toQueryParameter().values.first),
);
} }
if (filter.createdDateBefore != null) {
filterRules.add(FilterRule( // Parse added at
createdBeforeRule, apiDateFormat.format(filter.createdDateBefore!))); final added = filter.added;
if (added is FixedDateRangeQuery) {
if (added.after != null) {
filterRules.add(
FilterRule(addedAfterRule, apiDateFormat.format(added.after!)),
);
}
if (added.before != null) {
filterRules.add(
FilterRule(addedBeforeRule, apiDateFormat.format(added.before!)),
);
}
} else if (added is LastNDateRangeQuery) {
filterRules.add(
FilterRule(extendedRule, added.toQueryParameter().values.first),
);
} }
if (filter.addedDateAfter != null) {
filterRules.add(FilterRule( // Parse modified at
addedAfterRule, apiDateFormat.format(filter.addedDateAfter!))); final modified = filter.added;
} if (modified is FixedDateRangeQuery) {
if (filter.addedDateBefore != null) { if (modified.after != null) {
filterRules.add(FilterRule( filterRules.add(
addedBeforeRule, apiDateFormat.format(filter.addedDateBefore!))); FilterRule(modifiedAfterRule, apiDateFormat.format(modified.after!)),
);
}
if (modified.before != null) {
filterRules.add(
FilterRule(
modifiedBeforeRule, apiDateFormat.format(modified.before!)),
);
}
} else if (modified is LastNDateRangeQuery) {
filterRules.add(
FilterRule(extendedRule, modified.toQueryParameter().values.first),
);
} }
return filterRules; return filterRules;
} }

View File

@@ -0,0 +1,103 @@
import 'package:equatable/equatable.dart';
import 'package:paperless_api/src/constants.dart';
abstract class DateRangeQuery extends Equatable {
const DateRangeQuery();
Map<String, String> toQueryParameter();
}
class UnsetDateRangeQuery extends DateRangeQuery {
const UnsetDateRangeQuery();
@override
List<Object?> get props => [];
@override
Map<String, String> toQueryParameter() => const {};
}
class FixedDateRangeQuery extends DateRangeQuery {
final String _querySuffix;
final DateTime? after;
final DateTime? before;
const FixedDateRangeQuery._(this._querySuffix, {this.after, this.before})
: assert(after != null || before != null);
const FixedDateRangeQuery.created({DateTime? after, DateTime? before})
: this._('created', after: after, before: before);
const FixedDateRangeQuery.added({DateTime? after, DateTime? before})
: this._('added', after: after, before: before);
const FixedDateRangeQuery.modified({DateTime? after, DateTime? before})
: this._('modified', after: after, before: before);
@override
List<Object?> get props => [_querySuffix, after, before];
@override
Map<String, String> toQueryParameter() {
final Map<String, String> params = {};
// Add/subtract one day in the following because paperless uses gt/lt not gte/lte
if (after != null) {
params.putIfAbsent('${_querySuffix}__date__gt',
() => apiDateFormat.format(after!.subtract(const Duration(days: 1))));
}
if (before != null) {
params.putIfAbsent('${_querySuffix}__date__lt',
() => apiDateFormat.format(before!.add(const Duration(days: 1))));
}
return params;
}
FixedDateRangeQuery copyWith({
DateTime? before,
DateTime? after,
}) {
return FixedDateRangeQuery._(
_querySuffix,
before: before ?? this.before,
after: after ?? this.after,
);
}
}
class LastNDateRangeQuery extends DateRangeQuery {
final DateRangeUnit unit;
final int n;
final String _field;
const LastNDateRangeQuery._(
this._field, {
required this.n,
required this.unit,
});
const LastNDateRangeQuery.created(int n, DateRangeUnit unit)
: this._('created', unit: unit, n: n);
const LastNDateRangeQuery.added(int n, DateRangeUnit unit)
: this._('added', unit: unit, n: n);
const LastNDateRangeQuery.modified(int n, DateRangeUnit unit)
: this._('modified', unit: unit, n: n);
@override
// TODO: implement props
List<Object?> get props => [_field, n, unit];
@override
Map<String, String> toQueryParameter() {
return {
'query': '[$_field:$n ${unit.name} to now]',
};
}
}
enum DateRangeUnit {
day,
week,
month,
year;
}

View File

@@ -30,14 +30,16 @@ abstract class IdQueryParameter extends Equatable {
String get queryParameterKey; String get queryParameterKey;
String toQueryParameter() { Map<String, String> toQueryParameter() {
final Map<String, String> params = {};
if (onlyNotAssigned || onlyAssigned) { if (onlyNotAssigned || onlyAssigned) {
return "&${queryParameterKey}__isnull=$_assignmentStatus"; params.putIfAbsent(
'${queryParameterKey}__isnull', () => _assignmentStatus!.toString());
} }
if (isSet) { if (isSet) {
return "&${queryParameterKey}__id=$id"; params.putIfAbsent("${queryParameterKey}__id", () => id!.toString());
} }
return ""; return params;
} }
@override @override

View File

@@ -2,14 +2,14 @@ import 'package:equatable/equatable.dart';
abstract class TagsQuery extends Equatable { abstract class TagsQuery extends Equatable {
const TagsQuery(); const TagsQuery();
String toQueryParameter(); Map<String, String> toQueryParameter();
} }
class OnlyNotAssignedTagsQuery extends TagsQuery { class OnlyNotAssignedTagsQuery extends TagsQuery {
const OnlyNotAssignedTagsQuery(); const OnlyNotAssignedTagsQuery();
@override @override
String toQueryParameter() { Map<String, String> toQueryParameter() {
return '&is_tagged=0'; return {'is_tagged': '0'};
} }
@override @override
@@ -24,11 +24,11 @@ class AnyAssignedTagsQuery extends TagsQuery {
}); });
@override @override
String toQueryParameter() { Map<String, String> toQueryParameter() {
if (tagIds.isEmpty) { if (tagIds.isEmpty) {
return '&is_tagged=1'; return {'is_tagged': '1'};
} }
return '&tags__id__in=${tagIds.join(',')}'; return {'tags__id__in': tagIds.join(',')};
} }
@override @override
@@ -89,15 +89,15 @@ class IdsTagsQuery extends TagsQuery {
Iterable<int> get ids => [...includedIds, ...excludedIds]; Iterable<int> get ids => [...includedIds, ...excludedIds];
@override @override
String toQueryParameter() { Map<String, String> toQueryParameter() {
final StringBuffer sb = StringBuffer(""); final Map<String, String> params = {};
if (includedIds.isNotEmpty) { if (includedIds.isNotEmpty) {
sb.write('&tags__id__all=${includedIds.join(',')}'); params.putIfAbsent('tags__id__all', () => includedIds.join(','));
} }
if (excludedIds.isNotEmpty) { if (excludedIds.isNotEmpty) {
sb.write('&tags__id__none=${excludedIds.join(',')}'); params.putIfAbsent('tags__id__none', () => excludedIds.join(','));
} }
return sb.toString(); return params;
} }
@override @override

View File

@@ -136,9 +136,12 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
@override @override
Future<PagedSearchResult<DocumentModel>> find(DocumentFilter filter) async { Future<PagedSearchResult<DocumentModel>> find(DocumentFilter filter) async {
final filterParams = filter.toQueryString(); final filterParams = filter.toQueryParameters();
final response = await baseClient.get( final response = await baseClient.get(
Uri.parse("/api/documents/?$filterParams"), Uri(
path: "/api/documents/?$filterParams",
queryParameters: filterParams,
),
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
return compute( return compute(