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
35 changes: 21 additions & 14 deletions src/main/java/io/github/randomcodespace/iq/api/GraphController.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

import io.github.randomcodespace.iq.model.NodeKind;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
* REST API controller matching the Python OSSCodeIQ API paths.
Expand Down Expand Up @@ -62,7 +66,8 @@
@RequestParam(defaultValue = "50") int limit,
@RequestParam(defaultValue = "0") int offset) {
requireQueryService();
return queryService.nodesByKind(kind, Math.min(limit, 1000), offset);
validateNodeKind(kind);
return queryService.nodesByKind(kind, Math.min(limit, 1000), Math.max(0, offset));
}

@GetMapping("/nodes")
Expand All @@ -71,13 +76,10 @@
@RequestParam(defaultValue = "100") int limit,
@RequestParam(defaultValue = "0") int offset) {
requireQueryService();
return queryService.listNodes(kind, Math.min(limit, 1000), offset);
}

@GetMapping("/nodes/find")
public List<Map<String, Object>> findNode(@RequestParam String q) {
requireQueryService();
return queryService.searchGraph(q, 50);
if (kind != null) {
validateNodeKind(kind);
}
return queryService.listNodes(kind, Math.min(limit, 1000), Math.max(0, offset));
}

@GetMapping("/nodes/{nodeId}/detail")
Expand All @@ -104,7 +106,7 @@
@RequestParam(defaultValue = "100") int limit,
@RequestParam(defaultValue = "0") int offset) {
requireQueryService();
return queryService.listEdges(kind, Math.min(limit, 1000), offset);
return queryService.listEdges(kind, Math.min(limit, 1000), Math.max(0, offset));
}

@GetMapping("/ego/{center}")
Expand Down Expand Up @@ -196,11 +198,16 @@
return queryService.searchGraph(q, Math.min(limit, 1000));
}

/**
* Check whether Neo4j (via QueryService) is available for queries.
*/
private boolean useNeo4j() {
return queryService != null;
private void validateNodeKind(String kind) {
try {
NodeKind.fromValue(kind);
} catch (IllegalArgumentException e) {

Check warning on line 204 in src/main/java/io/github/randomcodespace/iq/api/GraphController.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace "e" with an unnamed pattern.

See more on https://sonarcloud.io/project/issues?id=RandomCodeSpace_code-iq&issues=AZ1FjP5jUwmeBQ8reYFw&open=AZ1FjP5jUwmeBQ8reYFw&pullRequest=6
String valid = Arrays.stream(NodeKind.values())
.map(NodeKind::getValue)
.collect(Collectors.joining(", "));
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Invalid node kind: '" + kind + "'. Valid values: " + valid);
}
}

private void requireQueryService() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ public class CodeIqConfig {
/** Batch size for file processing during indexing (files per H2 flush). */
private int batchSize = 500;

/** Graph configuration sub-properties. */
private Graph graph = new Graph();

public static class Graph {
private String path = ".osscodeiq/graph.db";

public String getPath() { return path; }
public void setPath(String path) { this.path = path; }
}

/** Service name tag for multi-repo graph mode. */
private String serviceName;

Expand Down Expand Up @@ -77,4 +87,12 @@ public String getServiceName() {
public void setServiceName(String serviceName) {
this.serviceName = serviceName;
}

public Graph getGraph() {
return graph;
}

public void setGraph(Graph graph) {
this.graph = graph;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,8 @@ public class Neo4jConfig {
private int boltPort;

@Bean(destroyMethod = "shutdown")
DatabaseManagementService databaseManagementService(
@Value("${codeiq.graph.path:.osscodeiq/graph.db}") String dbPath) {
return new DatabaseManagementServiceBuilder(Path.of(dbPath))
DatabaseManagementService databaseManagementService(CodeIqConfig config) {
return new DatabaseManagementServiceBuilder(Path.of(config.getGraph().getPath()))
.setConfig(BoltConnector.enabled, true)
.setConfig(BoltConnector.listen_address, new SocketAddress("localhost", boltPort))
.build();
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ spring:
name: code-iq
main:
banner-mode: off
allow-bean-definition-overriding: true
allow-bean-definition-overriding: false
threads:
virtual:
enabled: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,34 @@ void nodesByKindShouldAcceptPaginationParams() throws Exception {
.andExpect(jsonPath("$.limit").value(25));
}

@Test
void nodesByKindShouldReturn400ForInvalidKind() throws Exception {
mockMvc.perform(get("/api/kinds/not_a_real_kind"))
.andExpect(status().isBadRequest());
}

@Test
void nodesByKindShouldClampNegativeOffset() throws Exception {
Map<String, Object> result = new LinkedHashMap<>();
result.put("kind", "endpoint");
result.put("nodes", List.of());
when(queryService.nodesByKind("endpoint", 50, 0)).thenReturn(result);

mockMvc.perform(get("/api/kinds/endpoint?offset=-5"))
.andExpect(status().isOk());
}

@Test
void nodesByKindShouldCapLimitTo1000() throws Exception {
Map<String, Object> result = new LinkedHashMap<>();
result.put("kind", "endpoint");
result.put("nodes", List.of());
when(queryService.nodesByKind("endpoint", 1000, 0)).thenReturn(result);

mockMvc.perform(get("/api/kinds/endpoint?limit=5000"))
.andExpect(status().isOk());
}

// --- /api/nodes ---

@Test
Expand All @@ -140,6 +168,34 @@ void listNodesShouldFilterByKind() throws Exception {
.andExpect(jsonPath("$.count").value(0));
}

@Test
void listNodesShouldReturn400ForInvalidKind() throws Exception {
mockMvc.perform(get("/api/nodes?kind=bogus_kind"))
.andExpect(status().isBadRequest());
}

@Test
void listNodesShouldClampNegativeOffset() throws Exception {
Map<String, Object> result = new LinkedHashMap<>();
result.put("nodes", List.of());
result.put("count", 0);
when(queryService.listNodes(null, 100, 0)).thenReturn(result);

mockMvc.perform(get("/api/nodes?offset=-10"))
.andExpect(status().isOk());
}

@Test
void listNodesShouldCapLimitTo1000() throws Exception {
Map<String, Object> result = new LinkedHashMap<>();
result.put("nodes", List.of());
result.put("count", 0);
when(queryService.listNodes(null, 1000, 0)).thenReturn(result);

mockMvc.perform(get("/api/nodes?limit=9999"))
.andExpect(status().isOk());
}

// --- /api/nodes/{nodeId}/detail ---

@Test
Expand Down Expand Up @@ -208,6 +264,30 @@ void listEdgesShouldReturnEdges() throws Exception {
.andExpect(jsonPath("$.total").value(0));
}

@Test
void listEdgesShouldClampNegativeOffset() throws Exception {
Map<String, Object> result = new LinkedHashMap<>();
result.put("edges", List.of());
result.put("count", 0);
result.put("total", 0);
when(queryService.listEdges(null, 100, 0)).thenReturn(result);

mockMvc.perform(get("/api/edges?offset=-3"))
.andExpect(status().isOk());
}

@Test
void listEdgesShouldCapLimitTo1000() throws Exception {
Map<String, Object> result = new LinkedHashMap<>();
result.put("edges", List.of());
result.put("count", 0);
result.put("total", 0);
when(queryService.listEdges(null, 1000, 0)).thenReturn(result);

mockMvc.perform(get("/api/edges?limit=5000"))
.andExpect(status().isOk());
}

// --- /api/ego/{center} ---

@Test
Expand Down
Loading