Skip to content

Commit 5c64000

Browse files
authored
New tag to indicate search on the current user's liked packages. (#8928)
1 parent 329b269 commit 5c64000

File tree

10 files changed

+104
-22
lines changed

10 files changed

+104
-22
lines changed

app/lib/account/like_backend.dart

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,14 @@ class LikeBackend {
3434
}
3535

3636
/// Returns a list with [LikeData] of all the packages that the given
37-
/// [user] likes.
38-
Future<List<LikeData>> listPackageLikes(User user) async {
39-
return (await cache.userPackageLikes(user.userId).get(() async {
37+
/// user's likes.
38+
Future<List<LikeData>> listPackageLikes(String userId) async {
39+
return (await cache.userPackageLikes(userId).get(() async {
4040
// TODO(zarah): Introduce pagination and/or migrate this to search.
41-
final query = _db.query<Like>(ancestorKey: user.key)
42-
..order('-created')
43-
..limit(1000);
41+
final query =
42+
_db.query<Like>(ancestorKey: _db.emptyKey.append(User, id: userId))
43+
..order('-created')
44+
..limit(1000);
4445
final likes = await query.run().toList();
4546
return likes.map((Like l) => LikeData.fromModel(l)).toList();
4647
}))!;

app/lib/admin/backend.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ class AdminBackend {
178178
Future<void> _removeAndDecrementLikes(User user) async {
179179
final pool = Pool(5);
180180
final futures = <Future>[];
181-
for (final like in await likeBackend.listPackageLikes(user)) {
181+
for (final like in await likeBackend.listPackageLikes(user.userId)) {
182182
final f = pool
183183
.withResource(() => likeBackend.unlikePackage(user, like.package!));
184184
futures.add(f);

app/lib/frontend/handlers/account.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ Future<LikedPackagesResponse> listPackageLikesHandler(
197197
shelf.Request request) async {
198198
final authenticatedUser = await requireAuthenticatedWebUser();
199199
final user = authenticatedUser.user;
200-
final packages = await likeBackend.listPackageLikes(user);
200+
final packages = await likeBackend.listPackageLikes(user.userId);
201201
final List<PackageLikeResponse> packageLikes = packages
202202
.map((like) => PackageLikeResponse(
203203
liked: true, package: like.package, created: like.created))
@@ -294,7 +294,7 @@ Future<shelf.Response> accountMyLikedPackagesPageHandler(
294294

295295
final user = (await accountBackend
296296
.lookupUserById(requestContext.authenticatedUserId!))!;
297-
final likes = await likeBackend.listPackageLikes(user);
297+
final likes = await likeBackend.listPackageLikes(user.userId);
298298
final html = renderMyLikedPackagesPage(
299299
user: user,
300300
userSessionData: requestContext.sessionData!,

app/lib/frontend/handlers/experimental.dart

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,11 @@ typedef PublicFlag = ({String name, String description});
1010

1111
const _publicFlags = <PublicFlag>{
1212
(name: 'example', description: 'Short description'),
13-
(
14-
name: 'trending-search',
15-
description: 'Show trending packages and search by trending scores'
16-
),
1713
};
1814

1915
final _allFlags = <String>{
2016
'dark-as-default',
21-
'search-post',
17+
'my-liked-search',
2218
..._publicFlags.map((x) => x.name),
2319
};
2420

@@ -93,7 +89,7 @@ class ExperimentalFlags {
9389

9490
bool get isDarkModeDefault => isEnabled('dark-as-default');
9591

96-
bool get useSearchPost => isEnabled('search-post');
92+
bool get useMyLikedSearch => isEnabled('my-liked-search');
9793

9894
String encodedAsCookie() => _enabled.join(':');
9995

app/lib/search/mem_index.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'dart:math' as math;
66

77
import 'package:_pub_shared/search/search_form.dart';
88
import 'package:_pub_shared/search/search_request_data.dart';
9+
import 'package:_pub_shared/search/tags.dart';
910
import 'package:clock/clock.dart';
1011
import 'package:collection/collection.dart';
1112
import 'package:logging/logging.dart';
@@ -126,6 +127,13 @@ class InMemoryPackageIndex {
126127
if (query.offset >= _documents.length) {
127128
return PackageSearchResult.empty();
128129
}
130+
// Special case for the account-related tag.
131+
if (query.parsedQuery.tagsPredicate.hasTag(AccountTag.isLikedByMe)) {
132+
return PackageSearchResult.error(
133+
statusCode: 400,
134+
errorMessage: '`is:liked-by-me` is only for authenticated users.',
135+
);
136+
}
129137
return _bitArrayPool.withPoolItem(fn: (array) {
130138
return _scorePool.withItemGetter(
131139
(scoreFn) {

app/lib/search/search_client.dart

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
import 'dart:async';
66
import 'dart:convert';
77

8+
import 'package:_pub_shared/search/tags.dart';
89
import 'package:_pub_shared/utils/http.dart';
910
import 'package:clock/clock.dart';
1011
import 'package:gcloud/service_scope.dart' as ss;
11-
import 'package:pub_dev/frontend/request_context.dart';
1212

1313
import '../../../service/rate_limit/rate_limit.dart';
14+
import '../../account/like_backend.dart';
15+
import '../../frontend/request_context.dart';
1416
import '../shared/configuration.dart';
1517
import '../shared/redis_cache.dart' show cache;
1618
import '../shared/utils.dart';
@@ -66,15 +68,40 @@ class SearchClient {
6668
skipCache = true;
6769
}
6870

71+
final hasLikedByMeTag =
72+
query.parsedQuery.tagsPredicate.hasTag(AccountTag.isLikedByMe);
73+
final userId = requestContext.sessionData?.userId;
74+
if (hasLikedByMeTag) {
75+
skipCache = true;
76+
}
77+
78+
List<String>? packages;
79+
if (userId != null && hasLikedByMeTag) {
80+
final likedPackages = await likeBackend.listPackageLikes(userId);
81+
packages = likedPackages.map((l) => l.package!).toList();
82+
}
83+
6984
// Returns the status code and the body of the last response, or null on timeout.
7085
Future<({int statusCode, String? body})?> doCallHttpServiceEndpoint(
7186
{String? prefix}) async {
7287
final httpHostPort = prefix ?? activeConfiguration.searchServicePrefix;
7388
try {
74-
if (requestContext.experimentalFlags.useSearchPost) {
89+
if (requestContext.experimentalFlags.useMyLikedSearch) {
7590
return await withRetryHttpClient(
7691
(client) async {
77-
final data = query.toSearchRequestData();
92+
var data = query.toSearchRequestData();
93+
if (userId != null && hasLikedByMeTag) {
94+
final newQuery =
95+
data.query?.replaceAll(AccountTag.isLikedByMe, ' ').trim();
96+
final newTags = data.tags!
97+
.where((e) => e != AccountTag.isLikedByMe)
98+
.toList();
99+
data = data.replace(
100+
query: newQuery,
101+
tags: newTags,
102+
packages: packages,
103+
);
104+
}
78105
// NOTE: Keeping the query parameter to help investigating logs.
79106
final uri = Uri.parse('$httpHostPort/search').replace(
80107
queryParameters: {

app/test/admin/user_merger_test.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,17 +150,17 @@ void main() {
150150
final admin = await accountBackend.lookupUserByEmail('admin@pub.dev');
151151
await likeBackend.likePackage(admin, 'oxygen');
152152
expect(
153-
(await likeBackend.listPackageLikes(admin))
153+
(await likeBackend.listPackageLikes(admin.userId))
154154
.map((e) => e.package)
155155
.toList(),
156156
['oxygen']);
157-
expect(await likeBackend.listPackageLikes(user), isEmpty);
157+
expect(await likeBackend.listPackageLikes(user.userId), isEmpty);
158158

159159
await _corruptAndFix();
160160

161-
expect(await likeBackend.listPackageLikes(admin), isEmpty);
161+
expect(await likeBackend.listPackageLikes(admin.userId), isEmpty);
162162
expect(
163-
(await likeBackend.listPackageLikes(user))
163+
(await likeBackend.listPackageLikes(user.userId))
164164
.map((e) => e.package)
165165
.toList(),
166166
['oxygen']);

pkg/_pub_shared/lib/search/search_request_data.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,25 @@ class SearchRequestData {
8888
map.removeWhere((k, v) => v == null);
8989
return map;
9090
}
91+
92+
/// Creates a new instance with fields being replaced (if provided).
93+
SearchRequestData replace({
94+
String? query,
95+
List<String>? tags,
96+
List<String>? packages,
97+
}) {
98+
return SearchRequestData(
99+
query: query ?? this.query,
100+
minPoints: minPoints,
101+
publisherId: publisherId,
102+
tags: tags ?? this.tags,
103+
packages: packages ?? this.packages,
104+
order: order,
105+
offset: offset,
106+
limit: limit,
107+
textMatchExtent: textMatchExtent,
108+
);
109+
}
91110
}
92111

93112
/// The scope (depth) of the text matching.

pkg/_pub_shared/lib/search/tags.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,11 @@ abstract class PlatformTagValue {
151151
static const String windows = 'windows';
152152
}
153153

154+
/// Tags that control account-related search features.
155+
abstract class AccountTag {
156+
static const isLikedByMe = 'is:liked-by-me';
157+
}
158+
154159
/// Tags that may be relevant in search for packages that have preview or
155160
/// prerelease version published.
156161
const _futureVersionTags = <String>{

pkg/pub_integration/test/like_test.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'dart:convert';
66

77
import 'package:http/http.dart' as http;
88
import 'package:pub_integration/src/fake_test_context_provider.dart';
9+
import 'package:pub_integration/src/pub_puppeteer_helpers.dart';
910
import 'package:pub_integration/src/test_browser.dart';
1011
import 'package:test/test.dart';
1112

@@ -31,13 +32,31 @@ void main() {
3132
'defaultUser': 'admin@pub.dev',
3233
'generatedPackages': [
3334
{'name': 'test_pkg'},
35+
{'name': 'other_pkg'},
3436
],
3537
},
3638
},
3739
),
3840
);
3941

4042
final user = await fakeTestScenario.createTestUser(email: 'user@pub.dev');
43+
final anon = await fakeTestScenario.createAnonymousTestUser();
44+
45+
// checking that regular search returns two packages
46+
await user.withBrowserPage((page) async {
47+
await page.gotoOrigin('/packages?q=pkg');
48+
final info = await listingPageInfo(page);
49+
expect(info.packageNames.toSet(), {'test_pkg', 'other_pkg'});
50+
});
51+
52+
// checking that anonymous page request gets an error
53+
await anon.withBrowserPage((page) async {
54+
await page.gotoOrigin('/experimental?my-liked-search=1');
55+
await page.gotoOrigin('/packages?q=pkg+is:liked-by-me');
56+
expect(await page.content, contains('is only for authenticated users'));
57+
final info = await listingPageInfo(page);
58+
expect(info.packageNames, isEmpty);
59+
});
4160

4261
await user.withBrowserPage((page) async {
4362
Future<List<String>> getCountLabels() async {
@@ -53,13 +72,20 @@ void main() {
5372
];
5473
}
5574

75+
await page.gotoOrigin('/experimental?my-liked-search=1');
76+
5677
await page.gotoOrigin('/packages/test_pkg');
5778
expect(await getCountLabels(), ['0', '0', '']);
5879

5980
await page.click('.like-button-and-label--button');
6081
await Future.delayed(Duration(seconds: 1));
6182
expect(await getCountLabels(), ['1', '1', '']);
6283

84+
// checking search with my-liked packages
85+
await page.gotoOrigin('/packages?q=pkg+is:liked-by-me');
86+
final info = await listingPageInfo(page);
87+
expect(info.packageNames.toSet(), {'test_pkg'});
88+
6389
// displaying all three
6490
await page.gotoOrigin('/packages/test_pkg/score');
6591
expect(await getCountLabels(), ['1', '1', '1']);

0 commit comments

Comments
 (0)