Skip to content
Merged
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
13 changes: 7 additions & 6 deletions app/lib/account/like_backend.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,14 @@ class LikeBackend {
}

/// Returns a list with [LikeData] of all the packages that the given
/// [user] likes.
Future<List<LikeData>> listPackageLikes(User user) async {
return (await cache.userPackageLikes(user.userId).get(() async {
/// user's likes.
Future<List<LikeData>> listPackageLikes(String userId) async {
return (await cache.userPackageLikes(userId).get(() async {
// TODO(zarah): Introduce pagination and/or migrate this to search.
final query = _db.query<Like>(ancestorKey: user.key)
..order('-created')
..limit(1000);
final query =
_db.query<Like>(ancestorKey: _db.emptyKey.append(User, id: userId))
..order('-created')
..limit(1000);
final likes = await query.run().toList();
return likes.map((Like l) => LikeData.fromModel(l)).toList();
}))!;
Expand Down
2 changes: 1 addition & 1 deletion app/lib/admin/backend.dart
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ class AdminBackend {
Future<void> _removeAndDecrementLikes(User user) async {
final pool = Pool(5);
final futures = <Future>[];
for (final like in await likeBackend.listPackageLikes(user)) {
for (final like in await likeBackend.listPackageLikes(user.userId)) {
final f = pool
.withResource(() => likeBackend.unlikePackage(user, like.package!));
futures.add(f);
Expand Down
4 changes: 2 additions & 2 deletions app/lib/frontend/handlers/account.dart
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ Future<LikedPackagesResponse> listPackageLikesHandler(
shelf.Request request) async {
final authenticatedUser = await requireAuthenticatedWebUser();
final user = authenticatedUser.user;
final packages = await likeBackend.listPackageLikes(user);
final packages = await likeBackend.listPackageLikes(user.userId);
final List<PackageLikeResponse> packageLikes = packages
.map((like) => PackageLikeResponse(
liked: true, package: like.package, created: like.created))
Expand Down Expand Up @@ -294,7 +294,7 @@ Future<shelf.Response> accountMyLikedPackagesPageHandler(

final user = (await accountBackend
.lookupUserById(requestContext.authenticatedUserId!))!;
final likes = await likeBackend.listPackageLikes(user);
final likes = await likeBackend.listPackageLikes(user.userId);
final html = renderMyLikedPackagesPage(
user: user,
userSessionData: requestContext.sessionData!,
Expand Down
8 changes: 2 additions & 6 deletions app/lib/frontend/handlers/experimental.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,11 @@ typedef PublicFlag = ({String name, String description});

const _publicFlags = <PublicFlag>{
(name: 'example', description: 'Short description'),
(
name: 'trending-search',
description: 'Show trending packages and search by trending scores'
),
};

final _allFlags = <String>{
'dark-as-default',
'search-post',
'my-liked-search',
..._publicFlags.map((x) => x.name),
};

Expand Down Expand Up @@ -93,7 +89,7 @@ class ExperimentalFlags {

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

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

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

Expand Down
8 changes: 8 additions & 0 deletions app/lib/search/mem_index.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'dart:math' as math;

import 'package:_pub_shared/search/search_form.dart';
import 'package:_pub_shared/search/search_request_data.dart';
import 'package:_pub_shared/search/tags.dart';
import 'package:clock/clock.dart';
import 'package:collection/collection.dart';
import 'package:logging/logging.dart';
Expand Down Expand Up @@ -126,6 +127,13 @@ class InMemoryPackageIndex {
if (query.offset >= _documents.length) {
return PackageSearchResult.empty();
}
// Special case for the account-related tag.
if (query.parsedQuery.tagsPredicate.hasTag(AccountTag.isLikedByMe)) {
return PackageSearchResult.error(
statusCode: 400,
errorMessage: '`is:liked-by-me` is only for authenticated users.',
);
}
return _bitArrayPool.withPoolItem(fn: (array) {
return _scorePool.withItemGetter(
(scoreFn) {
Expand Down
33 changes: 30 additions & 3 deletions app/lib/search/search_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
import 'dart:async';
import 'dart:convert';

import 'package:_pub_shared/search/tags.dart';
import 'package:_pub_shared/utils/http.dart';
import 'package:clock/clock.dart';
import 'package:gcloud/service_scope.dart' as ss;
import 'package:pub_dev/frontend/request_context.dart';

import '../../../service/rate_limit/rate_limit.dart';
import '../../account/like_backend.dart';
import '../../frontend/request_context.dart';
import '../shared/configuration.dart';
import '../shared/redis_cache.dart' show cache;
import '../shared/utils.dart';
Expand Down Expand Up @@ -66,15 +68,40 @@ class SearchClient {
skipCache = true;
}

final hasLikedByMeTag =
query.parsedQuery.tagsPredicate.hasTag(AccountTag.isLikedByMe);
final userId = requestContext.sessionData?.userId;
if (hasLikedByMeTag) {
skipCache = true;
}

List<String>? packages;
if (userId != null && hasLikedByMeTag) {
final likedPackages = await likeBackend.listPackageLikes(userId);
packages = likedPackages.map((l) => l.package!).toList();
}

// Returns the status code and the body of the last response, or null on timeout.
Future<({int statusCode, String? body})?> doCallHttpServiceEndpoint(
{String? prefix}) async {
final httpHostPort = prefix ?? activeConfiguration.searchServicePrefix;
try {
if (requestContext.experimentalFlags.useSearchPost) {
if (requestContext.experimentalFlags.useMyLikedSearch) {
return await withRetryHttpClient(
(client) async {
final data = query.toSearchRequestData();
var data = query.toSearchRequestData();
if (userId != null && hasLikedByMeTag) {
final newQuery =
data.query?.replaceAll(AccountTag.isLikedByMe, ' ').trim();
final newTags = data.tags!
.where((e) => e != AccountTag.isLikedByMe)
.toList();
data = data.replace(
query: newQuery,
tags: newTags,
packages: packages,
);
}
// NOTE: Keeping the query parameter to help investigating logs.
final uri = Uri.parse('$httpHostPort/search').replace(
queryParameters: {
Expand Down
8 changes: 4 additions & 4 deletions app/test/admin/user_merger_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -150,17 +150,17 @@ void main() {
final admin = await accountBackend.lookupUserByEmail('admin@pub.dev');
await likeBackend.likePackage(admin, 'oxygen');
expect(
(await likeBackend.listPackageLikes(admin))
(await likeBackend.listPackageLikes(admin.userId))
.map((e) => e.package)
.toList(),
['oxygen']);
expect(await likeBackend.listPackageLikes(user), isEmpty);
expect(await likeBackend.listPackageLikes(user.userId), isEmpty);

await _corruptAndFix();

expect(await likeBackend.listPackageLikes(admin), isEmpty);
expect(await likeBackend.listPackageLikes(admin.userId), isEmpty);
expect(
(await likeBackend.listPackageLikes(user))
(await likeBackend.listPackageLikes(user.userId))
.map((e) => e.package)
.toList(),
['oxygen']);
Expand Down
19 changes: 19 additions & 0 deletions pkg/_pub_shared/lib/search/search_request_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,25 @@ class SearchRequestData {
map.removeWhere((k, v) => v == null);
return map;
}

/// Creates a new instance with fields being replaced (if provided).
SearchRequestData replace({
String? query,
List<String>? tags,
List<String>? packages,
}) {
return SearchRequestData(
query: query ?? this.query,
minPoints: minPoints,
publisherId: publisherId,
tags: tags ?? this.tags,
packages: packages ?? this.packages,
order: order,
offset: offset,
limit: limit,
textMatchExtent: textMatchExtent,
);
}
}

/// The scope (depth) of the text matching.
Expand Down
5 changes: 5 additions & 0 deletions pkg/_pub_shared/lib/search/tags.dart
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@ abstract class PlatformTagValue {
static const String windows = 'windows';
}

/// Tags that control account-related search features.
abstract class AccountTag {
static const isLikedByMe = 'is:liked-by-me';
}

/// Tags that may be relevant in search for packages that have preview or
/// prerelease version published.
const _futureVersionTags = <String>{
Expand Down
26 changes: 26 additions & 0 deletions pkg/pub_integration/test/like_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'dart:convert';

import 'package:http/http.dart' as http;
import 'package:pub_integration/src/fake_test_context_provider.dart';
import 'package:pub_integration/src/pub_puppeteer_helpers.dart';
import 'package:pub_integration/src/test_browser.dart';
import 'package:test/test.dart';

Expand All @@ -31,13 +32,31 @@ void main() {
'defaultUser': 'admin@pub.dev',
'generatedPackages': [
{'name': 'test_pkg'},
{'name': 'other_pkg'},
],
},
},
),
);

final user = await fakeTestScenario.createTestUser(email: 'user@pub.dev');
final anon = await fakeTestScenario.createAnonymousTestUser();

// checking that regular search returns two packages
await user.withBrowserPage((page) async {
await page.gotoOrigin('/packages?q=pkg');
final info = await listingPageInfo(page);
expect(info.packageNames.toSet(), {'test_pkg', 'other_pkg'});
});

// checking that anonymous page request gets an error
await anon.withBrowserPage((page) async {
await page.gotoOrigin('/experimental?my-liked-search=1');
await page.gotoOrigin('/packages?q=pkg+is:liked-by-me');
expect(await page.content, contains('is only for authenticated users'));
final info = await listingPageInfo(page);
expect(info.packageNames, isEmpty);
});

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

await page.gotoOrigin('/experimental?my-liked-search=1');

await page.gotoOrigin('/packages/test_pkg');
expect(await getCountLabels(), ['0', '0', '']);

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

// checking search with my-liked packages
await page.gotoOrigin('/packages?q=pkg+is:liked-by-me');
final info = await listingPageInfo(page);
expect(info.packageNames.toSet(), {'test_pkg'});

// displaying all three
await page.gotoOrigin('/packages/test_pkg/score');
expect(await getCountLabels(), ['1', '1', '1']);
Expand Down