This commit is contained in:
2026-08-01 15:41:35 +05:30
parent dcb6850306
commit 934937feb0
28 changed files with 1306 additions and 132 deletions

View File

@@ -0,0 +1,5 @@
package com.cygnus.cloud.query;
public record CloudQuery(String queryId, String query) {
}

View File

@@ -0,0 +1,30 @@
package com.cygnus.cloud.query;
import jakarta.validation.constraints.Pattern;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@Validated
@RestController
@RequestMapping("/api/v1/queries")
public class CloudQueryController {
private final QueryCatalogRepository repository;
public CloudQueryController(QueryCatalogRepository repository) {
this.repository = repository;
}
@GetMapping("/{queryId}")
public Mono<CloudQuery> query(
@PathVariable
@Pattern(regexp = "^[A-Za-z0-9._-]+$") String queryId) {
return repository.findEnabled(queryId)
.switchIfEmpty(Mono.error(new QueryNotFoundException(queryId)));
}
}

View File

@@ -0,0 +1,57 @@
package com.cygnus.cloud.query;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
@Component
public class QueryCatalogInitializer implements ApplicationRunner {
static final String DELIMITER = "!C0L!";
private final QueryCatalogRepository repository;
public QueryCatalogInitializer(QueryCatalogRepository repository) {
this.repository = repository;
}
@Override
public void run(ApplicationArguments arguments) {
List<CloudQuery> queries = readCatalog();
repository.initialize()
.thenMany(Flux.fromIterable(queries)
.concatMap(repository::insertIfAbsent))
.then()
.block(Duration.ofMinutes(2));
}
List<CloudQuery> readCatalog() {
ClassPathResource resource = new ClassPathResource("queries/nimble.qry");
List<CloudQuery> queries = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(
resource.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
int delimiter = line.indexOf(DELIMITER);
if (delimiter <= 0) {
continue;
}
String queryId = line.substring(0, delimiter).trim();
String query = line.substring(delimiter + DELIMITER.length());
if (queryId.matches("^Query\\d+$") && !query.isBlank()) {
queries.add(new CloudQuery(queryId, query));
}
}
return List.copyOf(queries);
} catch (Exception exception) {
throw new IllegalStateException("Unable to load cloud query catalog", exception);
}
}
}

View File

@@ -0,0 +1,61 @@
package com.cygnus.cloud.query;
import com.cygnus.cloud.database.ReactiveDatabaseClient;
import io.vertx.sqlclient.Tuple;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Mono;
@Repository
public class QueryCatalogRepository {
private static final String INITIALIZE = """
CREATE SCHEMA IF NOT EXISTS platform;
CREATE TABLE IF NOT EXISTS platform.application_query (
query_id varchar(100) PRIMARY KEY,
query_text text NOT NULL,
enabled boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT current_timestamp,
updated_at timestamptz NOT NULL DEFAULT current_timestamp,
CONSTRAINT ck_platform_application_query_id
CHECK (query_id ~ '^[A-Za-z0-9._-]+$'),
CONSTRAINT ck_platform_application_query_text
CHECK (length(btrim(query_text)) > 0)
)
""";
private static final String FIND = """
SELECT query_id, query_text
FROM platform.application_query
WHERE query_id = $1
AND enabled = true
""";
private static final String INSERT_IF_ABSENT = """
INSERT INTO platform.application_query (query_id, query_text)
VALUES ($1, $2)
ON CONFLICT (query_id) DO NOTHING
""";
private final ReactiveDatabaseClient database;
public QueryCatalogRepository(ReactiveDatabaseClient database) {
this.database = database;
}
public Mono<Void> initialize() {
return database.query(INITIALIZE).then();
}
public Mono<CloudQuery> findEnabled(String queryId) {
return database.preparedQuery(FIND, Tuple.of(queryId))
.flatMapMany(rows -> reactor.core.publisher.Flux.fromIterable(rows))
.next()
.map(row -> new CloudQuery(
row.getString("query_id"), row.getString("query_text")));
}
public Mono<Void> insertIfAbsent(CloudQuery query) {
return database.preparedUpdate(
INSERT_IF_ABSENT,
Tuple.of(query.queryId(), query.query()))
.then();
}
}

View File

@@ -0,0 +1,13 @@
package com.cygnus.cloud.query;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
public class QueryNotFoundException extends RuntimeException {
public QueryNotFoundException(String queryId) {
super("Query was not found: " + queryId);
}
}

View File

@@ -36,6 +36,8 @@ public class CloudSecurityConfiguration {
.permitAll()
.pathMatchers("/api/v1/identity/login")
.hasAuthority("SCOPE_identity.login")
.pathMatchers("/api/v1/queries/**")
.hasAuthority("SCOPE_identity.login")
.pathMatchers("/api/v1/admin/**")
.hasAuthority("SCOPE_cygnus.admin")
.anyExchange().authenticated())