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 authored Nov 7, 2024
1 parent 876b56a commit f4f7c48
Show file tree
Hide file tree
Showing 30 changed files with 543 additions and 210 deletions.
1 change: 1 addition & 0 deletions search-service/config/detekt/baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<ID>LongMethod:V0_29__JsonLd_migration.kt$V0_29__JsonLd_migration$override fun migrate(context: Context)</ID>
<ID>LongParameterList:AttributeInstance.kt$AttributeInstance.Companion$( attributeUuid: UUID, instanceId: URI = generateRandomInstanceId(), timeAndProperty: Pair&lt;ZonedDateTime, TemporalProperty&gt;, value: Triple&lt;String?, Double?, WKTCoordinates?&gt;, payload: ExpandedAttributeInstance, sub: String? )</ID>
<ID>LongParameterList:AttributeInstance.kt$AttributeInstance.Companion$( attributeUuid: UUID, instanceId: URI = generateRandomInstanceId(), timeProperty: TemporalProperty? = TemporalProperty.OBSERVED_AT, modifiedAt: ZonedDateTime? = null, attributeMetadata: AttributeMetadata, payload: ExpandedAttributeInstance, time: ZonedDateTime, sub: String? = null )</ID>
<ID>LongParameterList:EntitiesQuery.kt$EntitiesQuery$( open val q: String?, open val scopeQ: String?, open val paginationQuery: PaginationQuery, open val attrs: Set&lt;ExpandedTerm&gt;, open val datasetId: Set&lt;String&gt;, open val geoQuery: GeoQuery?, open val contexts: List&lt;String&gt; )</ID>
<ID>LongParameterList:EntityAttributeService.kt$EntityAttributeService$( attribute: Attribute, attributeName: ExpandedTerm, attributeMetadata: AttributeMetadata, mergedAt: ZonedDateTime, observedAt: ZonedDateTime?, attributePayload: ExpandedAttributeInstance, sub: Sub? )</ID>
<ID>LongParameterList:EntityAttributeService.kt$EntityAttributeService$( attribute: Attribute, ngsiLdAttribute: NgsiLdAttribute, attributeMetadata: AttributeMetadata, createdAt: ZonedDateTime, attributePayload: ExpandedAttributeInstance, sub: Sub? )</ID>
<ID>LongParameterList:EntityAttributeService.kt$EntityAttributeService$( entityId: URI, attributeName: ExpandedTerm, attributeMetadata: AttributeMetadata, createdAt: ZonedDateTime, attributePayload: ExpandedAttributeInstance, sub: Sub? )</ID>
Expand Down
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?,
open val scopeQ: String?,
open val paginationQuery: PaginationQuery,
open val attrs: Set<ExpandedTerm>,
open val datasetId: Set<String>,
open val geoQuery: GeoQuery?,
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>? = null,
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 f4f7c48

Please sign in to comment.