Skip to content

Commit

Permalink
feat: add support for many EntitySelector objects in Query datatype
Browse files Browse the repository at this point in the history
  • Loading branch information
bobeal committed Nov 2, 2024
1 parent cd7fa7a commit 88162e5
Show file tree
Hide file tree
Showing 31 changed files with 570 additions and 249 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.egm.stellio.search.authorization.service

import arrow.core.Either
import arrow.core.Option
import com.egm.stellio.search.entity.model.EntitiesQuery
import com.egm.stellio.search.entity.model.EntitiesQueryFromGet
import com.egm.stellio.shared.model.APIException
import com.egm.stellio.shared.model.ExpandedEntity
import com.egm.stellio.shared.util.Sub
Expand All @@ -22,7 +22,7 @@ interface AuthorizationService {
suspend fun removeRightsOnEntity(entityId: URI): Either<APIException, Unit>

suspend fun getAuthorizedEntities(
entitiesQuery: EntitiesQuery,
entitiesQuery: EntitiesQueryFromGet,
contexts: List<String>,
sub: Option<Sub>
): Either<APIException, Pair<Int, List<ExpandedEntity>>>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.egm.stellio.search.authorization.service
import arrow.core.Either
import arrow.core.Option
import arrow.core.right
import com.egm.stellio.search.entity.model.EntitiesQuery
import com.egm.stellio.search.entity.model.EntitiesQueryFromGet
import com.egm.stellio.shared.model.APIException
import com.egm.stellio.shared.model.ExpandedEntity
import com.egm.stellio.shared.util.Sub
Expand Down Expand Up @@ -40,7 +40,7 @@ class DisabledAuthorizationService : AuthorizationService {
override suspend fun removeRightsOnEntity(entityId: URI): Either<APIException, Unit> = Unit.right()

override suspend fun getAuthorizedEntities(
entitiesQuery: EntitiesQuery,
entitiesQuery: EntitiesQueryFromGet,
contexts: List<String>,
sub: Option<Sub>
): Either<APIException, Pair<Int, List<ExpandedEntity>>> = Pair(-1, emptyList<ExpandedEntity>()).right()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import arrow.core.left
import arrow.core.raise.either
import arrow.core.right
import arrow.fx.coroutines.parMap
import com.egm.stellio.search.entity.model.EntitiesQuery
import com.egm.stellio.search.entity.model.EntitiesQueryFromGet
import com.egm.stellio.shared.model.APIException
import com.egm.stellio.shared.model.AccessDeniedException
import com.egm.stellio.shared.model.ExpandedEntity
Expand Down Expand Up @@ -107,7 +107,7 @@ class EnabledAuthorizationService(
entityAccessRightsService.removeRolesOnEntity(entityId)

override suspend fun getAuthorizedEntities(
entitiesQuery: EntitiesQuery,
entitiesQuery: EntitiesQueryFromGet,
contexts: List<String>,
sub: Option<Sub>
): Either<APIException, Pair<Int, List<ExpandedEntity>>> = either {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import com.egm.stellio.search.entity.model.NotUpdatedDetails
import com.egm.stellio.search.entity.model.UpdateAttributeResult
import com.egm.stellio.search.entity.model.UpdateOperationResult
import com.egm.stellio.search.entity.model.updateResultFromDetailedResult
import com.egm.stellio.search.entity.util.composeEntitiesQuery
import com.egm.stellio.search.entity.util.composeEntitiesQueryFromGet
import com.egm.stellio.shared.config.ApplicationProperties
import com.egm.stellio.shared.model.AccessDeniedException
import com.egm.stellio.shared.model.BadRequestDataException
Expand Down Expand Up @@ -74,7 +74,7 @@ class EntityAccessControlHandler(
val contexts = getAuthzContextFromLinkHeaderOrDefault(httpHeaders, applicationProperties.contexts).bind()
val mediaType = getApplicableMediaType(httpHeaders).bind()

val entitiesQuery = composeEntitiesQuery(
val entitiesQuery = composeEntitiesQueryFromGet(
applicationProperties.pagination,
params,
contexts
Expand Down Expand Up @@ -121,7 +121,7 @@ class EntityAccessControlHandler(

val contexts = getAuthzContextFromLinkHeaderOrDefault(httpHeaders, applicationProperties.contexts).bind()
val mediaType = getApplicableMediaType(httpHeaders).bind()
val entitiesQuery = composeEntitiesQuery(
val entitiesQuery = composeEntitiesQueryFromGet(
applicationProperties.pagination,
params,
contexts
Expand Down Expand Up @@ -167,7 +167,7 @@ class EntityAccessControlHandler(

val contexts = getAuthzContextFromLinkHeaderOrDefault(httpHeaders, applicationProperties.contexts).bind()
val mediaType = getApplicableMediaType(httpHeaders).bind()
val entitiesQuery = composeEntitiesQuery(
val entitiesQuery = composeEntitiesQueryFromGet(
applicationProperties.pagination,
params,
contexts
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,42 @@
package com.egm.stellio.search.entity.model

import com.egm.stellio.shared.model.EntitySelector
import com.egm.stellio.shared.model.EntityTypeSelection
import com.egm.stellio.shared.model.ExpandedTerm
import com.egm.stellio.shared.model.GeoQuery
import com.egm.stellio.shared.model.PaginationQuery
import java.net.URI

data class EntitiesQuery(
sealed class EntitiesQuery(
open val q: String? = null,
open val scopeQ: String? = null,
open val paginationQuery: PaginationQuery,
open val attrs: Set<ExpandedTerm> = emptySet(),
open val datasetId: Set<String> = emptySet(),
open val geoQuery: GeoQuery? = null,
open val contexts: List<String>
)

data class EntitiesQueryFromGet(
val ids: Set<URI> = emptySet(),
val typeSelection: EntityTypeSelection? = null,
val idPattern: String? = null,
val q: String? = null,
val scopeQ: String? = null,
val paginationQuery: PaginationQuery,
val attrs: Set<ExpandedTerm> = emptySet(),
val datasetId: Set<String> = emptySet(),
val geoQuery: GeoQuery? = null,
val contexts: List<String>
)
override val q: String? = null,
override val scopeQ: String? = null,
override val paginationQuery: PaginationQuery,
override val attrs: Set<ExpandedTerm> = emptySet(),
override val datasetId: Set<String> = emptySet(),
override val geoQuery: GeoQuery? = null,
override val contexts: List<String>
) : EntitiesQuery(q, scopeQ, paginationQuery, attrs, datasetId, geoQuery, contexts)

data class EntitiesQueryFromPost(
val entitySelectors: List<EntitySelector>?,
override val q: String? = null,
override val scopeQ: String? = null,
override val paginationQuery: PaginationQuery,
override val attrs: Set<ExpandedTerm> = emptySet(),
override val datasetId: Set<String> = emptySet(),
override val geoQuery: GeoQuery? = null,
override val contexts: List<String>
) : EntitiesQuery(q, scopeQ, paginationQuery, attrs, datasetId, geoQuery, contexts)
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import com.egm.stellio.search.common.util.oneToResult
import com.egm.stellio.search.common.util.toUri
import com.egm.stellio.search.common.util.wrapToAndClause
import com.egm.stellio.search.entity.model.EntitiesQuery
import com.egm.stellio.search.entity.model.EntitiesQueryFromGet
import com.egm.stellio.search.entity.model.EntitiesQueryFromPost
import com.egm.stellio.search.entity.model.Entity
import com.egm.stellio.search.entity.util.rowToEntity
import com.egm.stellio.shared.model.APIException
Expand Down Expand Up @@ -119,23 +121,32 @@ class EntityQueryService(
buildEntitiesQueryFilter(
entitiesQuery,
accessRightFilter
).let {
if (entitiesQuery.q != null)
it.wrapToAndClause(buildQQuery(entitiesQuery.q, entitiesQuery.contexts))
else it
}.let {
if (entitiesQuery.scopeQ != null)
it.wrapToAndClause(buildScopeQQuery(entitiesQuery.scopeQ))
else it
}.let {
if (entitiesQuery.geoQuery != null)
it.wrapToAndClause(buildGeoQuery(entitiesQuery.geoQuery))
else it
).let { sqlFilter ->
entitiesQuery.q?.let { q ->
sqlFilter.wrapToAndClause(buildQQuery(q, entitiesQuery.contexts))
} ?: sqlFilter
}.let { sqlFilter ->
entitiesQuery.scopeQ?.let { scopeQ ->
sqlFilter.wrapToAndClause(buildScopeQQuery(scopeQ))
} ?: sqlFilter
}.let { sqlFilter ->
entitiesQuery.geoQuery?.let { geoQuery ->
sqlFilter.wrapToAndClause(buildGeoQuery(geoQuery))
} ?: sqlFilter
}

fun buildEntitiesQueryFilter(
entitiesQuery: EntitiesQuery,
accessRightFilter: () -> String?
): String =
when (entitiesQuery) {
is EntitiesQueryFromGet -> buildEntitiesQueryFilterFromGet(entitiesQuery, accessRightFilter)
is EntitiesQueryFromPost -> buildEntitiesQueryFilterFromPost(entitiesQuery, accessRightFilter)
}

fun buildEntitiesQueryFilterFromGet(
entitiesQuery: EntitiesQueryFromGet,
accessRightFilter: () -> String?
): String {
val formattedIds =
if (entitiesQuery.ids.isNotEmpty())
Expand Down Expand Up @@ -171,6 +182,37 @@ class EntityQueryService(
return queryFilter.joinToString(separator = " AND ")
}

fun buildEntitiesQueryFilterFromPost(
entitiesQuery: EntitiesQueryFromPost,
accessRightFilter: () -> String?
): String {
val entitySelectorFilter = entitiesQuery.entitySelectors?.map { entitySelector ->
val formattedId =
entitySelector.id?.let { "entity_payload.entity_id = '${entitySelector.id}'" }
val formattedIdPattern =
entitySelector.idPattern?.let { "entity_payload.entity_id ~ '${entitySelector.idPattern}'" }
val formattedType = entitySelector.typeSelection.let { "(" + buildTypeQuery(it) + ")" }
val formattedAttrs =
if (entitiesQuery.attrs.isNotEmpty())
entitiesQuery.attrs.joinToString(
separator = ",",
prefix = "attribute_name in (",
postfix = ")"
) { "'$it'" }
else null

listOfNotNull(
formattedId,
formattedIdPattern,
formattedType,
formattedAttrs,
accessRightFilter()
).joinToString(separator = " AND ", prefix = "(", postfix = ")")
}

return entitySelectorFilter?.joinToString(separator = " OR ") ?: " 1 = 1 "
}

suspend fun retrieve(entityId: URI): Either<APIException, Entity> =
databaseClient.sql(
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import arrow.core.Either
import arrow.core.left
import arrow.core.raise.either
import com.egm.stellio.search.common.model.Query
import com.egm.stellio.search.entity.model.EntitiesQuery
import com.egm.stellio.search.entity.model.EntitiesQueryFromGet
import com.egm.stellio.search.entity.model.EntitiesQueryFromPost
import com.egm.stellio.shared.config.ApplicationProperties
import com.egm.stellio.shared.model.APIException
import com.egm.stellio.shared.model.BadRequestDataException
import com.egm.stellio.shared.model.EntitySelector
import com.egm.stellio.shared.util.JsonLdUtils
import com.egm.stellio.shared.util.QUERY_PARAM_ATTRS
import com.egm.stellio.shared.util.QUERY_PARAM_DATASET_ID
Expand All @@ -26,11 +28,11 @@ import com.egm.stellio.shared.util.toListOfUri
import com.egm.stellio.shared.util.validateIdPattern
import org.springframework.util.MultiValueMap

fun composeEntitiesQuery(
fun composeEntitiesQueryFromGet(
defaultPagination: ApplicationProperties.Pagination,
requestParams: MultiValueMap<String, String>,
contexts: List<String>
): Either<APIException, EntitiesQuery> = either {
): Either<APIException, EntitiesQueryFromGet> = either {
val ids = requestParams.getFirst(QUERY_PARAM_ID)?.split(",").orEmpty().toListOfUri().toSet()
val typeSelection = expandTypeSelection(requestParams.getFirst(QUERY_PARAM_TYPE), contexts)
val idPattern = validateIdPattern(requestParams.getFirst(QUERY_PARAM_ID_PATTERN)).bind()
Expand All @@ -51,7 +53,7 @@ fun composeEntitiesQuery(

val geoQuery = parseGeoQueryParameters(requestParams.toSingleValueMap(), contexts).bind()

EntitiesQuery(
EntitiesQueryFromGet(
ids = ids,
typeSelection = typeSelection,
idPattern = idPattern,
Expand All @@ -65,7 +67,7 @@ fun composeEntitiesQuery(
)
}

fun EntitiesQuery.validateMinimalQueryEntitiesParameters(): Either<APIException, EntitiesQuery> = either {
fun EntitiesQueryFromGet.validateMinimalQueryEntitiesParameters(): Either<APIException, EntitiesQueryFromGet> = either {
if (
geoQuery == null &&
q.isNullOrEmpty() &&
Expand All @@ -74,20 +76,25 @@ fun EntitiesQuery.validateMinimalQueryEntitiesParameters(): Either<APIException,
)
return@either BadRequestDataException(
"One of 'type', 'attrs', 'q', 'geoQ' must be provided in the query"
).left().bind<EntitiesQuery>()
).left().bind<EntitiesQueryFromGet>()

this@validateMinimalQueryEntitiesParameters
}

fun composeEntitiesQueryFromPostRequest(
fun composeEntitiesQueryFromPost(
defaultPagination: ApplicationProperties.Pagination,
query: Query,
requestParams: MultiValueMap<String, String>,
contexts: List<String>
): Either<APIException, EntitiesQuery> = either {
val entitySelector = query.entities?.get(0)
val typeSelection = expandTypeSelection(entitySelector?.typeSelection, contexts)
val idPattern = validateIdPattern(entitySelector?.idPattern).bind()
): Either<APIException, EntitiesQueryFromPost> = either {
val entitySelectors = query.entities?.map { entitySelector ->
validateIdPattern(entitySelector.idPattern).bind()
EntitySelector(
entitySelector.id,
entitySelector.idPattern,
expandTypeSelection(entitySelector.typeSelection, contexts)!!
)
}
val attrs = query.attrs.orEmpty().map { JsonLdUtils.expandJsonLdTerm(it.trim(), contexts) }.toSet()
val datasetId = query.datasetId.orEmpty().toSet()
val geoQuery = query.geoQ?.let {
Expand All @@ -106,10 +113,8 @@ fun composeEntitiesQueryFromPostRequest(
defaultPagination.limitMax
).bind()

EntitiesQuery(
ids = setOfNotNull(entitySelector?.id),
typeSelection = typeSelection,
idPattern = idPattern,
EntitiesQueryFromPost(
entitySelectors = entitySelectors,
q = query.q?.decode(),
scopeQ = query.scopeQ,
paginationQuery = paginationQuery,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import arrow.core.raise.either
import arrow.core.right
import com.egm.stellio.search.entity.service.EntityQueryService
import com.egm.stellio.search.entity.service.EntityService
import com.egm.stellio.search.entity.util.composeEntitiesQuery
import com.egm.stellio.search.entity.util.composeEntitiesQueryFromGet
import com.egm.stellio.search.entity.util.validateMinimalQueryEntitiesParameters
import com.egm.stellio.shared.config.ApplicationProperties
import com.egm.stellio.shared.model.BadRequestDataException
Expand Down Expand Up @@ -177,7 +177,7 @@ class EntityHandler(
val sub = getSubFromSecurityContext()

val contexts = getContextFromLinkHeaderOrDefault(httpHeaders, applicationProperties.contexts.core).bind()
val entitiesQuery = composeEntitiesQuery(
val entitiesQuery = composeEntitiesQueryFromGet(
applicationProperties.pagination,
params,
contexts
Expand Down Expand Up @@ -218,7 +218,7 @@ class EntityHandler(
val sub = getSubFromSecurityContext()

val contexts = getContextFromLinkHeaderOrDefault(httpHeaders, applicationProperties.contexts.core).bind()
val queryParams = composeEntitiesQuery(
val queryParams = composeEntitiesQueryFromGet(
applicationProperties.pagination,
params,
contexts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ import arrow.core.right
import com.egm.stellio.search.common.model.Query
import com.egm.stellio.search.entity.service.EntityOperationService
import com.egm.stellio.search.entity.service.EntityQueryService
import com.egm.stellio.search.entity.util.composeEntitiesQueryFromPostRequest
import com.egm.stellio.search.entity.util.validateMinimalQueryEntitiesParameters
import com.egm.stellio.search.entity.util.composeEntitiesQueryFromPost
import com.egm.stellio.shared.config.ApplicationProperties
import com.egm.stellio.shared.model.APIException
import com.egm.stellio.shared.model.BadRequestDataException
Expand Down Expand Up @@ -243,13 +242,12 @@ class EntityOperationHandler(
val mediaType = getApplicableMediaType(httpHeaders).bind()
val query = Query(requestBody.awaitFirst()).bind()

val entitiesQuery = composeEntitiesQueryFromPostRequest(
val entitiesQuery = composeEntitiesQueryFromPost(
applicationProperties.pagination,
query,
params,
contexts
).bind()
.validateMinimalQueryEntitiesParameters().bind()

val (entities, count) = entityQueryService.queryEntities(entitiesQuery, sub.getOrNull()).bind()

Expand Down
Loading

0 comments on commit 88162e5

Please sign in to comment.