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
23 changes: 23 additions & 0 deletions server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

import java.io.InputStream;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -1153,4 +1154,26 @@ public RegistryVersionJson getRegistryVersion() {
json.setVersion(registryVersion);
return json;
}

public ChangesResultJson getChanges(LocalDateTime since, LocalDateTime before, int size, int offset) {
var page = repositories.findChanges(since, before, size, offset);
var result = new ChangesResultJson();
result.setOffset(offset);
result.setTotalSize((int) page.getTotalElements());
result.setChanges(page.getContent().stream()
.map(ev -> {
var entry = new ChangeEntryJson();
entry.setNamespace(ev.getExtension().getNamespace().getName());
entry.setName(ev.getExtension().getName());
entry.setVersion(ev.getVersion());
entry.setTargetPlatform(ev.getTargetPlatform());
entry.setState(ev.getState().name());
entry.setTimestamp(TimeUtil.toUTCString(ev.getTimestamp()));
entry.setLastUpdated(TimeUtil.toUTCString(ev.getLastUpdated()));
entry.setExtension(ev.toExtensionJson());
return entry;
})
.toList());
return result;
}
}
58 changes: 58 additions & 0 deletions server/src/main/java/org/eclipse/openvsx/RegistryAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

import java.io.InputStream;
import java.net.URI;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -1358,6 +1359,63 @@ public ResponseEntity<ResultJson> deleteReview(@PathVariable String namespace, @
}
}

@GetMapping(
path = "/api/-/changes",
produces = MediaType.APPLICATION_JSON_VALUE
)
@CrossOrigin
@Operation(summary = "Provides a paginated feed of registry changes")
@ApiResponse(
responseCode = "200",
description = "The changes are returned in JSON format"
)
@ApiResponse(
responseCode = "400",
description = "The request contains an invalid parameter value"
)
public ResponseEntity<ChangesResultJson> getChanges(
@RequestParam(required = false)
@Parameter(description = "Only include changes at or after this timestamp (ISO-8601 UTC, e.g. 2024-01-01T00:00:00Z)")
String since,
@RequestParam(required = false)
@Parameter(description = "Only include changes before this timestamp, exclusive (ISO-8601 UTC, e.g. 2024-12-31T23:59:59Z)")
String until,
@RequestParam(defaultValue = "100")
@Parameter(description = "Maximal number of entries to return", schema = @Schema(type = "integer", minimum = "0", defaultValue = "100"))
int size,
@RequestParam(defaultValue = "0")
@Parameter(description = "Number of entries to skip (usually a multiple of the page size)", schema = @Schema(type = "integer", minimum = "0", defaultValue = "0"))
int offset
) {
if (size < 0) {
return new ResponseEntity<>(ChangesResultJson.error(negativeParameterMessage("size")), HttpStatus.BAD_REQUEST);
}
if (offset < 0) {
return new ResponseEntity<>(ChangesResultJson.error(negativeOffsetMessage()), HttpStatus.BAD_REQUEST);
}

LocalDateTime sinceDate = null;
LocalDateTime untilDate = null;
if (since != null) {
try {
sinceDate = TimeUtil.fromUTCString(since);
} catch (Exception e) {
return new ResponseEntity<>(ChangesResultJson.error("Invalid 'since' parameter: " + since), HttpStatus.BAD_REQUEST);
}
}
if (until != null) {
try {
untilDate = TimeUtil.fromUTCString(until);
} catch (Exception e) {
return new ResponseEntity<>(ChangesResultJson.error("Invalid 'until' parameter: " + until), HttpStatus.BAD_REQUEST);
}
}

return ResponseEntity.ok()
.cacheControl(CacheControl.noCache().cachePublic())
.body(local.getChanges(sinceDate, untilDate, size, offset));
}

@GetMapping(
path = "/api/-/public-key/{publicId}",
produces = MediaType.TEXT_PLAIN_VALUE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ public enum Type {
EXTENDED
}

public enum State {
ACTIVE, INACTIVE, DELETED
}

@Id
@GeneratedValue(generator = "extensionVersionSeq")
@SequenceGenerator(name = "extensionVersionSeq", sequenceName = "extension_version_seq")
Expand Down Expand Up @@ -77,6 +81,11 @@ public enum Type {

private boolean active;

@Enumerated(EnumType.STRING)
private State state = State.ACTIVE;

private LocalDateTime lastUpdated = TimeUtil.getCurrentUTC();

private boolean potentiallyMalicious;

private String displayName;
Expand Down Expand Up @@ -314,11 +323,32 @@ public void setPublishedWith(PersonalAccessToken publishedWith) {
}

public boolean isActive() {
return active;
return state == State.ACTIVE;
}

public void setActive(boolean active) {
this.active = active;
setState(active ? State.ACTIVE : State.INACTIVE);
Comment thread
gnugomez marked this conversation as resolved.
}

public State getState() {
return state;
}

public void setState(State state) {
if (this.state == State.DELETED && state != State.DELETED) {
throw new IllegalStateException("Cannot transition away from DELETED state");
}
this.active = state == State.ACTIVE;
this.state = state;
this.lastUpdated = TimeUtil.getCurrentUTC();
}

public LocalDateTime getLastUpdated() {
return lastUpdated;
}

public void setLastUpdated(LocalDateTime lastUpdated) {
this.lastUpdated = lastUpdated;
}

public boolean isPotentiallyMalicious() {
Expand Down
106 changes: 106 additions & 0 deletions server/src/main/java/org/eclipse/openvsx/json/ChangeEntryJson.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/********************************************************************************
* Copyright (c) 2026 Eclipse Foundation and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
package org.eclipse.openvsx.json;

import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;

@Schema(name = "ChangeEntry", description = "A single registry change entry")
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ChangeEntryJson {

@Schema(description = "Namespace of the extension")
private String namespace;

@Schema(description = "Name of the extension")
private String name;

@Schema(description = "Version string")
private String version;

@Schema(description = "Target platform (e.g. universal, linux-x64)")
private String targetPlatform;

@Schema(description = "Current state of this extension version (active, inactive, deleted)")
private String state;

@Schema(description = "Timestamp of the version publication (ISO-8601 UTC)")
private String timestamp;

@Schema(description = "Timestamp of the last state change (ISO-8601 UTC)")
private String lastUpdated;

@Schema(description = "Full extension metadata")
private ExtensionJson extension;

public String getNamespace() {
return namespace;
}

public void setNamespace(String namespace) {
this.namespace = namespace;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getVersion() {
return version;
}

public void setVersion(String version) {
this.version = version;
}

public String getTargetPlatform() {
return targetPlatform;
}

public void setTargetPlatform(String targetPlatform) {
this.targetPlatform = targetPlatform;
}

public String getState() {
return state;
}

public void setState(String state) {
this.state = state;
}

public String getTimestamp() {
return timestamp;
}

public void setTimestamp(String timestamp) {
this.timestamp = timestamp;
}

public String getLastUpdated() {
return lastUpdated;
}

public void setLastUpdated(String lastUpdated) {
this.lastUpdated = lastUpdated;
}

public ExtensionJson getExtension() {
return extension;
}

public void setExtension(ExtensionJson extension) {
this.extension = extension;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/********************************************************************************
* Copyright (c) 2026 Eclipse Foundation and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
package org.eclipse.openvsx.json;

import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;

import java.util.List;

@Schema(name = "ChangesResult", description = "Paginated list of registry changes")
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ChangesResultJson extends ResultJson {

public static ChangesResultJson error(String message) {
var result = new ChangesResultJson();
result.setError(message);
return result;
}

@Schema(description = "Number of skipped entries according to the changes request")
@NotNull
@Min(0)
private int offset;

@Schema(description = "Total number of changes matching the request")
@NotNull
@Min(0)
private int totalSize;

@Schema(description = "List of change entries, limited to the size specified in the request")
@NotNull
private List<ChangeEntryJson> changes;

public int getOffset() {
return offset;
}

public void setOffset(int offset) {
this.offset = offset;
}

public int getTotalSize() {
return totalSize;
}

public void setTotalSize(int totalSize) {
this.totalSize = totalSize;
}

public List<ChangeEntryJson> getChanges() {
return changes;
}

public void setChanges(List<ChangeEntryJson> changes) {
this.changes = changes;
}
}
Loading