commit
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
package com.cygnus.cloud.query;
|
||||
|
||||
public record CloudQuery(String queryId, String query) {
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user