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
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.kakao.actionbase.engine.service

import com.kakao.actionbase.core.metadata.common.StructField
import com.kakao.actionbase.engine.sql.DataFrame
import com.kakao.actionbase.v2.engine.Graph
import com.kakao.actionbase.v2.engine.sql.StatKey

import com.fasterxml.jackson.databind.JsonNode
import reactor.core.publisher.Mono

/**
* A query is stored under an id the server assigns, and a name is kept separately and points at that id,
* so that a name can be moved onto another query in one write and put back the same way.
*
* Running one goes through [QueryService] because that executor owns the transform cache.
*/
class PreparedQueryService(
private val graph: Graph,
private val queries: QueryService,
) {
fun register(
desc: String,
arguments: List<StructField>,
fetch: JsonNode,
transform: JsonNode,
stats: Set<StatKey> = emptySet(),
): Mono<PreparedQueryDescriptor> = TODO("Not yet implemented")

fun amend(
id: String,
desc: String? = null,
arguments: List<StructField>? = null,
fetch: JsonNode? = null,
transform: JsonNode? = null,
stats: Set<StatKey>? = null,
): Mono<PreparedQueryDescriptor> = TODO("Not yet implemented")

fun get(id: String): Mono<PreparedQueryDescriptor> = TODO("Not yet implemented")

fun list(status: MetadataStatus = MetadataStatus.ACTIVE): Mono<List<PreparedQueryDescriptor>> = TODO("Not yet implemented")

fun delete(id: String): Mono<Void> = TODO("Not yet implemented")

fun createAlias(
alias: String,
desc: String,
target: String,
): Mono<PreparedQueryAliasDescriptor> = TODO("Not yet implemented")

fun updateAlias(
alias: String,
desc: String? = null,
target: String? = null,
active: Boolean? = null,
): Mono<PreparedQueryAliasDescriptor> = TODO("Not yet implemented")

fun alias(alias: String): Mono<PreparedQueryAliasDescriptor> = TODO("Not yet implemented")

fun aliases(status: MetadataStatus = MetadataStatus.ACTIVE): Mono<List<PreparedQueryAliasDescriptor>> = TODO("Not yet implemented")

fun deleteAlias(alias: String): Mono<Void> = TODO("Not yet implemented")

fun query(
id: String,
arguments: Map<String, Any>,
): Mono<Map<String, DataFrame>> = TODO("Not yet implemented")
}

/** Named separately so that a 404 does not follow from every missing map key in the engine. */
class NoSuchPreparedQueryException(
id: String,
) : NoSuchElementException("No prepared query `$id`.")

enum class MetadataStatus {
ACTIVE,
INACTIVE,
ALL,
;

fun matches(active: Boolean): Boolean =
when (this) {
ACTIVE -> active
INACTIVE -> !active
ALL -> true
}
}

data class PreparedQueryDescriptor(
val id: String,
val arguments: List<StructField>,
val fetch: JsonNode,
val transform: JsonNode,
val stats: Set<StatKey>,
val active: Boolean,
val desc: String,
)

data class PreparedQueryAliasDescriptor(
val alias: String,
val target: String,
val active: Boolean,
val desc: String,
)
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package com.kakao.actionbase.server.api.graph.v3

import com.kakao.actionbase.engine.query.ActionbaseQuery
import com.kakao.actionbase.engine.service.PreparedQueryService
import com.kakao.actionbase.engine.service.QueryService
import com.kakao.actionbase.engine.sql.DataFrame
import com.kakao.actionbase.server.api.graph.v3.query.PreparedQueryExecuteRequest
import com.kakao.actionbase.server.util.mapToResponseEntity
import com.kakao.actionbase.v2.engine.sql.QueryResult

import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
Expand All @@ -16,6 +19,7 @@ import reactor.core.publisher.Mono
@RestController
class QueryController(
private val queryService: QueryService,
private val preparedQueryService: PreparedQueryService,
) {
@PostMapping("/graph/v3/query")
fun query(
Expand All @@ -28,6 +32,16 @@ class QueryController(
NamedQueryResult(items)
}.mapToResponseEntity()

@PostMapping("/graph/v3/query/{id}")
fun queryById(
@PathVariable id: String,
@RequestBody request: PreparedQueryExecuteRequest,
): Mono<ResponseEntity<NamedQueryResult>> =
preparedQueryService
.query(id, request.arguments)
.map { NamedQueryResult(it.map { entry -> entry.value.toNamedJsonFormat(entry.key) }) }
.mapToResponseEntity()

private fun DataFrame.toNamedJsonFormat(name: String): QueryResult.NamedJsonFormat {
val meta = schema.fields.map { QueryResult.Meta(it.name, it.type.name) }
val data = rows.map { it.data }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package com.kakao.actionbase.server.api.graph.v3.query

import com.kakao.actionbase.engine.service.MetadataStatus
import com.kakao.actionbase.engine.service.PreparedQueryAliasDescriptor
import com.kakao.actionbase.engine.service.PreparedQueryDescriptor
import com.kakao.actionbase.engine.service.PreparedQueryService
import com.kakao.actionbase.v2.engine.entity.EntityName

import jakarta.validation.Valid

import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.DeleteMapping
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.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController

import reactor.core.publisher.Mono

@RestController
class PreparedQueryController(
private val preparedQueryService: PreparedQueryService,
) {
private val tenant: String
get() = EntityName.tenant

@GetMapping("/graph/v3/prepared-queries")
fun listQueries(
@RequestParam(required = false, defaultValue = "ACTIVE") status: MetadataStatus,
): Mono<ResponseEntity<List<PreparedQueryResponse>>> =
preparedQueryService
.list(status)
.map { queries -> ResponseEntity.ok(queries.map { it.toResponse() }) }

/** [id] takes either form: the id a registration was given, or a name pointing at one. */
@GetMapping("/graph/v3/prepared-queries/{id}")
fun getQuery(
@PathVariable id: String,
): Mono<ResponseEntity<PreparedQueryResponse>> =
preparedQueryService
.get(id)
.map { ResponseEntity.ok(it.toResponse()) }
.defaultIfEmpty(ResponseEntity.notFound().build())

@PostMapping("/graph/v3/prepared-queries")
fun createQuery(
@Valid @RequestBody request: PreparedQueryCreateRequest,
): Mono<ResponseEntity<PreparedQueryResponse>> =
preparedQueryService
.register(
desc = request.comment,
arguments = request.arguments.map { it.toStructField() },
fetch = request.fetch,
transform = request.transform,
stats = request.stats,
).map { ResponseEntity.ok(it.toResponse()) }

@PutMapping("/graph/v3/prepared-queries/{id}")
fun updateQuery(
@PathVariable id: String,
@Valid @RequestBody request: PreparedQueryUpdateRequest,
): Mono<ResponseEntity<PreparedQueryResponse>> =
preparedQueryService
.amend(
id = id,
desc = request.comment,
arguments = request.arguments?.map { it.toStructField() },
fetch = request.fetch,
transform = request.transform,
stats = request.stats,
).map { ResponseEntity.ok(it.toResponse()) }
.defaultIfEmpty(ResponseEntity.notFound().build())

@DeleteMapping("/graph/v3/prepared-queries/{id}")
fun deleteQuery(
@PathVariable id: String,
): Mono<ResponseEntity<Void>> =
preparedQueryService
.delete(id)
.then(Mono.just(ResponseEntity.noContent().build<Void>()))

@GetMapping("/graph/v3/prepared-queries/aliases")
fun listAliases(
@RequestParam(required = false, defaultValue = "ACTIVE") status: MetadataStatus,
): Mono<ResponseEntity<List<PreparedQueryAliasResponse>>> =
preparedQueryService
.aliases(status)
.map { aliases -> ResponseEntity.ok(aliases.map { it.toResponse() }) }

@GetMapping("/graph/v3/prepared-queries/aliases/{alias}")
fun getAlias(
@PathVariable alias: String,
): Mono<ResponseEntity<PreparedQueryAliasResponse>> =
preparedQueryService
.alias(alias)
.map { ResponseEntity.ok(it.toResponse()) }
.defaultIfEmpty(ResponseEntity.notFound().build())

@PostMapping("/graph/v3/prepared-queries/aliases")
fun createAlias(
@Valid @RequestBody request: PreparedQueryAliasCreateRequest,
): Mono<ResponseEntity<PreparedQueryAliasResponse>> =
preparedQueryService
.createAlias(request.alias, request.comment, request.target)
.map { ResponseEntity.ok(it.toResponse()) }

@PutMapping("/graph/v3/prepared-queries/aliases/{alias}")
fun updateAlias(
@PathVariable alias: String,
@Valid @RequestBody request: PreparedQueryAliasUpdateRequest,
): Mono<ResponseEntity<PreparedQueryAliasResponse>> =
preparedQueryService
.updateAlias(alias, request.comment, request.target, request.active)
.map { ResponseEntity.ok(it.toResponse()) }
.defaultIfEmpty(ResponseEntity.notFound().build())

@DeleteMapping("/graph/v3/prepared-queries/aliases/{alias}")
fun deleteAlias(
@PathVariable alias: String,
): Mono<ResponseEntity<Void>> =
preparedQueryService
.deleteAlias(alias)
.then(Mono.just(ResponseEntity.noContent().build<Void>()))

private fun PreparedQueryDescriptor.toResponse(): PreparedQueryResponse =
PreparedQueryResponse(
tenant = tenant,
id = id,
arguments = arguments,
fetch = fetch,
transform = transform,
stats = stats,
active = active,
comment = desc,
)

private fun PreparedQueryAliasDescriptor.toResponse(): PreparedQueryAliasResponse =
PreparedQueryAliasResponse(
tenant = tenant,
alias = alias,
target = target,
active = active,
comment = desc,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.kakao.actionbase.server.api.graph.v3.query

import com.kakao.actionbase.core.Constants
import com.kakao.actionbase.core.metadata.common.StructField
import com.kakao.actionbase.core.types.PrimitiveType
import com.kakao.actionbase.v2.engine.sql.StatKey

import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Pattern
import jakarta.validation.constraints.Size

import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.node.JsonNodeFactory

data class QueryArgumentRequest(
@field:NotBlank(message = "argument name is required")
val name: String,
val type: PrimitiveType,
@field:Size(max = Constants.Name.COMMENT_MAX_LENGTH, message = Constants.Name.COMMENT_SIZE_MESSAGE)
val comment: String = "",
) {
fun toStructField(): StructField = StructField(name = name, type = type, comment = comment, nullable = false)
}

data class PreparedQueryCreateRequest(
@field:Size(max = Constants.Name.COMMENT_MAX_LENGTH, message = Constants.Name.COMMENT_SIZE_MESSAGE)
val comment: String = "",
val arguments: List<QueryArgumentRequest> = emptyList(),
val fetch: JsonNode = JsonNodeFactory.instance.arrayNode(),
val transform: JsonNode = JsonNodeFactory.instance.arrayNode(),
val stats: Set<StatKey> = emptySet(),
)

data class PreparedQueryUpdateRequest(
val active: Boolean? = null,
@field:Size(max = Constants.Name.COMMENT_MAX_LENGTH, message = Constants.Name.COMMENT_SIZE_MESSAGE)
val comment: String? = null,
val arguments: List<QueryArgumentRequest>? = null,
val fetch: JsonNode? = null,
val transform: JsonNode? = null,
val stats: Set<StatKey>? = null,
)

data class PreparedQueryAliasCreateRequest(
@field:NotBlank(message = "alias is required")
@field:Pattern(regexp = Constants.Name.PATTERN, message = Constants.Name.MESSAGE)
val alias: String,
@field:NotBlank(message = "target is required")
val target: String,
@field:Size(max = Constants.Name.COMMENT_MAX_LENGTH, message = Constants.Name.COMMENT_SIZE_MESSAGE)
val comment: String = "",
)

data class PreparedQueryAliasUpdateRequest(
val active: Boolean? = null,
@field:Size(max = Constants.Name.COMMENT_MAX_LENGTH, message = Constants.Name.COMMENT_SIZE_MESSAGE)
val comment: String? = null,
val target: String? = null,
)

data class PreparedQueryExecuteRequest(
val arguments: Map<String, Any> = emptyMap(),
)

data class PreparedQueryResponse(
val tenant: String,
val id: String,
val arguments: List<StructField>,
val fetch: JsonNode,
val transform: JsonNode,
val stats: Set<StatKey>,
val active: Boolean = true,
val comment: String = Constants.DEFAULT_COMMENT,
val revision: Long = Constants.DEFAULT_REVISION,
val createdAt: Long = Constants.DEFAULT_CREATED_AT,
val createdBy: String = Constants.DEFAULT_CREATED_BY,
val updatedAt: Long = Constants.DEFAULT_UPDATED_AT,
val updatedBy: String = Constants.DEFAULT_UPDATED_BY,
)

data class PreparedQueryAliasResponse(
val tenant: String,
val alias: String,
val target: String,
val active: Boolean = true,
val comment: String = Constants.DEFAULT_COMMENT,
val revision: Long = Constants.DEFAULT_REVISION,
val createdAt: Long = Constants.DEFAULT_CREATED_AT,
val createdBy: String = Constants.DEFAULT_CREATED_BY,
val updatedAt: Long = Constants.DEFAULT_UPDATED_AT,
val updatedBy: String = Constants.DEFAULT_UPDATED_BY,
)
Loading
Loading