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
147 changes: 147 additions & 0 deletions server/src/main/java/org/eclipse/openvsx/admin/ConsistencyAPI.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/******************************************************************************
* Copyright (c) 2026 Contributors to the Eclipse Foundation.
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* https://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
package org.eclipse.openvsx.admin;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import org.eclipse.openvsx.consistency.ConsistencyCheckService;
import org.eclipse.openvsx.consistency.ConsistencyCheckSummary;
import org.eclipse.openvsx.consistency.ConsistencyFinding;
import org.eclipse.openvsx.json.ConsistencyCheckJson;
import org.eclipse.openvsx.json.ConsistencyCheckListJson;
import org.eclipse.openvsx.json.ConsistencyFindingJson;
import org.eclipse.openvsx.json.ConsistencyFindingListJson;
import org.eclipse.openvsx.json.ResultJson;
import org.eclipse.openvsx.settings.MutatingOperation;
import org.eclipse.openvsx.util.ErrorResultException;
import org.eclipse.openvsx.util.NotFoundException;

/**
* Admin dashboard endpoints for the data consistency checks (see #1622): a live overview of every
* registered {@link org.eclipse.openvsx.consistency.ConsistencyCheck}, its findings, and actions to fix
* them - one at a time or all at once. There is no "run now" action here: findings are always
* recomputed live, and the scheduled sweep that auto-fixes what it can runs independently of this page.
*/
@RestController
@RequestMapping("/admin/consistency")
@ApiResponse(
responseCode = "403",
description = "Administration role is required",
content = @Content()
)
public class ConsistencyAPI {

private final AdminService admins;
private final ConsistencyCheckService service;

public ConsistencyAPI(AdminService admins, ConsistencyCheckService service) {
this.admins = admins;
this.service = service;
}

@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "Get an overview of every registered consistency check")
public ResponseEntity<ConsistencyCheckListJson> listChecks() {
try {
admins.checkAdminUser();
var json = new ConsistencyCheckListJson();
json.setChecks(
service.listSummaries().stream()
.map(ConsistencyAPI::toJson)
.toList());
return ResponseEntity.ok(json);
} catch (ErrorResultException exc) {
return exc.toResponseEntity(ConsistencyCheckListJson.class);
}
}

@GetMapping(path = "/{checkId}/findings", produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "Get the current findings of one consistency check")
public ResponseEntity<ConsistencyFindingListJson> findings(@PathVariable String checkId) {
try {
admins.checkAdminUser();
var json = new ConsistencyFindingListJson();
json.setFindings(
service.findings(checkId).stream()
.map(ConsistencyAPI::toJson)
.toList());
return ResponseEntity.ok(json);
} catch (NotFoundException exc) {
var json = ConsistencyFindingListJson.error("Unknown consistency check: " + checkId);
return new ResponseEntity<>(json, HttpStatus.NOT_FOUND);
} catch (ErrorResultException exc) {
return exc.toResponseEntity(ConsistencyFindingListJson.class);
}
}

@PostMapping(path = "/{checkId}/fix", produces = MediaType.APPLICATION_JSON_VALUE)
@MutatingOperation
@Operation(summary = "Fix every current finding of one consistency check")
public ResponseEntity<ResultJson> fixAll(@PathVariable String checkId) {
try {
admins.checkAdminUser();
var fixed = service.fixAll(checkId);
return ResponseEntity.ok(ResultJson.success("Fixed " + fixed + " finding(s) for check '" + checkId + "'."));
} catch (NotFoundException exc) {
return new ResponseEntity<>(
ResultJson.error("Unknown consistency check: " + checkId),
HttpStatus.NOT_FOUND);
} catch (ErrorResultException exc) {
return exc.toResponseEntity();
}
}

@PostMapping(path = "/{checkId}/fix/{entityId}", produces = MediaType.APPLICATION_JSON_VALUE)
@MutatingOperation
@Operation(summary = "Fix a single finding of one consistency check")
public ResponseEntity<ResultJson> fixOne(@PathVariable String checkId, @PathVariable long entityId) {
try {
admins.checkAdminUser();
service.fixOne(checkId, entityId);
return ResponseEntity.ok(ResultJson.success("Fixed entity " + entityId + " for check '" + checkId + "'."));
} catch (NotFoundException exc) {
return new ResponseEntity<>(
ResultJson.error("Unknown consistency check: " + checkId),
HttpStatus.NOT_FOUND);
} catch (ErrorResultException exc) {
return exc.toResponseEntity();
}
}

private static ConsistencyCheckJson toJson(ConsistencyCheckSummary summary) {
var json = new ConsistencyCheckJson();
json.setId(summary.id());
json.setName(summary.name());
json.setDescription(summary.description());
json.setCurrentFindingsCount(summary.currentFindingsCount());
return json;
}

private static ConsistencyFindingJson toJson(ConsistencyFinding finding) {
var json = new ConsistencyFindingJson();
json.setEntityId(finding.entityId());
json.setLabel(finding.label());
json.setDetail(finding.detail());
return json;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/******************************************************************************
* Copyright (c) 2026 Contributors to the Eclipse Foundation.
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* https://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*****************************************************************************/
package org.eclipse.openvsx.consistency;

import java.util.List;

/**
* A single, self-contained check for one kind of data inconsistency (see issue #1622: "Need a way to
* check the database for consistency"). Implementations are picked up automatically as Spring beans by
* {@link ConsistencyCheckService} - registering a new kind of check is exactly one new {@code @Component}
* implementing this interface, with no other wiring needed.
* <p>
* Findings are always computed live from current data, never cached: a stored list of affected entities
* would go stale the moment anything about them changes, which is exactly the kind of silent drift this
* feature exists to catch.
*/
public interface ConsistencyCheck {

/**
* A stable, unique identifier for this check (e.g. {@code "extension-active-flag"}). Used as the
* path segment in the admin API and as the key under which run history is recorded, so it must
* never change once a check has shipped.
*/
String getId();

/**
* A short, human-readable name shown in the admin UI.
*/
String getName();

/**
* Explains what this check looks for and why it matters, shown in the admin UI.
*/
String getDescription();

/**
* Runs the check now and returns every entity currently found inconsistent. Empty means healthy.
*/
List<ConsistencyFinding> check();

/**
* Repairs the entity identified by {@code entityId} (one of {@link ConsistencyFinding#entityId()}
* from a prior {@link #check()} call). A no-op if the entity no longer exists or is no longer
* inconsistent (e.g. it was already fixed by something else in the meantime).
*/
void fix(long entityId);

/**
* Whether the scheduled sweep (and the admin dashboard's "run now" action) should automatically fix
* this check's findings, rather than only recording them in run history for a human to fix from the
* dashboard. Defaults to {@code true}: a purely mechanical recomputation like
* {@link ExtensionActiveFlagCheck} is always safe to fix unattended. Override to return
* {@code false} only when fixing requires a judgment call a human needs to make - e.g. deciding
* which of two conflicting records is the correct one - not merely because a check is new.
*/
default boolean autoFixOnSchedule() {
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/******************************************************************************
* Copyright (c) 2026 Contributors to the Eclipse Foundation.
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* https://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
package org.eclipse.openvsx.consistency;

import org.jobrunr.jobs.annotations.Job;
import org.jobrunr.jobs.lambdas.JobRequestHandler;
import org.springframework.stereotype.Component;

import org.eclipse.openvsx.migration.HandlerJobRequest;

@Component
public class ConsistencyCheckJobRequestHandler implements JobRequestHandler<HandlerJobRequest<?>> {

private final ConsistencyCheckService service;

public ConsistencyCheckJobRequestHandler(ConsistencyCheckService service) {
this.service = service;
}

@Override
@Job(name = "Run data consistency checks", retries = 0)
public void run(HandlerJobRequest<?> jobRequest) {
service.runAllChecks();
}
}
Loading