Skip to content
Open
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
57 changes: 57 additions & 0 deletions server/src/main/java/org/eclipse/openvsx/admin/AdminAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,63 @@ public ResponseEntity<ResultJson> editNamespaceMember(
}
}

@DeleteMapping(
path = "/admin/namespace/{namespaceName}",
produces = MediaType.APPLICATION_JSON_VALUE
)
@CrossOrigin
@Operation(summary = "Delete a namespace and all its extensions")
@ApiResponse(
responseCode = "200",
description = "The namespace was successfully deleted",
content = @Content(schema = @Schema(implementation = ResultJson.class))
)
@ApiResponse(
responseCode = "404",
description = "Namespace not found",
content = @Content(schema = @Schema(implementation = ResultJson.class))
)
public ResponseEntity<ResultJson> deleteNamespace(
@PathVariable @Parameter(description = "Namespace name", example = "my-namespace") String namespaceName
) {
try {
var adminUser = admins.checkAdminUser();
var result = admins.deleteNamespace(namespaceName, adminUser);
return ResponseEntity.ok(result);
} catch (ErrorResultException exc) {
return exc.toResponseEntity();
}
}

@DeleteMapping(
path = "/admin/api/namespace/{namespaceName}",
produces = MediaType.APPLICATION_JSON_VALUE
)
@CrossOrigin
@Operation(summary = "Delete a namespace and all its extensions (with token authentication)")
@ApiResponse(
responseCode = "200",
description = "The namespace was successfully deleted",
content = @Content(schema = @Schema(implementation = ResultJson.class))
)
@ApiResponse(
responseCode = "404",
description = "Namespace not found",
content = @Content(schema = @Schema(implementation = ResultJson.class))
)
public ResponseEntity<ResultJson> deleteNamespaceWithToken(
@PathVariable @Parameter(description = "Namespace name", example = "my-namespace") String namespaceName,
@RequestParam(value = "token") @Parameter(description = "A personal access token") String tokenValue
) {
try {
var adminUser = admins.checkAdminUser(tokenValue);
var result = admins.deleteNamespace(namespaceName, adminUser);
return ResponseEntity.ok(result);
} catch (ErrorResultException exc) {
return exc.toResponseEntity();
}
}

@GetMapping(
path = "/admin/publisher/{provider}/{loginName}",
produces = MediaType.APPLICATION_JSON_VALUE
Expand Down
61 changes: 61 additions & 0 deletions server/src/main/java/org/eclipse/openvsx/admin/AdminService.java
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,67 @@ public ResultJson createNamespace(NamespaceJson json) {
return ResultJson.success("Created namespace " + namespace.getName());
}

@Transactional(rollbackOn = ErrorResultException.class)
public ResultJson deleteNamespace(String namespaceName, UserData admin) {
var namespace = repositories.findNamespace(namespaceName);
if (namespace == null) {
throw new ErrorResultException("Namespace not found: " + namespaceName, HttpStatus.NOT_FOUND);
}

// Get all extensions in the namespace
var extensions = repositories.findExtensions(namespace);

// Delete all extensions and their versions
for (var extension : extensions) {
// Remove reviews (associated with extension, not version)
var reviews = repositories.findAllReviews(extension);
for (var review : reviews) {
entityManager.remove(review);
}

// Remove all versions and their resources
var versions = repositories.findVersions(extension);
for (var version : versions) {
// Remove file resources
var resources = repositories.findFiles(version);
for (var resource : resources) {
storageUtil.removeFile(resource);
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File removal should be done asynchronously via job scheduler, similar to removeExtensionVersion() on line 243 which uses scheduler.enqueue(new RemoveFileJobRequest(resource)). This prevents blocking the transaction on potentially slow external storage operations.

Suggested change
storageUtil.removeFile(resource);
scheduler.enqueue(new RemoveFileJobRequest(resource));

Copilot uses AI. Check for mistakes.
entityManager.remove(resource);
}

// Remove the version
entityManager.remove(version);
}

// Clear cache for the extension
cache.evictExtensionJsons(extension);
cache.evictLatestExtensionVersion(extension);

// Remove the extension
entityManager.remove(extension);
}
Comment on lines +338 to +365
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The namespace deletion logic does not handle deprecated extension references. When an extension is deleted, other extensions that reference it as a replacement should have their replacement field cleared. See deleteExtension() on lines 217-221 which handles this with findDeprecatedExtensions().

Copilot uses AI. Check for mistakes.

// Remove all namespace memberships
var memberships = repositories.findMemberships(namespace);
for (var membership : memberships) {
entityManager.remove(membership);
}

// Clear cache for the namespace
cache.evictNamespaceDetails(namespace);
cache.evictSitemap();

// Remove the namespace
entityManager.remove(namespace);

// Update search index
search.updateSearchEntries(extensions.toList());
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After deleting extensions, updateSearchEntries should not be called with the deleted extensions. This will attempt to re-index extensions that have just been removed. Use removeSearchEntries with extension IDs instead, or call search.removeSearchEntry() for each extension before removal.

Copilot uses AI. Check for mistakes.

var result = ResultJson.success("Deleted namespace " + namespaceName + " with " + extensions.toList().size() + " extension(s)");
logAdminAction(admin, result);
return result;
}

public void changeNamespace(ChangeNamespaceJson json) {
if (StringUtils.isEmpty(json.oldNamespace())) {
throw new ErrorResultException("Old namespace must have a value");
Expand Down
Loading
Loading