Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): add support for query entities via POST #1017

Merged
merged 4 commits into from
Oct 16, 2023
Merged
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
6 changes: 3 additions & 3 deletions search-service/config/detekt/baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<CurrentIssues>
<ID>ClassNaming:V0_29_JsonLd_migrationTests.kt$V0_29_JsonLd_migrationTests</ID>
<ID>ClassNaming:V0_29__JsonLd_migration.kt$V0_29__JsonLd_migration : BaseJavaMigration</ID>
<ID>ComplexCondition:EntityHandler.kt$EntityHandler$queryParams.ids.isEmpty() &amp;&amp; queryParams.q.isNullOrEmpty() &amp;&amp; queryParams.type.isNullOrEmpty() &amp;&amp; queryParams.attrs.isEmpty()</ID>
<ID>ComplexCondition:EntitiesQueryUtils.kt$geoQuery == null &amp;&amp; q.isNullOrEmpty() &amp;&amp; type.isNullOrEmpty() &amp;&amp; attrs.isEmpty()</ID>
<ID>ComplexCondition:EntityPayloadService.kt$EntityPayloadService$it &amp;&amp; !inverse || !it &amp;&amp; inverse</ID>
<ID>Filename:V0_29__JsonLd_migration.kt$db.migration.V0_29__JsonLd_migration.kt</ID>
<ID>LongMethod:AttributeInstanceService.kt$AttributeInstanceService$@Transactional suspend fun create(attributeInstance: AttributeInstance): Either&lt;APIException, Unit&gt;</ID>
Expand All @@ -25,8 +25,8 @@
<ID>LongParameterList:TemporalEntityAttributeService.kt$TemporalEntityAttributeService$( temporalEntityAttribute: TemporalEntityAttribute, ngsiLdAttribute: NgsiLdAttribute, attributeMetadata: AttributeMetadata, createdAt: ZonedDateTime, attributePayload: ExpandedAttributeInstance, sub: Sub? )</ID>
<ID>LongParameterList:V0_29__JsonLd_migration.kt$V0_29__JsonLd_migration$( entityId: URI, attributeName: ExpandedTerm, datasetId: URI?, attributePayload: ExpandedAttributeInstance, ngsiLdAttributeInstance: NgsiLdAttributeInstance, defaultCreatedAt: ZonedDateTime )</ID>
<ID>NestedBlockDepth:V0_29__JsonLd_migration.kt$V0_29__JsonLd_migration$override fun migrate(context: Context)</ID>
<ID>ReturnCount:TemporalQueryUtils.kt$fun buildTemporalQuery( params: MultiValueMap&lt;String, String&gt;, inQueryEntities: Boolean = false, withAggregatedValues: Boolean = false ): Either&lt;APIException, TemporalQuery&gt;</ID>
<ID>SwallowedException:TemporalQueryUtils.kt$e: IllegalArgumentException</ID>
<ID>ReturnCount:EntitiesQueryUtils.kt$fun buildTemporalQuery( params: MultiValueMap&lt;String, String&gt;, inQueryEntities: Boolean = false, withAggregatedValues: Boolean = false ): Either&lt;APIException, TemporalQuery&gt;</ID>
<ID>SwallowedException:EntitiesQueryUtils.kt$e: IllegalArgumentException</ID>
<ID>TooManyFunctions:EntityPayloadService.kt$EntityPayloadService</ID>
<ID>TooManyFunctions:TemporalEntityAttributeService.kt$TemporalEntityAttributeService</ID>
</CurrentIssues>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package com.egm.stellio.search.authorization

import arrow.core.Either
import arrow.core.Option
import com.egm.stellio.search.model.EntitiesQuery
import com.egm.stellio.shared.model.APIException
import com.egm.stellio.shared.model.JsonLdEntity
import com.egm.stellio.shared.model.QueryParams
import com.egm.stellio.shared.util.Sub
import java.net.URI

Expand All @@ -22,7 +22,7 @@ interface AuthorizationService {
suspend fun removeRightsOnEntity(entityId: URI): Either<APIException, Unit>

suspend fun getAuthorizedEntities(
queryParams: QueryParams,
entitiesQuery: EntitiesQuery,
contextLink: String,
sub: Option<Sub>
): Either<APIException, Pair<Int, List<JsonLdEntity>>>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package com.egm.stellio.search.authorization
import arrow.core.Either
import arrow.core.Option
import arrow.core.right
import com.egm.stellio.search.model.EntitiesQuery
import com.egm.stellio.shared.model.APIException
import com.egm.stellio.shared.model.JsonLdEntity
import com.egm.stellio.shared.model.QueryParams
import com.egm.stellio.shared.util.Sub
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.stereotype.Component
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(
queryParams: QueryParams,
entitiesQuery: EntitiesQuery,
contextLink: String,
sub: Option<Sub>
): Either<APIException, Pair<Int, List<JsonLdEntity>>> = Pair(-1, emptyList<JsonLdEntity>()).right()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ package com.egm.stellio.search.authorization
import arrow.core.*
import arrow.core.raise.either
import arrow.fx.coroutines.parMap
import com.egm.stellio.search.model.EntitiesQuery
import com.egm.stellio.shared.model.APIException
import com.egm.stellio.shared.model.AccessDeniedException
import com.egm.stellio.shared.model.JsonLdEntity
import com.egm.stellio.shared.model.QueryParams
import com.egm.stellio.shared.util.*
import com.egm.stellio.shared.util.AuthContextModel.SpecificAccessPolicy
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
Expand Down Expand Up @@ -94,17 +94,17 @@ class EnabledAuthorizationService(
entityAccessRightsService.removeRolesOnEntity(entityId)

override suspend fun getAuthorizedEntities(
queryParams: QueryParams,
entitiesQuery: EntitiesQuery,
contextLink: String,
sub: Option<Sub>
): Either<APIException, Pair<Int, List<JsonLdEntity>>> = either {
val accessRights = queryParams.attrs.mapNotNull { AccessRight.forExpandedAttributeName(it).getOrNull() }
val accessRights = entitiesQuery.attrs.mapNotNull { AccessRight.forExpandedAttributeName(it).getOrNull() }
val entitiesAccessControl = entityAccessRightsService.getSubjectAccessRights(
sub,
accessRights,
queryParams.type,
queryParams.limit,
queryParams.offset
entitiesQuery.type,
entitiesQuery.paginationQuery.limit,
entitiesQuery.paginationQuery.offset
).bind()

// for each entity user is admin of, retrieve the full details of rights other users have on it
Expand Down Expand Up @@ -133,7 +133,7 @@ class EnabledAuthorizationService(
val count = entityAccessRightsService.getSubjectAccessRightsCount(
sub,
accessRights,
queryParams.type
entitiesQuery.type
).bind()

Pair(count, entitiesAccessControlWithSubjectRights)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
package com.egm.stellio.shared.model
package com.egm.stellio.search.model

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

data class QueryParams(
data class EntitiesQuery(
val ids: Set<URI> = emptySet(),
val type: String? = null,
val idPattern: String? = null,
val q: String? = null,
val scopeQ: String? = null,
val limit: Int,
val offset: Int,
val count: Boolean = false,
val paginationQuery: PaginationQuery,
val attrs: Set<ExpandedTerm> = emptySet(),
val includeSysAttrs: Boolean = false,
val useSimplifiedRepresentation: Boolean = false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.egm.stellio.search.model

import arrow.core.Either
import arrow.core.left
import arrow.core.raise.either
import arrow.core.raise.ensure
import com.egm.stellio.shared.model.APIException
import com.egm.stellio.shared.model.BadRequestDataException
import com.egm.stellio.shared.util.JsonUtils

/**
* A Query data type as defined in 5.2.23.
*
* It represents the raw data as received in the API. To have a consistent validation handling, even mandatory
* parameters are declared as optional here, validation is performed later in #{EntitiesQueryUtils}.
*/
data class Query private constructor(
val type: String,
val entities: List<EntitySelector>? = null,
val attrs: List<String>? = null,
val q: String? = null,
val geoQ: UnparsedGeoQuery? = null,
val temporalQ: UnparsedTemporalQuery? = null,
val scopeQ: String? = null
) {
companion object {
operator fun invoke(queryBody: String): Either<APIException, Query> = either {
runCatching {
JsonUtils.deserializeDataTypeAs<Query>(queryBody)
}.fold(
{
ensure(it.type == "Query") {
BadRequestDataException("The type parameter should be equals to 'Query'")
}
it
},
{
BadRequestDataException(
"The supplied query could not be parsed: ${it.message}"
).left().bind<Query>()
}
)
}
}
}

data class EntitySelector(
val id: String? = null,
val idPattern: String? = null,
val type: String? = null
)

data class UnparsedTemporalQuery(
val timerel: String? = null,
val timeAt: String? = null,
val endTimeAt: String? = null,
val aggrPeriodDuration: String? = null,
val aggrMethods: List<String>? = null,
val lastN: Int? = null,
val timeproperty: String = "observedAt"
)

data class UnparsedGeoQuery(
val geometry: String,
val coordinates: List<Any>,
val georel: String,
val geoproperty: String
)
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package com.egm.stellio.search.model

import com.egm.stellio.shared.model.QueryParams

data class TemporalEntitiesQuery(
val queryParams: QueryParams,
val entitiesQuery: EntitiesQuery,
val temporalQuery: TemporalQuery,
val withTemporalValues: Boolean,
val withAudit: Boolean,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,10 +256,10 @@ class EntityPayloadService(
}

suspend fun queryEntities(
queryParams: QueryParams,
entitiesQuery: EntitiesQuery,
accessRightFilter: () -> String?
): List<URI> {
val filterQuery = buildFullEntitiesFilter(queryParams, accessRightFilter)
val filterQuery = buildFullEntitiesFilter(entitiesQuery, accessRightFilter)

val selectQuery =
"""
Expand All @@ -275,16 +275,16 @@ class EntityPayloadService(

return databaseClient
.sql(selectQuery)
.bind("limit", queryParams.limit)
.bind("offset", queryParams.offset)
.bind("limit", entitiesQuery.paginationQuery.limit)
.bind("offset", entitiesQuery.paginationQuery.offset)
.allToMappedList { toUri(it["entity_id"]) }
}

suspend fun queryEntitiesCount(
queryParams: QueryParams,
entitiesQuery: EntitiesQuery,
accessRightFilter: () -> String?
): Either<APIException, Int> {
val filterQuery = buildFullEntitiesFilter(queryParams, accessRightFilter)
val filterQuery = buildFullEntitiesFilter(entitiesQuery, accessRightFilter)

val countQuery =
"""
Expand All @@ -301,44 +301,44 @@ class EntityPayloadService(
.map { it.toInt() }
}

private fun buildFullEntitiesFilter(queryParams: QueryParams, accessRightFilter: () -> String?): String =
private fun buildFullEntitiesFilter(entitiesQuery: EntitiesQuery, accessRightFilter: () -> String?): String =
buildEntitiesQueryFilter(
queryParams,
entitiesQuery,
accessRightFilter
).let {
if (queryParams.q != null)
it.wrapToAndClause(buildQQuery(queryParams.q!!, listOf(queryParams.context)))
if (entitiesQuery.q != null)
it.wrapToAndClause(buildQQuery(entitiesQuery.q, listOf(entitiesQuery.context)))
else it
}.let {
if (queryParams.scopeQ != null)
it.wrapToAndClause(buildScopeQQuery(queryParams.scopeQ!!))
if (entitiesQuery.scopeQ != null)
it.wrapToAndClause(buildScopeQQuery(entitiesQuery.scopeQ))
else it
}.let {
if (queryParams.geoQuery != null)
it.wrapToAndClause(buildGeoQuery(queryParams.geoQuery!!))
if (entitiesQuery.geoQuery != null)
it.wrapToAndClause(buildGeoQuery(entitiesQuery.geoQuery))
else it
}

fun buildEntitiesQueryFilter(
queryParams: QueryParams,
entitiesQuery: EntitiesQuery,
accessRightFilter: () -> String?
): String {
val formattedIds =
if (queryParams.ids.isNotEmpty())
queryParams.ids.joinToString(
if (entitiesQuery.ids.isNotEmpty())
entitiesQuery.ids.joinToString(
separator = ",",
prefix = "entity_payload.entity_id in(",
postfix = ")"
) { "'$it'" }
else null
val formattedIdPattern =
if (!queryParams.idPattern.isNullOrEmpty())
"entity_payload.entity_id ~ '${queryParams.idPattern}'"
if (!entitiesQuery.idPattern.isNullOrEmpty())
"entity_payload.entity_id ~ '${entitiesQuery.idPattern}'"
else null
val formattedType = queryParams.type?.let { buildTypeQuery(it) }
val formattedType = entitiesQuery.type?.let { buildTypeQuery(it) }
val formattedAttrs =
if (queryParams.attrs.isNotEmpty())
queryParams.attrs.joinToString(
if (entitiesQuery.attrs.isNotEmpty())
entitiesQuery.attrs.joinToString(
separator = ",",
prefix = "attribute_name in (",
postfix = ")"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,19 @@ class QueryService(
}

suspend fun queryEntities(
queryParams: QueryParams,
entitiesQuery: EntitiesQuery,
accessRightFilter: () -> String?
): Either<APIException, Pair<List<JsonLdEntity>, Int>> = either {
val entitiesIds = entityPayloadService.queryEntities(queryParams, accessRightFilter)
val count = entityPayloadService.queryEntitiesCount(queryParams, accessRightFilter).bind()
val entitiesIds = entityPayloadService.queryEntities(entitiesQuery, accessRightFilter)
val count = entityPayloadService.queryEntitiesCount(entitiesQuery, accessRightFilter).bind()

// we can have an empty list of entities with a non-zero count (e.g., offset too high)
if (entitiesIds.isEmpty())
return@either Pair<List<JsonLdEntity>, Int>(emptyList(), count)

val entitiesPayloads =
entityPayloadService.retrieve(entitiesIds)
.map { toJsonLdEntity(it, listOf(queryParams.context)) }
.map { toJsonLdEntity(it, listOf(entitiesQuery.context)) }

Pair(entitiesPayloads, count).right().bind()
}
Expand All @@ -56,11 +56,11 @@ class QueryService(
temporalEntitiesQuery: TemporalEntitiesQuery,
contextLink: String
): Either<APIException, CompactedJsonLdEntity> = either {
val attrs = temporalEntitiesQuery.queryParams.attrs
val attrs = temporalEntitiesQuery.entitiesQuery.attrs
val temporalEntityAttributes = temporalEntityAttributeService.getForEntity(entityId, attrs).let {
if (it.isEmpty())
ResourceNotFoundException(
entityOrAttrsNotFoundMessage(entityId.toString(), temporalEntitiesQuery.queryParams.attrs)
entityOrAttrsNotFoundMessage(entityId.toString(), temporalEntitiesQuery.entitiesQuery.attrs)
).left()
else it.right()
}.bind()
Expand Down Expand Up @@ -107,7 +107,7 @@ class QueryService(
val originForTemporalEntityAttributes =
attributeInstanceService.selectOldestDate(temporalQuery, temporalEntityAttributes)

val attrs = temporalEntitiesQuery.queryParams.attrs
val attrs = temporalEntitiesQuery.entitiesQuery.attrs
val originForScope =
if (attrs.isEmpty() || attrs.contains(NGSILD_SCOPE_PROPERTY))
scopeService.selectOldestDate(entityId, temporalEntitiesQuery.temporalQuery.timeproperty)
Expand All @@ -125,9 +125,9 @@ class QueryService(
temporalEntitiesQuery: TemporalEntitiesQuery,
accessRightFilter: () -> String?
): Either<APIException, Pair<List<CompactedJsonLdEntity>, Int>> = either {
val attrs = temporalEntitiesQuery.queryParams.attrs
val entitiesIds = entityPayloadService.queryEntities(temporalEntitiesQuery.queryParams, accessRightFilter)
val count = entityPayloadService.queryEntitiesCount(temporalEntitiesQuery.queryParams, accessRightFilter)
val attrs = temporalEntitiesQuery.entitiesQuery.attrs
val entitiesIds = entityPayloadService.queryEntities(temporalEntitiesQuery.entitiesQuery, accessRightFilter)
val count = entityPayloadService.queryEntitiesCount(temporalEntitiesQuery.entitiesQuery, accessRightFilter)
.getOrElse { 0 }

// we can have an empty list of entities with a non-zero count (e.g., offset too high)
Expand All @@ -136,7 +136,7 @@ class QueryService(

val temporalEntityAttributes = temporalEntityAttributeService.getForTemporalEntities(
entitiesIds,
temporalEntitiesQuery.queryParams
temporalEntitiesQuery.entitiesQuery
)

val scopesHistory =
Expand Down Expand Up @@ -174,7 +174,7 @@ class QueryService(
TemporalEntityBuilder.buildTemporalEntities(
attributeInstancesPerEntityAndAttribute,
temporalEntitiesQuery,
listOf(temporalEntitiesQuery.queryParams.context)
listOf(temporalEntitiesQuery.entitiesQuery.context)
),
count
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -357,11 +357,11 @@ class TemporalEntityAttributeService(

suspend fun getForTemporalEntities(
entitiesIds: List<URI>,
queryParams: QueryParams
entitiesQuery: EntitiesQuery
): List<TemporalEntityAttribute> {
val filterOnAttributes =
if (queryParams.attrs.isNotEmpty())
" AND " + queryParams.attrs.joinToString(
if (entitiesQuery.attrs.isNotEmpty())
" AND " + entitiesQuery.attrs.joinToString(
separator = ",",
prefix = "attribute_name in (",
postfix = ")"
Expand Down
Loading
Loading