Skip to content

Commit 4dce8b3

Browse files
authored
Package[Version] new fields: isAdminDeleted, adminDeletedAt (#8778)
1 parent 3ab0492 commit 4dce8b3

File tree

11 files changed

+94
-17
lines changed

11 files changed

+94
-17
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ AppEngine version, listed here to ease deployment and troubleshooting.
55
* Bump runtimeVersion to `2025.05.21`.
66
* Upgraded runtime Dart SDK to `3.8.0`.
77
* Upgraded stable Flutter analysis SDK to `3.32.0`.
8+
* Note: started to backfill `Package[Version]`'s `isAdminDeleted` field.
89

910
## `20250519t093200-all`
1011

app/lib/admin/actions/package_version_retraction.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ value of `set-retracted`, which should either be `true` or `false`.
6969
if (pv == null) {
7070
throw NotFoundException.resource(version);
7171
}
72-
if (pv.isModerated) {
72+
if (pv.isNotVisible) {
7373
throw ModeratedException.packageVersion(packageName, version);
7474
}
7575

app/lib/frontend/handlers/atom_feed.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Future<shelf.Response> packageAtomFeedhandler(
5252
/// Builds the content of the /feed.atom endpoint.
5353
Future<String> buildAllPackagesAtomFeedContent() async {
5454
final versions = await packageBackend.latestPackageVersions(limit: 100);
55-
versions.removeWhere((pv) => pv.isModerated || pv.isRetracted);
55+
versions.removeWhere((pv) => pv.isNotVisible || pv.isRetracted);
5656
final feed = _allPackagesFeed(versions);
5757
return feed.toXmlDocument();
5858
}
@@ -69,7 +69,7 @@ Future<String> buildPackageAtomFeedContent(String package) async {
6969
limit: 10,
7070
)
7171
.toList();
72-
versions.removeWhere((pv) => pv.isModerated || pv.isRetracted);
72+
versions.removeWhere((pv) => pv.isNotVisible || pv.isRetracted);
7373
final feed = _packageFeed(package, versions);
7474
return feed.toXmlDocument();
7575
}

app/lib/frontend/handlers/package.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ Future<shelf.Response> _handlePackagePage({
317317
} on NotFoundException {
318318
return formattedNotFoundHandler(request);
319319
}
320-
if (data.version.isModerated) {
320+
if (data.version.isNotVisible) {
321321
final content = renderModeratedPackagePage(packageName);
322322
return htmlResponse(content, status: 404);
323323
}

app/lib/package/backend.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@ class PackageBackend {
501501
if (pv == null) {
502502
throw NotFoundException.resource(version);
503503
}
504-
if (pv.isModerated) {
504+
if (pv.isNotVisible) {
505505
throw ModeratedException.packageVersion(package, version);
506506
}
507507

@@ -797,7 +797,7 @@ class PackageBackend {
797797
throw NotFoundException.resource('package "$package"');
798798
}
799799
final packageVersions = (await packageBackend.versionsOfPackage(package))
800-
.where((v) => !v.isModerated)
800+
.where((v) => v.isVisible)
801801
.toList();
802802
if (packageVersions.isEmpty) {
803803
throw NotFoundException.resource('package "$package"');

app/lib/package/models.dart

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,15 @@ class Package extends db.ExpandoModel<String> {
129129
@db.DateTimeProperty()
130130
DateTime? moderatedAt;
131131

132+
/// `true` if package was deleted by admins (pending final deletion).
133+
/// TODO: mark `required: true` after backfill is done and all runtimes use the field
134+
@db.BoolProperty(required: false)
135+
bool? isAdminDeleted;
136+
137+
/// The timestamp when the package was deleted by admins.
138+
@db.DateTimeProperty()
139+
DateTime? adminDeletedAt;
140+
132141
/// Tags that are assigned to this package.
133142
///
134143
/// The permissions required to assign a tag typically depends on the tag.
@@ -179,19 +188,19 @@ class Package extends db.ExpandoModel<String> {
179188
..isDiscontinued = false
180189
..isUnlisted = false
181190
..isModerated = false
191+
..isAdminDeleted = false
182192
..assignedTags = []
183193
..deletedVersions = [];
184194
}
185195

186196
// Convenience Fields:
187197

188-
bool get isVisible => !isModerated;
198+
bool get isVisible => !isModerated && !(isAdminDeleted ?? false);
189199
bool get isNotVisible => !isVisible;
190200

191201
bool get isIncludedInRobots {
192202
final now = clock.now();
193203
return isVisible &&
194-
!isModerated &&
195204
!isDiscontinued &&
196205
!isUnlisted &&
197206
now.difference(created!) > robotsVisibilityMinAge &&
@@ -284,8 +293,8 @@ class Package extends db.ExpandoModel<String> {
284293
.toList();
285294

286295
final isAllRetracted = versions.every((v) => v.isRetracted);
287-
final isAllModerated = versions.every((v) => v.isModerated);
288-
if (isAllModerated) {
296+
final noVisibleVersions = versions.every((v) => v.isNotVisible);
297+
if (noVisibleVersions) {
289298
throw NotAcceptableException('No visible versions left.');
290299
}
291300

@@ -301,7 +310,7 @@ class Package extends db.ExpandoModel<String> {
301310

302311
for (final pv in versions) {
303312
// Skip all moderated versions.
304-
if (pv.isModerated) {
313+
if (pv.isNotVisible) {
305314
continue;
306315
}
307316

@@ -586,13 +595,26 @@ class PackageVersion extends db.ExpandoModel<String> {
586595
@db.DateTimeProperty()
587596
DateTime? moderatedAt;
588597

598+
/// `true` if package version was deleted by admins (pending final deletion).
599+
/// TODO: mark `required: true` after backfill is done and all runtimes use the field
600+
@db.BoolProperty(required: false)
601+
bool? isAdminDeleted;
602+
603+
/// The timestamp when the package version was deleted by admins.
604+
@db.DateTimeProperty()
605+
DateTime? adminDeletedAt;
606+
589607
PackageVersion();
590608

591609
PackageVersion.init() {
592610
isModerated = false;
611+
isAdminDeleted = false;
593612
isRetracted = false;
594613
}
595614

615+
late final isVisible = !isModerated && !(isAdminDeleted ?? false);
616+
late final isNotVisible = !isVisible;
617+
596618
// Convenience Fields:
597619

598620
late final semanticVersion = Version.parse(version!);

app/lib/package/tarball_storage.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ class TarballStorage {
217217
if (lastPackage?.name != pv.package) {
218218
lastPackage = await packageBackend.lookupPackage(pv.package);
219219
}
220-
final isModerated = lastPackage!.isModerated || pv.isModerated;
220+
final isModerated = lastPackage!.isNotVisible || pv.isNotVisible;
221221

222222
final objectName = tarballObjectName(pv.package, pv.version!);
223223
final publicInfo = await _publicBucket.tryInfo(objectName);

app/lib/scorecard/backend.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ class ScoreCardBackend {
142142
throw NotFoundException(
143143
'Package version "$packageName $packageVersion" does not exist.');
144144
}
145-
if (version.isModerated) {
145+
if (version.isNotVisible) {
146146
throw ModeratedException.packageVersion(packageName, packageVersion);
147147
}
148148
final status = PackageStatus.fromModels(package, version);

app/lib/shared/integrity.dart

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ class IntegrityChecker {
383383
// while the integrity check is running.
384384
if (!pv.created!.isAfter(p.lastVersionPublished!)) {
385385
// Moderated versions are not counted.
386-
if (!pv.isModerated) {
386+
if (pv.isVisible) {
387387
versionCountUntilLastPublished++;
388388
}
389389
}
@@ -442,6 +442,12 @@ class IntegrityChecker {
442442
isModerated: p.isModerated,
443443
moderatedAt: p.moderatedAt,
444444
);
445+
yield* _checkAdminDeletedFlags(
446+
kind: 'Package',
447+
id: p.name!,
448+
isAdminDeleted: p.isAdminDeleted ?? false,
449+
adminDeletedAt: p.adminDeletedAt,
450+
);
445451
if (p.isModerated) {
446452
_packagesWithIsModeratedFlag.add(p.name!);
447453
}
@@ -575,7 +581,7 @@ class IntegrityChecker {
575581
yield 'PackageVersion "${pv.qualifiedVersionKey}" is retracted, but `retracted` property is null.';
576582
}
577583
final shouldBeInPublicBucket =
578-
!_packagesWithIsModeratedFlag.contains(pv.package) && !pv.isModerated;
584+
!_packagesWithIsModeratedFlag.contains(pv.package) && pv.isVisible;
579585
final tarballItems = await retry(
580586
() async {
581587
return await _checkTarballInBuckets(pv, archiveDownloadUri,
@@ -592,6 +598,12 @@ class IntegrityChecker {
592598
isModerated: pv.isModerated,
593599
moderatedAt: pv.moderatedAt,
594600
);
601+
yield* _checkAdminDeletedFlags(
602+
kind: 'PackageVersion',
603+
id: pv.qualifiedVersionKey.toString(),
604+
isAdminDeleted: pv.isAdminDeleted ?? false,
605+
adminDeletedAt: pv.adminDeletedAt,
606+
);
595607

596608
// Sanity checks for the `created` property
597609
if (pv.created == null) {
@@ -1012,3 +1024,18 @@ Stream<String> _checkModeratedFlags({
10121024
yield '$kind "$id" has `isModerated = false` but `moderatedAt` is not null.';
10131025
}
10141026
}
1027+
1028+
/// Check that `isAdminDeleted` and `adminDeletedAt` are consistent.
1029+
Stream<String> _checkAdminDeletedFlags({
1030+
required String kind,
1031+
required String id,
1032+
required bool isAdminDeleted,
1033+
required DateTime? adminDeletedAt,
1034+
}) async* {
1035+
if (isAdminDeleted && adminDeletedAt == null) {
1036+
yield '$kind "$id" has `isAdminDeleted = true` but `adminDeletedAt` is null.';
1037+
}
1038+
if (!isAdminDeleted && adminDeletedAt != null) {
1039+
yield '$kind "$id" has `isAdminDeleted = false` but `adminDeletedAt` is not null.';
1040+
}
1041+
}

app/lib/task/backend.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1179,7 +1179,7 @@ List<Version> _versionsToTrack(
11791179
// Ignore retracted versions
11801180
.where((pv) => !pv.isRetracted)
11811181
// Ignore moderated versions
1182-
.where((pv) => !pv.isModerated)
1182+
.where((pv) => pv.isVisible)
11831183
.map((pv) => pv.semanticVersion)
11841184
.toSet();
11851185
final visibleStableVersions = visibleVersions

app/lib/tool/backfill/backfill_new_fields.dart

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'package:logging/logging.dart';
6+
import 'package:pub_dev/package/models.dart';
7+
import 'package:pub_dev/shared/datastore.dart';
68

79
final _logger = Logger('backfill_new_fields');
810

@@ -12,5 +14,30 @@ final _logger = Logger('backfill_new_fields');
1214
/// CHANGELOG.md must be updated with the new fields, and the next
1315
/// release could remove the backfill from here.
1416
Future<void> backfillNewFields() async {
15-
_logger.info('No new fields.');
17+
_logger.info('Backfill admin deleted fields...');
18+
await for (final e in dbService.query<Package>().run()) {
19+
if (e.isAdminDeleted == null) {
20+
await withRetryTransaction(dbService, (tx) async {
21+
final p = await tx.lookupOrNull<Package>(e.key);
22+
if (p == null) {
23+
return;
24+
}
25+
p.isAdminDeleted ??= false;
26+
tx.insert(p);
27+
});
28+
}
29+
}
30+
31+
await for (final e in dbService.query<PackageVersion>().run()) {
32+
if (e.isAdminDeleted == null) {
33+
await withRetryTransaction(dbService, (tx) async {
34+
final p = await tx.lookupOrNull<PackageVersion>(e.key);
35+
if (p == null) {
36+
return;
37+
}
38+
p.isAdminDeleted ??= false;
39+
tx.insert(p);
40+
});
41+
}
42+
}
1643
}

0 commit comments

Comments
 (0)