diff --git a/search-service/config/detekt/baseline.xml b/search-service/config/detekt/baseline.xml
index b2b802489..f467d3c62 100644
--- a/search-service/config/detekt/baseline.xml
+++ b/search-service/config/detekt/baseline.xml
@@ -9,9 +9,9 @@
LongMethod:AttributeInstanceService.kt$AttributeInstanceService$@Transactional suspend fun create(attributeInstance: AttributeInstance): Either<APIException, Unit>
LongMethod:EnabledAuthorizationServiceTests.kt$EnabledAuthorizationServiceTests$@Test fun `it should return serialized access control entities with other rigths if user is owner`()
LongMethod:EntityAccessControlHandler.kt$EntityAccessControlHandler$@PostMapping("/{subjectId}/attrs", consumes = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) suspend fun addRightsOnEntities( @RequestHeader httpHeaders: HttpHeaders, @PathVariable subjectId: String, @RequestBody requestBody: Mono<String>, @AllowedParameters @RequestParam queryParams: MultiValueMap<String, String> ): ResponseEntity<*>
+ LongMethod:EntityEventService.kt$EntityEventService$private fun publishAttributeChangeEvent( sub: String?, tenantName: String, entityId: URI, entityTypesAndPayload: Pair<List<ExpandedTerm>, String>, attributeOperationResult: SucceededAttributeOperationResult )
LongMethod:EntityHandler.kt$EntityHandler$@GetMapping("/{entityId}", produces = [APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE, GEO_JSON_CONTENT_TYPE]) suspend fun getByURI( @RequestHeader httpHeaders: HttpHeaders, @PathVariable entityId: URI, @AllowedParameters( implemented = [ QP.OPTIONS, QP.TYPE, QP.ATTRS, QP.GEOMETRY_PROPERTY, QP.LANG, QP.CONTAINED_BY, QP.JOIN, QP.JOIN_LEVEL, QP.DATASET_ID, ], notImplemented = [QP.FORMAT, QP.PICK, QP.OMIT, QP.ENTITY_MAP, QP.LOCAL, QP.VIA] ) @RequestParam queryParams: MultiValueMap<String, String> ): ResponseEntity<*>
LongMethod:EntityHandler.kt$EntityHandler$@GetMapping(produces = [APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE, GEO_JSON_CONTENT_TYPE]) suspend fun getEntities( @RequestHeader httpHeaders: HttpHeaders, @AllowedParameters( implemented = [ QP.OPTIONS, QP.COUNT, QP.OFFSET, QP.LIMIT, QP.ID, QP.TYPE, QP.ID_PATTERN, QP.ATTRS, QP.Q, QP.GEOMETRY, QP.GEOREL, QP.COORDINATES, QP.GEOPROPERTY, QP.GEOMETRY_PROPERTY, QP.LANG, QP.SCOPEQ, QP.CONTAINED_BY, QP.JOIN, QP.JOIN_LEVEL, QP.DATASET_ID, ], notImplemented = [QP.FORMAT, QP.PICK, QP.OMIT, QP.EXPAND_VALUES, QP.CSF, QP.ENTITY_MAP, QP.LOCAL, QP.VIA] ) @RequestParam queryParams: MultiValueMap<String, String> ): ResponseEntity<*>
- LongMethod:EntityEventService.kt$EntityEventService$private fun publishAttributeChangeEvent( updatedDetails: UpdatedDetails, sub: String?, tenantName: String, entityId: URI, entityTypesAndPayload: Pair<List<ExpandedTerm>, String>, serializedAttribute: Pair<ExpandedTerm, String>, overwrite: Boolean )
LongMethod:LinkedEntityServiceTests.kt$LinkedEntityServiceTests$@Test fun `it should inline entities up to the asked 2nd level`()
LongMethod:PatchAttributeTests.kt$PatchAttributeTests.Companion$@JvmStatic fun mergePatchProvider(): Stream<Arguments>
LongMethod:PatchAttributeTests.kt$PatchAttributeTests.Companion$@JvmStatic fun partialUpdatePatchProvider(): Stream<Arguments>
@@ -28,7 +28,6 @@
LongParameterList:EntityAttributeService.kt$EntityAttributeService$( entityId: URI, attributeName: ExpandedTerm, attributeMetadata: AttributeMetadata, createdAt: ZonedDateTime, attributePayload: ExpandedAttributeInstance, sub: Sub? )
LongParameterList:EntityAttributeService.kt$EntityAttributeService$( entityUri: URI, ngsiLdAttributes: List<NgsiLdAttribute>, expandedAttributes: ExpandedAttributes, createdAt: ZonedDateTime, observedAt: ZonedDateTime?, sub: Sub? )
LongParameterList:EntityAttributeService.kt$EntityAttributeService$( entityUri: URI, ngsiLdAttributes: List<NgsiLdAttribute>, expandedAttributes: ExpandedAttributes, disallowOverwrite: Boolean, createdAt: ZonedDateTime, sub: Sub? )
- LongParameterList:EntityEventService.kt$EntityEventService$( updatedDetails: UpdatedDetails, sub: String?, tenantName: String, entityId: URI, entityTypesAndPayload: Pair<List<ExpandedTerm>, String>, serializedAttribute: Pair<ExpandedTerm, String>, overwrite: Boolean )
LongParameterList:TemporalEntityHandler.kt$TemporalEntityHandler$( @RequestHeader httpHeaders: HttpHeaders, @PathVariable entityId: URI, @PathVariable attrId: String, @PathVariable instanceId: URI, @RequestBody requestBody: Mono<String>, @AllowedParameters(notImplemented = [QP.LOCAL, QP.VIA]) @RequestParam queryParams: MultiValueMap<String, String> )
LongParameterList:V0_29__JsonLd_migration.kt$V0_29__JsonLd_migration$( entityId: URI, attributeName: ExpandedTerm, datasetId: URI?, attributePayload: ExpandedAttributeInstance, ngsiLdAttributeInstance: NgsiLdAttributeInstance, defaultCreatedAt: ZonedDateTime )
NestedBlockDepth:V0_29__JsonLd_migration.kt$V0_29__JsonLd_migration$override fun migrate(context: Context)
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/web/EntityAccessControlHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/web/EntityAccessControlHandler.kt
index 7de1a6867..cbbdf667c 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/web/EntityAccessControlHandler.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/web/EntityAccessControlHandler.kt
@@ -4,10 +4,11 @@ import arrow.core.left
import arrow.core.raise.either
import com.egm.stellio.search.authorization.service.AuthorizationService
import com.egm.stellio.search.authorization.service.EntityAccessRightsService
+import com.egm.stellio.search.entity.model.FailedAttributeOperationResult
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.model.OperationStatus
+import com.egm.stellio.search.entity.model.SucceededAttributeOperationResult
+import com.egm.stellio.search.entity.model.UpdateResult
import com.egm.stellio.search.entity.util.composeEntitiesQueryFromGet
import com.egm.stellio.shared.config.ApplicationProperties
import com.egm.stellio.shared.model.AccessDeniedException
@@ -257,24 +258,24 @@ class EntityAccessControlHandler(
AccessRight.forAttributeName(ngsiLdRel.name).getOrNull()!!
).fold(
ifLeft = { apiException ->
- UpdateAttributeResult(
+ FailedAttributeOperationResult(
ngsiLdRel.name,
ngsiLdRelInstance.datasetId,
- UpdateOperationResult.FAILED,
+ OperationStatus.FAILED,
apiException.message
)
},
ifRight = {
- UpdateAttributeResult(
+ SucceededAttributeOperationResult(
ngsiLdRel.name,
ngsiLdRelInstance.datasetId,
- UpdateOperationResult.APPENDED,
- null
+ OperationStatus.APPENDED,
+ emptyMap()
)
}
)
}
- val appendResult = updateResultFromDetailedResult(results)
+ val appendResult = UpdateResult(results)
if (invalidAttributes.isEmpty() && unauthorizedInstances.isEmpty())
ResponseEntity.status(HttpStatus.NO_CONTENT).build()
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/listener/ObservationEventListener.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/listener/ObservationEventListener.kt
index cb9a6f115..db1a4a7c9 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/listener/ObservationEventListener.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/listener/ObservationEventListener.kt
@@ -3,6 +3,8 @@ package com.egm.stellio.search.entity.listener
import arrow.core.Either
import arrow.core.left
import arrow.core.raise.either
+import com.egm.stellio.search.entity.model.OperationStatus
+import com.egm.stellio.search.entity.model.SucceededAttributeOperationResult
import com.egm.stellio.search.entity.service.EntityEventService
import com.egm.stellio.search.entity.service.EntityService
import com.egm.stellio.shared.model.APIException
@@ -120,9 +122,14 @@ class ObservationEventListener(
entityEventService.publishAttributeChangeEvents(
observationEvent.sub,
observationEvent.entityId,
- expandedAttribute.toExpandedAttributes(),
- it,
- false
+ listOf(
+ SucceededAttributeOperationResult(
+ observationEvent.attributeName,
+ observationEvent.datasetId,
+ OperationStatus.UPDATED,
+ expandedAttribute.toExpandedAttributes()
+ )
+ )
)
}
}
@@ -143,7 +150,7 @@ class ObservationEventListener(
entityService.appendAttributes(
observationEvent.entityId,
expandedAttribute.toExpandedAttributes(),
- !observationEvent.overwrite,
+ false,
observationEvent.sub
).map {
if (it.notUpdated.isNotEmpty()) {
@@ -157,9 +164,14 @@ class ObservationEventListener(
entityEventService.publishAttributeChangeEvents(
observationEvent.sub,
observationEvent.entityId,
- expandedAttribute.toExpandedAttributes(),
- it,
- observationEvent.overwrite
+ listOf(
+ SucceededAttributeOperationResult(
+ observationEvent.attributeName,
+ observationEvent.datasetId,
+ OperationStatus.APPENDED,
+ expandedAttribute.toExpandedAttributes()
+ )
+ )
)
}
}
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/Attribute.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/Attribute.kt
index f3d4474af..d776b3572 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/Attribute.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/Attribute.kt
@@ -7,6 +7,7 @@ import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_OBJECT
import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_TYPE_TERM
import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_VALUE_TERM
+import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_DATASET_ID_PROPERTY
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_GEOPROPERTY_TYPE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_GEOPROPERTY_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_GEOPROPERTY_VALUES
@@ -27,6 +28,7 @@ import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_RELATIONSHIP_TYPE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_VOCABPROPERTY_TYPE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_VOCABPROPERTY_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_VOCABPROPERTY_VALUES
+import com.egm.stellio.shared.util.JsonLdUtils.buildNonReifiedPropertyValue
import com.egm.stellio.shared.util.JsonUtils.serializeObject
import io.r2dbc.postgresql.codec.Json
import org.springframework.data.annotation.Id
@@ -107,7 +109,7 @@ data class Attribute(
VocabProperty -> NGSILD_VOCABPROPERTY_VALUES
}
- fun toNullCompactedRepresentation(): Map =
+ fun toNullCompactedRepresentation(datasetId: URI? = null): Map =
when (this) {
Property, GeoProperty, JsonProperty, VocabProperty ->
mapOf(
@@ -124,6 +126,12 @@ data class Attribute(
JSONLD_TYPE_TERM to this.name,
JSONLD_LANGUAGEMAP_TERM to mapOf(NGSILD_NONE_TERM to NGSILD_NULL)
)
+ }.let { nullAttrRepresentation ->
+ if (datasetId != null)
+ nullAttrRepresentation.plus(
+ NGSILD_DATASET_ID_PROPERTY to buildNonReifiedPropertyValue(datasetId.toString())
+ )
+ else nullAttrRepresentation
}
fun toNullValue(): String =
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/Entity.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/Entity.kt
index f54601471..9461f2860 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/Entity.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/Entity.kt
@@ -23,6 +23,7 @@ data class Entity(
val scopes: List? = null,
val createdAt: ZonedDateTime,
val modifiedAt: ZonedDateTime? = null,
+ val deletedAt: ZonedDateTime? = null,
val payload: Json,
val specificAccessPolicy: SpecificAccessPolicy? = null
) {
@@ -47,17 +48,19 @@ data class Entity(
return resultEntity
}
- companion object {
-
- fun toExpandedDeletedEntity(
- entityId: URI,
- deletedAt: ZonedDateTime
- ): ExpandedEntity =
- ExpandedEntity(
- members = mapOf(
- JSONLD_ID to entityId,
- NGSILD_DELETED_AT_PROPERTY to buildNonReifiedTemporalValue(deletedAt)
- )
- )
- }
+ fun toExpandedDeletedEntity(
+ deletedAt: ZonedDateTime
+ ): ExpandedEntity =
+ ExpandedEntity(
+ members = mapOf(
+ JSONLD_ID to entityId,
+ JSONLD_TYPE to types,
+ NGSILD_CREATED_AT_PROPERTY to buildNonReifiedTemporalValue(createdAt),
+ NGSILD_DELETED_AT_PROPERTY to buildNonReifiedTemporalValue(deletedAt),
+ ).run {
+ if (modifiedAt != null)
+ this.plus(NGSILD_MODIFIED_AT_PROPERTY to buildNonReifiedTemporalValue(modifiedAt))
+ else this
+ }
+ )
}
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/UpdateResult.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/UpdateResult.kt
index b854ede7d..a34eef570 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/UpdateResult.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/UpdateResult.kt
@@ -1,64 +1,72 @@
package com.egm.stellio.search.entity.model
+import com.egm.stellio.shared.model.ExpandedAttributeInstance
import com.fasterxml.jackson.annotation.JsonIgnore
-import com.fasterxml.jackson.annotation.JsonValue
import java.net.URI
+/**
+ * UpdateResult datatype as defined in 5.2.18
+ */
data class UpdateResult(
- val updated: List,
+ val updated: List,
val notUpdated: List
) {
@JsonIgnore
fun isSuccessful(): Boolean =
- notUpdated.isEmpty() &&
- updated.all { it.updateOperationResult.isSuccessResult() }
+ notUpdated.isEmpty()
- @JsonIgnore
- fun mergeWith(other: UpdateResult): UpdateResult =
- UpdateResult(
- updated = this.updated.plus(other.updated),
- notUpdated = this.notUpdated.plus(other.notUpdated)
- )
+ companion object {
- @JsonIgnore
- fun hasSuccessfulUpdate(): Boolean =
- this.updated.isNotEmpty()
+ operator fun invoke(operationsResults: List): UpdateResult =
+ operationsResults.map {
+ when (it) {
+ is SucceededAttributeOperationResult -> it.attributeName
+ is FailedAttributeOperationResult -> NotUpdatedDetails(it.attributeName, it.errorMessage)
+ }
+ }.let {
+ UpdateResult(
+ it.filterIsInstance(),
+ it.filterIsInstance()
+ )
+ }
+ }
}
val EMPTY_UPDATE_RESULT: UpdateResult = UpdateResult(emptyList(), emptyList())
+/**
+ * NotUpdatedDetails as defined in 5.2.19
+ */
data class NotUpdatedDetails(
val attributeName: String,
val reason: String
)
-data class UpdatedDetails(
- @JsonValue
- val attributeName: String,
- @JsonIgnore
- val datasetId: URI?,
- @JsonIgnore
- val updateOperationResult: UpdateOperationResult
+/**
+ * Internal structure used to convey the result of an operation (update, delete...)
+ */
+sealed class AttributeOperationResult(
+ open val attributeName: String,
+ open val datasetId: URI? = null,
+ open val operationStatus: OperationStatus
)
-data class UpdateAttributeResult(
- val attributeName: String,
- val datasetId: URI? = null,
- val updateOperationResult: UpdateOperationResult,
- val errorMessage: String? = null
-) {
- fun isSuccessfullyUpdated() =
- this.updateOperationResult in listOf(
- UpdateOperationResult.APPENDED,
- UpdateOperationResult.REPLACED,
- UpdateOperationResult.UPDATED,
- UpdateOperationResult.DELETED,
- UpdateOperationResult.IGNORED
- )
-}
+data class SucceededAttributeOperationResult(
+ override val attributeName: String,
+ override val datasetId: URI? = null,
+ override val operationStatus: OperationStatus,
+ val newExpandedValue: ExpandedAttributeInstance,
+) : AttributeOperationResult(attributeName, datasetId, operationStatus)
+
+data class FailedAttributeOperationResult(
+ override val attributeName: String,
+ override val datasetId: URI? = null,
+ override val operationStatus: OperationStatus,
+ val errorMessage: String
+) : AttributeOperationResult(attributeName, datasetId, operationStatus)
-enum class UpdateOperationResult {
+enum class OperationStatus {
APPENDED,
REPLACED,
UPDATED,
@@ -66,15 +74,15 @@ enum class UpdateOperationResult {
IGNORED,
FAILED;
- fun isSuccessResult(): Boolean = listOf(APPENDED, REPLACED, UPDATED, DELETED).contains(this)
-}
+ fun isSuccessResult(): Boolean = getSuccessStatuses().contains(this)
-fun updateResultFromDetailedResult(updateStatuses: List): UpdateResult {
- val updated = updateStatuses.filter { it.isSuccessfullyUpdated() }
- .map { UpdatedDetails(it.attributeName, it.datasetId, it.updateOperationResult) }
+ companion object {
+ fun getSuccessStatuses(): List = listOf(APPENDED, REPLACED, UPDATED, DELETED, IGNORED)
+ }
+}
- val notUpdated = updateStatuses.filter { !it.isSuccessfullyUpdated() }
- .map { NotUpdatedDetails(it.attributeName, it.errorMessage!!) }
+fun List.hasSuccessfulResult(): Boolean =
+ this.any { it is SucceededAttributeOperationResult }
- return UpdateResult(updated, notUpdated)
-}
+fun List.getSucceededOperations(): List =
+ this.filterIsInstance()
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityAttributeService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityAttributeService.kt
index 72dc47c7b..223cec0e4 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityAttributeService.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityAttributeService.kt
@@ -22,11 +22,11 @@ import com.egm.stellio.search.common.util.valueToDoubleOrNull
import com.egm.stellio.search.common.util.valueToStringOrNull
import com.egm.stellio.search.entity.model.Attribute
import com.egm.stellio.search.entity.model.AttributeMetadata
+import com.egm.stellio.search.entity.model.AttributeOperationResult
import com.egm.stellio.search.entity.model.EntitiesQuery
-import com.egm.stellio.search.entity.model.UpdateAttributeResult
-import com.egm.stellio.search.entity.model.UpdateOperationResult
-import com.egm.stellio.search.entity.model.UpdateResult
-import com.egm.stellio.search.entity.model.updateResultFromDetailedResult
+import com.egm.stellio.search.entity.model.FailedAttributeOperationResult
+import com.egm.stellio.search.entity.model.OperationStatus
+import com.egm.stellio.search.entity.model.SucceededAttributeOperationResult
import com.egm.stellio.search.entity.util.guessAttributeValueType
import com.egm.stellio.search.entity.util.hasNgsiLdNullValue
import com.egm.stellio.search.entity.util.mergePatch
@@ -48,6 +48,7 @@ import com.egm.stellio.shared.model.NgsiLdAttribute
import com.egm.stellio.shared.model.NgsiLdEntity
import com.egm.stellio.shared.model.ResourceNotFoundException
import com.egm.stellio.shared.model.WKTCoordinates
+import com.egm.stellio.shared.model.addSysAttrs
import com.egm.stellio.shared.model.flatOnInstances
import com.egm.stellio.shared.model.getAttributeFromExpandedAttributes
import com.egm.stellio.shared.model.getDatasetId
@@ -83,7 +84,6 @@ import org.springframework.transaction.annotation.Transactional
import java.net.URI
import java.time.ZonedDateTime
import java.util.*
-import kotlin.collections.map
@Service
class EntityAttributeService(
@@ -204,40 +204,39 @@ class EntityAttributeService(
createdAt: ZonedDateTime,
attributePayload: ExpandedAttributeInstance,
sub: Sub?
- ): Either =
- either {
- logger.debug("Adding attribute {} to entity {}", attributeName, entityId)
- val attribute = Attribute(
- entityId = entityId,
- attributeName = attributeName,
- attributeType = attributeMetadata.type,
- attributeValueType = attributeMetadata.valueType,
- datasetId = attributeMetadata.datasetId,
- createdAt = createdAt,
- payload = Json.of(serializeObject(attributePayload))
- )
- create(attribute).bind()
+ ): Either = either {
+ logger.debug("Adding attribute {} to entity {}", attributeName, entityId)
+ val attribute = Attribute(
+ entityId = entityId,
+ attributeName = attributeName,
+ attributeType = attributeMetadata.type,
+ attributeValueType = attributeMetadata.valueType,
+ datasetId = attributeMetadata.datasetId,
+ createdAt = createdAt,
+ payload = Json.of(serializeObject(attributePayload))
+ )
+ create(attribute).bind()
+
+ val attributeInstance = AttributeInstance(
+ attributeUuid = attribute.id,
+ timeProperty = AttributeInstance.TemporalProperty.CREATED_AT,
+ time = createdAt,
+ attributeMetadata = attributeMetadata,
+ payload = attributePayload,
+ sub = sub
+ )
+ attributeInstanceService.create(attributeInstance).bind()
- val attributeInstance = AttributeInstance(
+ if (attributeMetadata.observedAt != null) {
+ val attributeObservedAtInstance = AttributeInstance(
attributeUuid = attribute.id,
- timeProperty = AttributeInstance.TemporalProperty.CREATED_AT,
- time = createdAt,
+ time = attributeMetadata.observedAt,
attributeMetadata = attributeMetadata,
- payload = attributePayload,
- sub = sub
+ payload = attributePayload
)
- attributeInstanceService.create(attributeInstance).bind()
-
- if (attributeMetadata.observedAt != null) {
- val attributeObservedAtInstance = AttributeInstance(
- attributeUuid = attribute.id,
- time = attributeMetadata.observedAt,
- attributeMetadata = attributeMetadata,
- payload = attributePayload
- )
- attributeInstanceService.create(attributeObservedAtInstance).bind()
- }
+ attributeInstanceService.create(attributeObservedAtInstance).bind()
}
+ }
@Transactional
suspend fun replaceAttribute(
@@ -310,53 +309,64 @@ class EntityAttributeService(
datasetId: URI?,
deleteAll: Boolean = false,
deletedAt: ZonedDateTime
- ): Either = either {
+ ): Either> = either {
logger.debug("Deleting attribute {} from entity {} (all: {})", attributeName, entityId, deleteAll)
val attributesToDelete =
if (deleteAll)
getForEntity(entityId, setOf(attributeName), emptySet())
else
listOf(getForEntityAndAttribute(entityId, attributeName, datasetId).bind())
+
deleteSelectedAttributes(attributesToDelete, deletedAt).bind()
+ .map { expandedAttributeInstance ->
+ SucceededAttributeOperationResult(
+ attributeName,
+ datasetId,
+ OperationStatus.DELETED,
+ expandedAttributeInstance
+ )
+ }
}
@Transactional
internal suspend fun deleteSelectedAttributes(
attributesToDelete: List,
deletedAt: ZonedDateTime
- ): Either = either {
- if (attributesToDelete.isEmpty()) return Unit.right()
+ ): Either> = either {
+ if (attributesToDelete.isEmpty()) return emptyList().right()
val attributesToDeleteWithPayload = attributesToDelete.map {
- Triple(
+ Pair(
it,
- deletedAt,
JsonLdUtils.expandAttribute(
it.attributeName,
- it.attributeType.toNullCompactedRepresentation(),
+ it.attributeType.toNullCompactedRepresentation(it.datasetId),
listOf(applicationProperties.contexts.core)
).second[0]
)
}
- databaseClient.sql(
+ val teasTimestamps = databaseClient.sql(
"""
UPDATE temporal_entity_attribute
SET deleted_at = new.deleted_at,
payload = new.payload
FROM (VALUES :values) AS new(uuid, deleted_at, payload)
WHERE temporal_entity_attribute.id = new.uuid
+ RETURNING id, created_at, modified_at, new.deleted_at
""".trimIndent()
)
- .bind("values", attributesToDeleteWithPayload.map { arrayOf(it.first.id, it.second, it.third.toJson()) })
- .allToMappedList {
- Triple(
- toUuid(it["id"]),
- Attribute.AttributeType.valueOf(it["attribute_type"] as String),
- it["attribute_name"] as String
+ .bind("values", attributesToDeleteWithPayload.map { arrayOf(it.first.id, deletedAt, it.second.toJson()) })
+ .allToMappedList { row ->
+ mapOf(
+ toUuid(row["id"]) to Triple(
+ toZonedDateTime(row["created_at"]),
+ toOptionalZonedDateTime(row["modified_at"]),
+ toZonedDateTime(row["deleted_at"])
+ )
)
}
- attributesToDeleteWithPayload.forEach { (attribute, deletedAt, expandedAttributePayload) ->
+ attributesToDeleteWithPayload.forEach { (attribute, expandedAttributePayload) ->
attributeInstanceService.addDeletedAttributeInstance(
attributeUuid = attribute.id,
value = attribute.attributeType.toNullValue(),
@@ -364,6 +374,11 @@ class EntityAttributeService(
attributeValues = expandedAttributePayload
).bind()
}
+
+ attributesToDeleteWithPayload.map { (attribute, expandedAttributeInstance) ->
+ val teaTimestamps = teasTimestamps.find { it.containsKey(attribute.id) }!!.values.first()
+ expandedAttributeInstance.addSysAttrs(true, teaTimestamps.first, teaTimestamps.second, teaTimestamps.third)
+ }
}
@Transactional
@@ -588,7 +603,7 @@ class EntityAttributeService(
disallowOverwrite: Boolean,
createdAt: ZonedDateTime,
sub: Sub?
- ): Either = either {
+ ): Either> = either {
val attributeInstances = ngsiLdAttributes.flatOnInstances()
attributeInstances.parMap { (ngsiLdAttribute, ngsiLdAttributeInstance) ->
logger.debug("Appending attribute {} in entity {}", ngsiLdAttribute.name, entityUri)
@@ -609,20 +624,20 @@ class EntityAttributeService(
attributePayload,
sub
).map {
- UpdateAttributeResult(
+ SucceededAttributeOperationResult(
ngsiLdAttribute.name,
ngsiLdAttributeInstance.datasetId,
- UpdateOperationResult.APPENDED
+ OperationStatus.APPENDED,
+ attributePayload
)
}.bind()
} else if (disallowOverwrite) {
- val message = "Attribute already exists on $entityUri and overwrite is not allowed, ignoring"
- logger.info(message)
- UpdateAttributeResult(
+ logger.info("Attribute already exists on $entityUri and overwrite is not allowed, ignoring")
+ SucceededAttributeOperationResult(
ngsiLdAttribute.name,
ngsiLdAttributeInstance.datasetId,
- UpdateOperationResult.IGNORED,
- message
+ OperationStatus.IGNORED,
+ attributePayload
).right().bind()
} else {
replaceAttribute(
@@ -633,15 +648,16 @@ class EntityAttributeService(
attributePayload,
sub
).map {
- UpdateAttributeResult(
+ SucceededAttributeOperationResult(
ngsiLdAttribute.name,
ngsiLdAttributeInstance.datasetId,
- UpdateOperationResult.REPLACED
+ OperationStatus.REPLACED,
+ attributePayload
)
}.bind()
}
}
- }.fold({ it.left() }, { updateResultFromDetailedResult(it).right() })
+ }.fold({ it.left() }, { it.right() })
@Transactional
suspend fun updateAttributes(
@@ -650,7 +666,7 @@ class EntityAttributeService(
expandedAttributes: ExpandedAttributes,
createdAt: ZonedDateTime,
sub: Sub?
- ): Either = either {
+ ): Either> = either {
val attributeInstances = ngsiLdAttributes.flatOnInstances()
attributeInstances.parMap { (ngsiLdAttribute, ngsiLdAttributeInstance) ->
logger.debug("Updating attribute {} in entity {}", ngsiLdAttribute.name, entityUri)
@@ -672,10 +688,11 @@ class EntityAttributeService(
attributePayload,
sub
).map {
- UpdateAttributeResult(
+ SucceededAttributeOperationResult(
ngsiLdAttribute.name,
ngsiLdAttributeInstance.datasetId,
- UpdateOperationResult.APPENDED
+ OperationStatus.APPENDED,
+ attributePayload
)
}.bind()
} else if (hasNgsiLdNullValue(attributePayload, currentAttribute.attributeType)) {
@@ -685,29 +702,26 @@ class EntityAttributeService(
ngsiLdAttributeInstance.datasetId,
false,
createdAt
- ).map {
- UpdateAttributeResult(
- ngsiLdAttribute.name,
- ngsiLdAttributeInstance.datasetId,
- UpdateOperationResult.DELETED
- )
- }.bind()
+ ).bind().first()
} else {
- partialUpdateAttribute(
- entityUri,
- Pair(ngsiLdAttribute.name, listOf(attributePayload)),
+ replaceAttribute(
+ currentAttribute,
+ ngsiLdAttribute,
+ attributeMetadata,
createdAt,
+ attributePayload,
sub
).map {
- UpdateAttributeResult(
+ SucceededAttributeOperationResult(
ngsiLdAttribute.name,
ngsiLdAttributeInstance.datasetId,
- UpdateOperationResult.REPLACED
+ OperationStatus.REPLACED,
+ attributePayload
)
}.bind()
}
}
- }.fold({ it.left() }, { updateResultFromDetailedResult(it).right() })
+ }.fold({ it.left() }, { it.right() })
@Transactional
suspend fun partialUpdateAttribute(
@@ -715,19 +729,19 @@ class EntityAttributeService(
expandedAttribute: ExpandedAttribute,
modifiedAt: ZonedDateTime,
sub: Sub?
- ): Either = either {
+ ): Either = either {
val attributeName = expandedAttribute.first
val attributeValues = expandedAttribute.second[0]
logger.debug("Partial updating attribute {} in entity {}", attributeName, entityId)
val datasetId = attributeValues.getDatasetId()
val currentAttribute = getForEntityAndAttribute(entityId, attributeName, datasetId).fold({ null }, { it })
- val updateAttributeResult =
+ val attributeOperationResult =
if (currentAttribute == null) {
- UpdateAttributeResult(
+ FailedAttributeOperationResult(
attributeName,
datasetId,
- UpdateOperationResult.FAILED,
+ OperationStatus.FAILED,
"Unknown attribute $attributeName with datasetId $datasetId in entity $entityId"
)
} else if (hasNgsiLdNullValue(attributeValues, currentAttribute.attributeType)) {
@@ -737,13 +751,7 @@ class EntityAttributeService(
datasetId,
false,
modifiedAt
- ).map {
- UpdateAttributeResult(
- attributeName,
- datasetId,
- UpdateOperationResult.DELETED
- )
- }.bind()
+ ).bind().first()
} else {
// first update payload in temporal entity attribute
val attribute = getForEntityAndAttribute(entityId, attributeName, datasetId).bind()
@@ -768,14 +776,15 @@ class EntityAttributeService(
)
attributeInstanceService.create(attributeInstance).bind()
- UpdateAttributeResult(
+ SucceededAttributeOperationResult(
attributeName,
datasetId,
- UpdateOperationResult.UPDATED
+ OperationStatus.UPDATED,
+ updatedAttributeInstance
)
}
- updateResultFromDetailedResult(listOf(updateAttributeResult))
+ attributeOperationResult
}
@Transactional
@@ -829,7 +838,7 @@ class EntityAttributeService(
createdAt: ZonedDateTime,
observedAt: ZonedDateTime?,
sub: Sub?
- ): Either = either {
+ ): Either> = either {
val attributeInstances = ngsiLdAttributes.flatOnInstances()
attributeInstances.parMap { (ngsiLdAttribute, ngsiLdAttributeInstance) ->
logger.debug("Merging attribute {} in entity {}", ngsiLdAttribute.name, entityUri)
@@ -851,10 +860,11 @@ class EntityAttributeService(
attributePayload,
sub
).map {
- UpdateAttributeResult(
+ SucceededAttributeOperationResult(
ngsiLdAttribute.name,
ngsiLdAttributeInstance.datasetId,
- UpdateOperationResult.APPENDED
+ OperationStatus.APPENDED,
+ attributePayload
)
}.bind()
else if (hasNgsiLdNullValue(attributePayload, currentAttribute.attributeType))
@@ -864,13 +874,7 @@ class EntityAttributeService(
ngsiLdAttributeInstance.datasetId,
false,
createdAt
- ).map {
- UpdateAttributeResult(
- ngsiLdAttribute.name,
- ngsiLdAttributeInstance.datasetId,
- UpdateOperationResult.DELETED
- )
- }.bind()
+ ).bind().first()
else
mergeAttribute(
currentAttribute,
@@ -881,14 +885,15 @@ class EntityAttributeService(
attributePayload,
sub
).map {
- UpdateAttributeResult(
+ SucceededAttributeOperationResult(
ngsiLdAttribute.name,
ngsiLdAttributeInstance.datasetId,
- UpdateOperationResult.UPDATED
+ OperationStatus.UPDATED,
+ attributePayload
)
}.bind()
}
- }.fold({ it.left() }, { updateResultFromDetailedResult(it).right() })
+ }.fold({ it.left() }, { it.right() })
@Transactional
suspend fun replaceAttribute(
@@ -897,19 +902,19 @@ class EntityAttributeService(
expandedAttribute: ExpandedAttribute,
replacedAt: ZonedDateTime,
sub: Sub?
- ): Either = either {
+ ): Either = either {
val ngsiLdAttributeInstance = ngsiLdAttribute.getAttributeInstances()[0]
val attributeName = ngsiLdAttribute.name
val datasetId = ngsiLdAttributeInstance.datasetId
val currentTea =
getForEntityAndAttribute(entityId, attributeName, datasetId).fold({ null }, { it })
val attributeMetadata = ngsiLdAttributeInstance.toAttributeMetadata().bind()
- val updateAttributeResult =
+ val attributeOperationResult =
if (currentTea == null) {
- UpdateAttributeResult(
+ FailedAttributeOperationResult(
attributeName,
datasetId,
- UpdateOperationResult.FAILED,
+ OperationStatus.FAILED,
"Unknown attribute $attributeName with datasetId $datasetId in entity $entityId"
)
} else {
@@ -922,14 +927,15 @@ class EntityAttributeService(
sub
).bind()
- UpdateAttributeResult(
+ SucceededAttributeOperationResult(
ngsiLdAttribute.name,
ngsiLdAttributeInstance.datasetId,
- UpdateOperationResult.REPLACED
+ OperationStatus.REPLACED,
+ expandedAttribute.second.first()
)
}
- updateResultFromDetailedResult(listOf(updateAttributeResult))
+ attributeOperationResult
}
suspend fun getValueFromPartialAttributePayload(
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityEventService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityEventService.kt
index 33d424f90..830a91a7b 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityEventService.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityEventService.kt
@@ -2,12 +2,10 @@ package com.egm.stellio.search.entity.service
import arrow.core.Either
import com.egm.stellio.search.entity.model.Entity
-import com.egm.stellio.search.entity.model.UpdateOperationResult
-import com.egm.stellio.search.entity.model.UpdateResult
-import com.egm.stellio.search.entity.model.UpdatedDetails
+import com.egm.stellio.search.entity.model.OperationStatus
+import com.egm.stellio.search.entity.model.SucceededAttributeOperationResult
import com.egm.stellio.shared.model.APIException
import com.egm.stellio.shared.model.AttributeAppendEvent
-import com.egm.stellio.shared.model.AttributeDeleteAllInstancesEvent
import com.egm.stellio.shared.model.AttributeDeleteEvent
import com.egm.stellio.shared.model.AttributeReplaceEvent
import com.egm.stellio.shared.model.AttributeUpdateEvent
@@ -16,10 +14,10 @@ import com.egm.stellio.shared.model.EntityDeleteEvent
import com.egm.stellio.shared.model.EntityEvent
import com.egm.stellio.shared.model.EntityReplaceEvent
import com.egm.stellio.shared.model.EventsType
-import com.egm.stellio.shared.model.ExpandedAttributes
+import com.egm.stellio.shared.model.ExpandedAttributeInstance
+import com.egm.stellio.shared.model.ExpandedEntity
import com.egm.stellio.shared.model.ExpandedTerm
-import com.egm.stellio.shared.model.getAttributeFromExpandedAttributes
-import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_TYPE
+import com.egm.stellio.shared.util.JsonUtils.deserializeAsMap
import com.egm.stellio.shared.util.JsonUtils.serializeObject
import com.egm.stellio.shared.util.getTenantFromContext
import kotlinx.coroutines.CoroutineScope
@@ -84,18 +82,20 @@ class EntityEventService(
suspend fun publishEntityDeleteEvent(
sub: String?,
- entity: Entity
+ previousEntity: Entity,
+ deletedEntityPayload: ExpandedEntity
): Job {
val tenantName = getTenantFromContext()
return coroutineScope.launch {
- logger.debug("Sending delete event for entity {} in tenant {}", entity.entityId, tenantName)
+ logger.debug("Sending delete event for entity {} in tenant {}", previousEntity.entityId, tenantName)
publishEntityEvent(
EntityDeleteEvent(
sub,
tenantName,
- entity.entityId,
- entity.types,
- entity.payload.asString(),
+ previousEntity.entityId,
+ previousEntity.types,
+ previousEntity.payload.asString(),
+ serializeObject(deletedEntityPayload.members),
emptyList()
)
)
@@ -105,27 +105,19 @@ class EntityEventService(
suspend fun publishAttributeChangeEvents(
sub: String?,
entityId: URI,
- jsonLdAttributes: Map,
- updateResult: UpdateResult,
- overwrite: Boolean
+ attributesOperationsResults: List
): Job {
val tenantName = getTenantFromContext()
val entity = getSerializedEntity(entityId)
return coroutineScope.launch {
- logger.debug("Sending attributes change events for entity {} in tenant {}", entityId, tenantName)
entity.onRight {
- updateResult.updated.forEach { updatedDetails ->
- val attributeName = updatedDetails.attributeName
- val serializedAttribute =
- getSerializedAttribute(jsonLdAttributes, attributeName, updatedDetails.datasetId)
+ attributesOperationsResults.forEach { attributeOperationResult ->
publishAttributeChangeEvent(
- updatedDetails,
sub,
tenantName,
entityId,
it,
- serializedAttribute,
- overwrite
+ attributeOperationResult
)
}
}.logAttributeEvent("Attribute Change", entityId, tenantName)
@@ -133,79 +125,85 @@ class EntityEventService(
}
private fun publishAttributeChangeEvent(
- updatedDetails: UpdatedDetails,
sub: String?,
tenantName: String,
entityId: URI,
entityTypesAndPayload: Pair, String>,
- serializedAttribute: Pair,
- overwrite: Boolean
+ attributeOperationResult: SucceededAttributeOperationResult
) {
- when (updatedDetails.updateOperationResult) {
- UpdateOperationResult.APPENDED ->
+ val attributeName = attributeOperationResult.attributeName
+ val (types, payload) = entityTypesAndPayload
+ logger.debug(
+ "Sending {} event for attribute {} of entity {} in tenant {}",
+ attributeOperationResult.operationStatus,
+ attributeName,
+ entityId,
+ tenantName
+ )
+ when (attributeOperationResult.operationStatus) {
+ OperationStatus.APPENDED ->
publishEntityEvent(
AttributeAppendEvent(
sub,
tenantName,
entityId,
- entityTypesAndPayload.first,
- serializedAttribute.first,
- updatedDetails.datasetId,
- overwrite,
- serializedAttribute.second,
- entityTypesAndPayload.second,
+ types,
+ attributeOperationResult.attributeName,
+ attributeOperationResult.datasetId,
+ serializeObject(attributeOperationResult.newExpandedValue),
+ payload,
emptyList()
)
)
- UpdateOperationResult.REPLACED ->
+ OperationStatus.REPLACED ->
publishEntityEvent(
AttributeReplaceEvent(
sub,
tenantName,
entityId,
- entityTypesAndPayload.first,
- serializedAttribute.first,
- updatedDetails.datasetId,
- serializedAttribute.second,
- entityTypesAndPayload.second,
+ types,
+ attributeOperationResult.attributeName,
+ attributeOperationResult.datasetId,
+ serializeObject(attributeOperationResult.newExpandedValue),
+ payload,
emptyList()
)
)
- UpdateOperationResult.UPDATED ->
+ OperationStatus.UPDATED ->
publishEntityEvent(
AttributeUpdateEvent(
sub,
tenantName,
entityId,
- entityTypesAndPayload.first,
- serializedAttribute.first,
- updatedDetails.datasetId,
- serializedAttribute.second,
- entityTypesAndPayload.second,
+ types,
+ attributeOperationResult.attributeName,
+ attributeOperationResult.datasetId,
+ serializeObject(attributeOperationResult.newExpandedValue),
+ payload,
emptyList()
)
)
- UpdateOperationResult.DELETED ->
+ OperationStatus.DELETED ->
publishEntityEvent(
AttributeDeleteEvent(
sub,
tenantName,
entityId,
- entityTypesAndPayload.first,
- serializedAttribute.first,
- updatedDetails.datasetId,
- serializedAttribute.second,
+ types,
+ attributeOperationResult.attributeName,
+ attributeOperationResult.datasetId,
+ injectDeletedAttribute(payload, attributeName, attributeOperationResult.newExpandedValue),
emptyList()
)
)
else ->
logger.warn(
- "Received an unexpected result (${updatedDetails.updateOperationResult} " +
- "for entity $entityId and attribute ${updatedDetails.attributeName}"
+ "Received an unexpected result (${attributeOperationResult.operationStatus} " +
+ "for entity $entityId and attribute ${attributeOperationResult.attributeName}"
)
}
}
@@ -213,11 +211,10 @@ class EntityEventService(
suspend fun publishAttributeDeleteEvent(
sub: String?,
entityId: URI,
- attributeName: ExpandedTerm,
- datasetId: URI? = null,
- deleteAll: Boolean
+ attributeOperationResult: SucceededAttributeOperationResult
): Job {
val tenantName = getTenantFromContext()
+ val attributeName = attributeOperationResult.attributeName
val entity = getSerializedEntity(entityId)
return coroutineScope.launch {
logger.debug(
@@ -227,31 +224,18 @@ class EntityEventService(
tenantName
)
entity.onRight {
- if (deleteAll)
- publishEntityEvent(
- AttributeDeleteAllInstancesEvent(
- sub,
- tenantName,
- entityId,
- it.first,
- attributeName,
- it.second,
- emptyList()
- )
- )
- else
- publishEntityEvent(
- AttributeDeleteEvent(
- sub,
- tenantName,
- entityId,
- it.first,
- attributeName,
- datasetId,
- it.second,
- emptyList()
- )
+ publishEntityEvent(
+ AttributeDeleteEvent(
+ sub,
+ tenantName,
+ entityId,
+ it.first,
+ attributeName,
+ attributeOperationResult.datasetId,
+ injectDeletedAttribute(it.second, attributeName, attributeOperationResult.newExpandedValue),
+ emptyList()
)
+ )
}.logAttributeEvent("Attribute Delete", entityId, tenantName)
}
}
@@ -264,21 +248,19 @@ class EntityEventService(
Pair(it.types, it.payload.asString())
}
- private fun getSerializedAttribute(
- jsonLdAttributes: Map,
+ internal fun injectDeletedAttribute(
+ entityPayload: String,
attributeName: ExpandedTerm,
- datasetId: URI?
- ): Pair =
- if (attributeName == JSONLD_TYPE) {
- Pair(JSONLD_TYPE, serializeObject(jsonLdAttributes[JSONLD_TYPE]!!))
- } else {
- val extractedPayload = (jsonLdAttributes as ExpandedAttributes).getAttributeFromExpandedAttributes(
- attributeName,
- datasetId
- )!!
- Pair(attributeName, serializeObject(extractedPayload))
+ attributeInstance: ExpandedAttributeInstance
+ ): String {
+ val entityPayload = entityPayload.deserializeAsMap().toMutableMap()
+ entityPayload.merge(attributeName, listOf(attributeInstance)) { currentValue, newValue ->
+ (currentValue as List).plus(newValue as List)
}
+ return serializeObject(entityPayload)
+ }
+
private fun Either.logEntityEvent(eventsType: EventsType, entityId: URI, tenantName: String) =
this.fold({
logger.error("Error sending {} event for entity {} in tenant {}: {}", eventsType, entityId, tenantName, it)
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityQueryService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityQueryService.kt
index d97eaadae..782b433df 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityQueryService.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityQueryService.kt
@@ -38,11 +38,10 @@ class EntityQueryService(
entityId: URI,
sub: Sub? = null
): Either = either {
- checkEntityExistence(entityId).bind()
+ val entity = retrieve(entityId).bind()
authorizationService.userCanReadEntity(entityId, sub.toOption()).bind()
- val entityPayload = retrieve(entityId).bind()
- toJsonLdEntity(entityPayload)
+ toJsonLdEntity(entity)
}
suspend fun queryEntities(
@@ -228,15 +227,16 @@ class EntityQueryService(
return entitySelectorFilter?.joinToString(separator = " OR ") ?: " 1 = 1 "
}
- suspend fun retrieve(entityId: URI): Either =
+ suspend fun retrieve(entityId: URI, allowDeleted: Boolean = false): Either =
databaseClient.sql(
"""
SELECT * from entity_payload
WHERE entity_id = :entity_id
+ ${if (!allowDeleted) " and deleted_at is null " else ""}
""".trimIndent()
)
.bind("entity_id", entityId)
- .oneToResult { it.rowToEntity() }
+ .oneToResult(ResourceNotFoundException(entityNotFoundMessage(entityId.toString()))) { it.rowToEntity() }
suspend fun retrieve(entitiesIds: List): List =
databaseClient.sql(
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityService.kt
index cac3a7d27..0f5acd87b 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityService.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityService.kt
@@ -13,17 +13,19 @@ import com.egm.stellio.search.common.util.execute
import com.egm.stellio.search.common.util.oneToResult
import com.egm.stellio.search.common.util.toZonedDateTime
import com.egm.stellio.search.entity.model.Attribute
-import com.egm.stellio.search.entity.model.EMPTY_UPDATE_RESULT
+import com.egm.stellio.search.entity.model.AttributeOperationResult
import com.egm.stellio.search.entity.model.Entity
+import com.egm.stellio.search.entity.model.FailedAttributeOperationResult
+import com.egm.stellio.search.entity.model.OperationStatus
import com.egm.stellio.search.entity.model.OperationType
import com.egm.stellio.search.entity.model.OperationType.APPEND_ATTRIBUTES
import com.egm.stellio.search.entity.model.OperationType.APPEND_ATTRIBUTES_OVERWRITE_ALLOWED
import com.egm.stellio.search.entity.model.OperationType.MERGE_ENTITY
import com.egm.stellio.search.entity.model.OperationType.UPDATE_ATTRIBUTES
-import com.egm.stellio.search.entity.model.UpdateAttributeResult
-import com.egm.stellio.search.entity.model.UpdateOperationResult
+import com.egm.stellio.search.entity.model.SucceededAttributeOperationResult
import com.egm.stellio.search.entity.model.UpdateResult
-import com.egm.stellio.search.entity.model.updateResultFromDetailedResult
+import com.egm.stellio.search.entity.model.getSucceededOperations
+import com.egm.stellio.search.entity.model.hasSuccessfulResult
import com.egm.stellio.search.entity.util.prepareAttributes
import com.egm.stellio.search.entity.util.rowToEntity
import com.egm.stellio.search.scope.ScopeService
@@ -37,7 +39,6 @@ import com.egm.stellio.shared.model.ExpandedEntity
import com.egm.stellio.shared.model.ExpandedTerm
import com.egm.stellio.shared.model.NgsiLdEntity
import com.egm.stellio.shared.model.addSysAttrs
-import com.egm.stellio.shared.model.toExpandedAttributes
import com.egm.stellio.shared.model.toNgsiLdAttributes
import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_EXPANDED_ENTITY_SPECIFIC_MEMBERS
import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_ID
@@ -154,8 +155,8 @@ class EntityService(
val mergedAt = ngsiLdDateTime()
logger.debug("Merging entity {}", entityId)
- val coreUpdateResult = updateCoreAttributes(entityId, coreAttrs, mergedAt, MERGE_ENTITY).bind()
- val attrsUpdateResult = entityAttributeService.mergeAttributes(
+ val coreOperationResult = updateCoreAttributes(entityId, coreAttrs, mergedAt, MERGE_ENTITY).bind()
+ val attrsOperationResult = entityAttributeService.mergeAttributes(
entityId,
otherAttrs.toMap().toNgsiLdAttributes().bind(),
expandedAttributes,
@@ -164,24 +165,20 @@ class EntityService(
sub
).bind()
- val updateResult = coreUpdateResult.mergeWith(attrsUpdateResult)
+ val operationResult = coreOperationResult.plus(attrsOperationResult)
// update modifiedAt in entity if at least one attribute has been merged
- if (updateResult.hasSuccessfulUpdate()) {
+ if (operationResult.hasSuccessfulResult()) {
val attributes = entityAttributeService.getForEntity(entityId, emptySet(), emptySet())
updateState(entityId, mergedAt, attributes).bind()
- }
- if (updateResult.updated.isNotEmpty()) {
entityEventService.publishAttributeChangeEvents(
sub,
entityId,
- expandedAttributes,
- updateResult,
- true
+ operationResult.getSucceededOperations()
)
}
- updateResult
+ UpdateResult(operationResult)
}
@Transactional
@@ -264,7 +261,7 @@ class EntityService(
coreAttrs: List>,
modifiedAt: ZonedDateTime,
operationType: OperationType
- ): Either = either {
+ ): Either> = either {
coreAttrs.map { (expandedTerm, expandedAttributeInstances) ->
when (expandedTerm) {
JSONLD_TYPE ->
@@ -273,11 +270,14 @@ class EntityService(
scopeService.update(entityId, expandedAttributeInstances, modifiedAt, operationType).bind()
else -> {
logger.warn("Ignoring unhandled core property: {}", expandedTerm)
- EMPTY_UPDATE_RESULT.right().bind()
+ FailedAttributeOperationResult(
+ attributeName = expandedTerm,
+ operationStatus = OperationStatus.IGNORED,
+ errorMessage = "Ignoring unhandled core property: $expandedTerm"
+ ).right().bind()
}
}
- }.ifEmpty { listOf(EMPTY_UPDATE_RESULT) }
- .reduce { acc, cur -> acc.mergeWith(cur) }
+ }
}
@Transactional
@@ -286,12 +286,16 @@ class EntityService(
newTypes: List,
modifiedAt: ZonedDateTime,
allowEmptyListOfTypes: Boolean = true
- ): Either = either {
+ ): Either = either {
val entityPayload = entityQueryService.retrieve(entityId).bind()
val currentTypes = entityPayload.types
// when dealing with an entity update, list of types can be empty if no change of type is requested
if (currentTypes.sorted() == newTypes.sorted() || newTypes.isEmpty() && allowEmptyListOfTypes)
- return@either UpdateResult(emptyList(), emptyList())
+ return@either SucceededAttributeOperationResult(
+ attributeName = JSONLD_TYPE,
+ operationStatus = OperationStatus.APPENDED,
+ newExpandedValue = mapOf(JSONLD_TYPE to currentTypes.toList())
+ )
val updatedTypes = currentTypes.union(newTypes)
val updatedPayload = entityPayload.payload.deserializeExpandedPayload()
@@ -316,13 +320,10 @@ class EntityService(
.bind("payload", Json.of(serializeObject(updatedPayload)))
.execute()
.map {
- updateResultFromDetailedResult(
- listOf(
- UpdateAttributeResult(
- attributeName = JSONLD_TYPE,
- updateOperationResult = UpdateOperationResult.APPENDED
- )
- )
+ SucceededAttributeOperationResult(
+ attributeName = JSONLD_TYPE,
+ operationStatus = OperationStatus.APPENDED,
+ newExpandedValue = mapOf(JSONLD_TYPE to updatedTypes.toList())
)
}.bind()
}
@@ -344,8 +345,8 @@ class EntityService(
val operationType =
if (disallowOverwrite) APPEND_ATTRIBUTES
else APPEND_ATTRIBUTES_OVERWRITE_ALLOWED
- val coreUpdateResult = updateCoreAttributes(entityId, coreAttrs, createdAt, operationType).bind()
- val attrsUpdateResult = entityAttributeService.appendAttributes(
+ val coreOperationResult = updateCoreAttributes(entityId, coreAttrs, createdAt, operationType).bind()
+ val attrsOperationResult = entityAttributeService.appendAttributes(
entityId,
otherAttrs.toMap().toNgsiLdAttributes().bind(),
expandedAttributes,
@@ -354,24 +355,10 @@ class EntityService(
sub
).bind()
- val updateResult = coreUpdateResult.mergeWith(attrsUpdateResult)
- // update modifiedAt in entity if at least one attribute has been added
- if (updateResult.hasSuccessfulUpdate()) {
- val attributes = entityAttributeService.getForEntity(entityId, emptySet(), emptySet())
- updateState(entityId, createdAt, attributes).bind()
- }
-
- if (updateResult.hasSuccessfulUpdate()) {
- entityEventService.publishAttributeChangeEvents(
- sub,
- entityId,
- expandedAttributes,
- updateResult,
- true
- )
- }
+ val operationResult = coreOperationResult.plus(attrsOperationResult)
+ handleSuccessOperationActions(operationResult, entityId, createdAt, sub).bind()
- updateResult
+ UpdateResult(operationResult)
}
@Transactional
@@ -387,8 +374,8 @@ class EntityService(
expandedAttributes.toList().partition { JSONLD_EXPANDED_ENTITY_SPECIFIC_MEMBERS.contains(it.first) }
val createdAt = ngsiLdDateTime()
- val coreUpdateResult = updateCoreAttributes(entityId, coreAttrs, createdAt, UPDATE_ATTRIBUTES).bind()
- val attrsUpdateResult = entityAttributeService.updateAttributes(
+ val coreOperationResult = updateCoreAttributes(entityId, coreAttrs, createdAt, UPDATE_ATTRIBUTES).bind()
+ val attrsOperationResult = entityAttributeService.updateAttributes(
entityId,
otherAttrs.toMap().toNgsiLdAttributes().bind(),
expandedAttributes,
@@ -396,24 +383,10 @@ class EntityService(
sub
).bind()
- val updateResult = coreUpdateResult.mergeWith(attrsUpdateResult)
- // update modifiedAt in entity if at least one attribute has been added
- if (updateResult.hasSuccessfulUpdate()) {
- val attributes = entityAttributeService.getForEntity(entityId, emptySet(), emptySet())
- updateState(entityId, createdAt, attributes).bind()
- }
+ val operationResult = coreOperationResult.plus(attrsOperationResult)
+ handleSuccessOperationActions(operationResult, entityId, createdAt, sub).bind()
- if (updateResult.updated.isNotEmpty()) {
- entityEventService.publishAttributeChangeEvents(
- sub,
- entityId,
- expandedAttributes,
- updateResult,
- true
- )
- }
-
- updateResult
+ UpdateResult(operationResult)
}
@Transactional
@@ -427,28 +400,16 @@ class EntityService(
val modifiedAt = ngsiLdDateTime()
- val updateResult = entityAttributeService.partialUpdateAttribute(
+ val operationResult = entityAttributeService.partialUpdateAttribute(
entityId,
expandedAttribute,
modifiedAt,
sub
- ).bind()
+ ).bind().let { listOf(it) }
- if (updateResult.isSuccessful()) {
- val attributes = entityAttributeService.getForEntity(entityId, emptySet(), emptySet())
- updateState(entityId, modifiedAt, attributes).bind()
- }
+ handleSuccessOperationActions(operationResult, entityId, modifiedAt, sub).bind()
- if (updateResult.updated.isNotEmpty())
- entityEventService.publishAttributeChangeEvents(
- sub,
- entityId,
- expandedAttribute.toExpandedAttributes(),
- updateResult,
- false
- )
-
- updateResult
+ UpdateResult(operationResult)
}
@Transactional
@@ -491,30 +452,37 @@ class EntityService(
val ngsiLdAttribute = listOf(expandedAttribute).toMap().toNgsiLdAttributes().bind()[0]
val replacedAt = ngsiLdDateTime()
- val updateResult = entityAttributeService.replaceAttribute(
+ val operationResult = entityAttributeService.replaceAttribute(
entityId,
ngsiLdAttribute,
expandedAttribute,
replacedAt,
sub
- ).bind()
+ ).bind().let { listOf(it) }
+
+ handleSuccessOperationActions(operationResult, entityId, replacedAt, sub).bind()
+ UpdateResult(operationResult)
+ }
+
+ @Transactional
+ internal suspend fun handleSuccessOperationActions(
+ operationResult: List,
+ entityId: URI,
+ createdAt: ZonedDateTime,
+ sub: Sub? = null
+ ): Either = either {
// update modifiedAt in entity if at least one attribute has been added
- if (updateResult.hasSuccessfulUpdate()) {
+ if (operationResult.hasSuccessfulResult()) {
val attributes = entityAttributeService.getForEntity(entityId, emptySet(), emptySet())
- updateState(entityId, replacedAt, attributes).bind()
- }
+ updateState(entityId, createdAt, attributes).bind()
- if (updateResult.updated.isNotEmpty())
entityEventService.publishAttributeChangeEvents(
sub,
entityId,
- expandedAttribute.toExpandedAttributes(),
- updateResult,
- false
+ operationResult.getSucceededOperations()
)
-
- updateResult
+ }
}
@Transactional
@@ -564,57 +532,71 @@ class EntityService(
@Transactional
suspend fun deleteEntity(entityId: URI, sub: Sub? = null): Either = either {
- entityQueryService.checkEntityExistence(entityId).bind()
+ val currentEntity = entityQueryService.retrieve(entityId).bind()
authorizationService.userCanAdminEntity(entityId, sub.toOption()).bind()
val deletedAt = ngsiLdDateTime()
- val entity = deleteEntityPayload(entityId, deletedAt).bind()
+ val deletedEntityPayload = currentEntity.toExpandedDeletedEntity(deletedAt)
+ val previousEntity = deleteEntityPayload(entityId, deletedAt, deletedEntityPayload).bind()
entityAttributeService.deleteAttributes(entityId, deletedAt).bind()
scopeService.addHistoryEntry(entityId, emptyList(), TemporalProperty.DELETED_AT, deletedAt, sub).bind()
- entityEventService.publishEntityDeleteEvent(sub, entity)
+ entityEventService.publishEntityDeleteEvent(sub, previousEntity, deletedEntityPayload)
}
@Transactional
- suspend fun deleteEntityPayload(entityId: URI, deletedAt: ZonedDateTime): Either = either {
- val expandedDeletedEntity = Entity.toExpandedDeletedEntity(entityId, deletedAt)
- val entity = databaseClient.sql(
+ suspend fun deleteEntityPayload(
+ entityId: URI,
+ deletedAt: ZonedDateTime,
+ deletedEntityPayload: ExpandedEntity
+ ): Either = either {
+ databaseClient.sql(
"""
- UPDATE entity_payload
- SET deleted_at = :deleted_at,
- payload = :payload,
- scopes = null,
- specific_access_policy = null,
- types = '{}'
- WHERE entity_id = :entity_id
- RETURNING *
+ WITH entity_before_delete AS (
+ SELECT *
+ FROM entity_payload
+ WHERE entity_id = :entity_id
+ ),
+ update_entity AS (
+ UPDATE entity_payload
+ SET deleted_at = :deleted_at,
+ payload = :payload,
+ scopes = null,
+ specific_access_policy = null,
+ types = '{}'
+ WHERE entity_id = :entity_id
+ )
+ SELECT * FROM entity_before_delete
""".trimIndent()
)
.bind("entity_id", entityId)
.bind("deleted_at", deletedAt)
- .bind("payload", Json.of(serializeObject(expandedDeletedEntity.members)))
+ .bind("payload", Json.of(serializeObject(deletedEntityPayload.members)))
.oneToResult {
it.rowToEntity()
}
.bind()
- entity
}
@Transactional
suspend fun permanentlyDeleteEntity(entityId: URI, sub: Sub? = null): Either = either {
- entityQueryService.checkEntityExistence(entityId, true).bind()
+ val currentEntity = entityQueryService.retrieve(entityId, true).bind()
authorizationService.userCanAdminEntity(entityId, sub.toOption()).bind()
- val entity = permanentyDeleteEntityPayload(entityId).bind()
+ val previousEntity = permanentyDeleteEntityPayload(entityId).bind()
entityAttributeService.permanentlyDeleteAttributes(entityId).bind()
authorizationService.removeRightsOnEntity(entityId).bind()
- entityEventService.publishEntityDeleteEvent(sub, entity)
+ if (currentEntity.deletedAt == null) {
+ // only send a notification if entity was not already previously deleted
+ val deletedEntityPayload = currentEntity.toExpandedDeletedEntity(ngsiLdDateTime())
+ entityEventService.publishEntityDeleteEvent(sub, previousEntity, deletedEntityPayload)
+ }
}
@Transactional
suspend fun permanentyDeleteEntityPayload(entityId: URI): Either = either {
- val entity = databaseClient.sql(
+ databaseClient.sql(
"""
DELETE FROM entity_payload
WHERE entity_id = :entity_id
@@ -626,7 +608,6 @@ class EntityService(
it.rowToEntity()
}
.bind()
- entity
}
@Transactional
@@ -639,7 +620,7 @@ class EntityService(
): Either = either {
authorizationService.userCanUpdateEntity(entityId, sub.toOption()).bind()
- if (attributeName == NGSILD_SCOPE_PROPERTY) {
+ val deleteAttributeResults = if (attributeName == NGSILD_SCOPE_PROPERTY) {
scopeService.delete(entityId).bind()
} else {
entityAttributeService.checkEntityAndAttributeExistence(
@@ -662,13 +643,10 @@ class EntityService(
entityAttributeService.getForEntity(entityId, emptySet(), emptySet())
).bind()
- entityEventService.publishAttributeDeleteEvent(
- sub,
- entityId,
- attributeName,
- datasetId,
- deleteAll
- )
+ deleteAttributeResults.filterIsInstance()
+ .forEach {
+ entityEventService.publishAttributeDeleteEvent(sub, entityId, it)
+ }
}
@Transactional
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/util/EntityUtils.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/util/EntityUtils.kt
index 09af4abf5..7e5c40b05 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/util/EntityUtils.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/util/EntityUtils.kt
@@ -17,6 +17,7 @@ fun Map.rowToEntity(): Entity =
scopes = toOptionalList(this["scopes"]),
createdAt = toZonedDateTime(this["created_at"]),
modifiedAt = toOptionalZonedDateTime(this["modified_at"]),
+ deletedAt = toOptionalZonedDateTime(this["deleted_at"]),
payload = toJson(this["payload"]),
specificAccessPolicy = toOptionalEnum(this["specific_access_policy"])
)
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/scope/ScopeService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/scope/ScopeService.kt
index 8cb37778b..9eacfef87 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/scope/ScopeService.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/scope/ScopeService.kt
@@ -16,12 +16,11 @@ import com.egm.stellio.search.common.util.toOptionalZonedDateTime
import com.egm.stellio.search.common.util.toUri
import com.egm.stellio.search.common.util.toZonedDateTime
import com.egm.stellio.search.entity.model.Attribute.AttributeValueType
-import com.egm.stellio.search.entity.model.NotUpdatedDetails
+import com.egm.stellio.search.entity.model.AttributeOperationResult
+import com.egm.stellio.search.entity.model.FailedAttributeOperationResult
+import com.egm.stellio.search.entity.model.OperationStatus
import com.egm.stellio.search.entity.model.OperationType
-import com.egm.stellio.search.entity.model.UpdateAttributeResult
-import com.egm.stellio.search.entity.model.UpdateOperationResult
-import com.egm.stellio.search.entity.model.UpdateResult
-import com.egm.stellio.search.entity.model.updateResultFromDetailedResult
+import com.egm.stellio.search.entity.model.SucceededAttributeOperationResult
import com.egm.stellio.search.temporal.model.AttributeInstance.TemporalProperty
import com.egm.stellio.search.temporal.model.TemporalEntitiesQuery
import com.egm.stellio.search.temporal.model.TemporalQuery
@@ -255,7 +254,7 @@ class ScopeService(
modifiedAt: ZonedDateTime,
operationType: OperationType,
sub: Sub? = null
- ): Either = either {
+ ): Either = either {
val scopes = mapOf(NGSILD_SCOPE_PROPERTY to expandedAttributeInstances).getScopes()!!
val (currentScopes, currentPayload) = retrieve(entityId).bind()
@@ -264,14 +263,10 @@ class ScopeService(
if (currentScopes != null) {
val updatedPayload = currentPayload.replaceScopeValue(expandedAttributeInstances)
Pair(scopes, updatedPayload)
- } else return@either UpdateResult(
- updated = emptyList(),
- notUpdated = listOf(
- NotUpdatedDetails(
- NGSILD_SCOPE_PROPERTY,
- "Attribute does not exist and operation does not allow creating it"
- )
- )
+ } else return@either FailedAttributeOperationResult(
+ attributeName = NGSILD_SCOPE_PROPERTY,
+ operationStatus = OperationStatus.FAILED,
+ errorMessage = "Scope does not exist and operation does not allow creating it"
)
}
OperationType.APPEND_ATTRIBUTES, OperationType.MERGE_ENTITY -> {
@@ -291,7 +286,7 @@ class ScopeService(
}
updatedScopes?.let {
- val updateResult =
+ val operationResult =
performUpdate(entityId, updatedScopes, modifiedAt, serializeObject(updatedPayload)).bind()
val temporalPropertyToAdd =
if (currentScopes == null) TemporalProperty.CREATED_AT
@@ -302,10 +297,11 @@ class ScopeService(
// change from the Core API, the observedAt sub-Property should be set as a copy of the modifiedAt
// sub-Property
addHistoryEntry(entityId, it, TemporalProperty.OBSERVED_AT, modifiedAt, sub).bind()
- updateResult
- } ?: UpdateResult(
- emptyList(),
- listOf(NotUpdatedDetails(NGSILD_SCOPE_PROPERTY, "Unrecognized operation type: $operationType"))
+ operationResult
+ } ?: FailedAttributeOperationResult(
+ attributeName = NGSILD_SCOPE_PROPERTY,
+ operationStatus = OperationStatus.FAILED,
+ errorMessage = "Unrecognized operation type on scope: $operationType"
)
}
@@ -315,7 +311,7 @@ class ScopeService(
scopes: List,
modifiedAt: ZonedDateTime,
payload: String
- ): Either = either {
+ ): Either = either {
databaseClient.sql(
"""
UPDATE entity_payload
@@ -331,13 +327,10 @@ class ScopeService(
.bind("payload", Json.of(payload))
.execute()
.map {
- updateResultFromDetailedResult(
- listOf(
- UpdateAttributeResult(
- attributeName = NGSILD_SCOPE_PROPERTY,
- updateOperationResult = UpdateOperationResult.APPENDED
- )
- )
+ SucceededAttributeOperationResult(
+ attributeName = NGSILD_SCOPE_PROPERTY,
+ operationStatus = OperationStatus.APPENDED,
+ newExpandedValue = mapOf(NGSILD_SCOPE_PROPERTY to scopes.toList())
)
}.bind()
}
@@ -348,12 +341,11 @@ class ScopeService(
createdAt: ZonedDateTime,
sub: Sub? = null
): Either = either {
- delete(ngsiLdEntity.id).bind()
createHistory(ngsiLdEntity, createdAt, sub).bind()
}
@Transactional
- suspend fun delete(entityId: URI): Either = either {
+ suspend fun delete(entityId: URI): Either> = either {
databaseClient.sql(
"""
UPDATE entity_payload
@@ -372,6 +364,15 @@ class ScopeService(
TemporalProperty.DELETED_AT,
ngsiLdDateTime(),
getSubFromSecurityContext().getOrNull()
+ ).bind()
+
+ listOf(
+ SucceededAttributeOperationResult(
+ NGSILD_SCOPE_PROPERTY,
+ null,
+ OperationStatus.DELETED,
+ mapOf(NGSILD_SCOPE_PROPERTY to listOf())
+ )
)
}
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/TemporalQueryService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/TemporalQueryService.kt
index 52cf57f83..8f0e742ef 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/TemporalQueryService.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/TemporalQueryService.kt
@@ -43,7 +43,7 @@ class TemporalQueryService(
temporalEntitiesQuery: TemporalEntitiesQuery,
sub: Sub? = null
): Either> = either {
- entityQueryService.checkEntityExistence(entityId).bind()
+ val entity = entityQueryService.retrieve(entityId).bind()
authorizationService.userCanReadEntity(entityId, sub.toOption()).bind()
val attrs = temporalEntitiesQuery.entitiesQuery.attrs
@@ -56,7 +56,6 @@ class TemporalQueryService(
else it.right()
}.bind()
- val entityPayload = entityQueryService.retrieve(entityId).bind()
val origin = calculateOldestTimestamp(entityId, temporalEntitiesQuery, attributes)
val scopeHistory =
@@ -76,7 +75,7 @@ class TemporalQueryService(
fillWithAttributesWithEmptyInstances(attributes, paginatedAttributesWithInstances)
TemporalEntityBuilder.buildTemporalEntity(
- EntityTemporalResult(entityPayload, scopeHistory, attributesWithInstances),
+ EntityTemporalResult(entity, scopeHistory, attributesWithInstances),
temporalEntitiesQuery
) to range
}
diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/entity/listener/ObservationEventListenerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/entity/listener/ObservationEventListenerTests.kt
index a464ec6c9..f9b6a6bc3 100644
--- a/search-service/src/test/kotlin/com/egm/stellio/search/entity/listener/ObservationEventListenerTests.kt
+++ b/search-service/src/test/kotlin/com/egm/stellio/search/entity/listener/ObservationEventListenerTests.kt
@@ -2,26 +2,23 @@ package com.egm.stellio.search.entity.listener
import arrow.core.right
import com.egm.stellio.search.entity.model.NotUpdatedDetails
-import com.egm.stellio.search.entity.model.UpdateOperationResult
+import com.egm.stellio.search.entity.model.OperationStatus
import com.egm.stellio.search.entity.model.UpdateResult
-import com.egm.stellio.search.entity.model.UpdatedDetails
import com.egm.stellio.search.entity.service.EntityEventService
import com.egm.stellio.search.entity.service.EntityService
-import com.egm.stellio.shared.model.ExpandedEntity
-import com.egm.stellio.shared.model.NgsiLdEntity
import com.egm.stellio.shared.util.BEEHIVE_TYPE
import com.egm.stellio.shared.util.TEMPERATURE_PROPERTY
import com.egm.stellio.shared.util.loadSampleData
import com.egm.stellio.shared.util.toUri
import com.ninjasquad.springmockk.MockkBean
import io.mockk.called
+import io.mockk.clearAllMocks
import io.mockk.coEvery
import io.mockk.coVerify
-import io.mockk.every
-import io.mockk.mockkClass
import io.mockk.verify
import kotlinx.coroutines.Job
import kotlinx.coroutines.test.runTest
+import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertDoesNotThrow
import org.springframework.beans.factory.annotation.Autowired
@@ -35,21 +32,26 @@ class ObservationEventListenerTests {
@Autowired
private lateinit var observationEventListener: ObservationEventListener
- @MockkBean(relaxed = true)
+ @MockkBean
private lateinit var entityService: EntityService
- @MockkBean(relaxed = true)
+ @MockkBean
private lateinit var entityEventService: EntityEventService
private val expectedEntityId = "urn:ngsi-ld:BeeHive:01".toUri()
private val expectedTemperatureDatasetId = "urn:ngsi-ld:Dataset:WeatherApi".toUri()
+ @BeforeEach
+ fun clearMocks() {
+ clearAllMocks()
+ }
+
@Test
fun `it should parse and transmit an ENTITY_CREATE event`() = runTest {
val observationEvent = loadSampleData("events/entity/entityCreateEvent.json")
coEvery {
- entityService.createEntity(any(), any(), any())
+ entityService.createEntity(any(), any(), any())
} returns Unit.right()
coEvery { entityEventService.publishEntityCreateEvent(any(), any(), any()) } returns Job()
@@ -57,7 +59,7 @@ class ObservationEventListenerTests {
coVerify {
entityService.createEntity(
- any(),
+ any(),
any(),
eq("0123456789-1234-5678-987654321")
)
@@ -79,23 +81,17 @@ class ObservationEventListenerTests {
coEvery {
entityService.partialUpdateAttribute(any(), any(), any())
} returns UpdateResult(
- updated = arrayListOf(
- UpdatedDetails(
- TEMPERATURE_PROPERTY,
- expectedTemperatureDatasetId,
- UpdateOperationResult.UPDATED
- )
- ),
- notUpdated = arrayListOf()
+ updated = listOf(TEMPERATURE_PROPERTY),
+ notUpdated = emptyList()
).right()
coEvery {
- entityEventService.publishAttributeChangeEvents(any(), any(), any(), any(), any())
+ entityEventService.publishAttributeChangeEvents(any(), any(), any())
} returns Job()
observationEventListener.dispatchObservationMessage(observationEvent)
- coVerify {
+ coVerify(timeout = 1000L) {
entityService.partialUpdateAttribute(
expectedEntityId,
match { it.first == TEMPERATURE_PROPERTY },
@@ -106,14 +102,12 @@ class ObservationEventListenerTests {
entityEventService.publishAttributeChangeEvents(
null,
eq(expectedEntityId),
- match { it.containsKey(TEMPERATURE_PROPERTY) },
match {
- it.updated.size == 1 &&
- it.updated[0].attributeName == TEMPERATURE_PROPERTY &&
- it.updated[0].datasetId == expectedTemperatureDatasetId &&
- it.updated[0].updateOperationResult == UpdateOperationResult.UPDATED
- },
- eq(false)
+ it.size == 1 &&
+ it[0].attributeName == TEMPERATURE_PROPERTY &&
+ it[0].datasetId == expectedTemperatureDatasetId &&
+ it[0].operationStatus == OperationStatus.UPDATED
+ }
)
}
}
@@ -131,6 +125,9 @@ class ObservationEventListenerTests {
observationEventListener.dispatchObservationMessage(observationEvent)
+ coVerify {
+ entityService.partialUpdateAttribute(any(), any(), any())
+ }
verify { entityEventService wasNot called }
}
@@ -141,19 +138,11 @@ class ObservationEventListenerTests {
coEvery {
entityService.appendAttributes(any(), any(), any(), any())
} returns UpdateResult(
- listOf(
- UpdatedDetails(
- TEMPERATURE_PROPERTY,
- expectedTemperatureDatasetId,
- UpdateOperationResult.APPENDED
- )
- ),
+ listOf(TEMPERATURE_PROPERTY),
emptyList()
).right()
- val mockedExpandedEntity = mockkClass(ExpandedEntity::class, relaxed = true)
- every { mockedExpandedEntity.types } returns listOf(BEEHIVE_TYPE)
coEvery {
- entityEventService.publishAttributeChangeEvents(any(), any(), any(), any(), any())
+ entityEventService.publishAttributeChangeEvents(any(), any(), any())
} returns Job()
observationEventListener.dispatchObservationMessage(observationEvent)
@@ -171,15 +160,11 @@ class ObservationEventListenerTests {
null,
eq(expectedEntityId),
match {
- it.containsKey(TEMPERATURE_PROPERTY)
- },
- match {
- it.updated.size == 1 &&
- it.updated[0].updateOperationResult == UpdateOperationResult.APPENDED &&
- it.updated[0].attributeName == TEMPERATURE_PROPERTY &&
- it.updated[0].datasetId == expectedTemperatureDatasetId
- },
- eq(true)
+ it.size == 1 &&
+ it[0].operationStatus == OperationStatus.APPENDED &&
+ it[0].attributeName == TEMPERATURE_PROPERTY &&
+ it[0].datasetId == expectedTemperatureDatasetId
+ }
)
}
}
diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/entity/model/UpdateResultTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/entity/model/UpdateResultTests.kt
index d46951752..da0933c18 100644
--- a/search-service/src/test/kotlin/com/egm/stellio/search/entity/model/UpdateResultTests.kt
+++ b/search-service/src/test/kotlin/com/egm/stellio/search/entity/model/UpdateResultTests.kt
@@ -1,6 +1,5 @@
package com.egm.stellio.search.entity.model
-import com.egm.stellio.shared.util.toUri
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
@@ -9,15 +8,15 @@ class UpdateResultTests {
@Test
fun `it should find the successful update operation results`() {
- assertTrue(UpdateOperationResult.UPDATED.isSuccessResult())
- assertTrue(UpdateOperationResult.APPENDED.isSuccessResult())
- assertTrue(UpdateOperationResult.REPLACED.isSuccessResult())
+ assertTrue(OperationStatus.UPDATED.isSuccessResult())
+ assertTrue(OperationStatus.APPENDED.isSuccessResult())
+ assertTrue(OperationStatus.REPLACED.isSuccessResult())
+ assertTrue(OperationStatus.IGNORED.isSuccessResult())
}
@Test
fun `it should find the failed update operation results`() {
- assertFalse(UpdateOperationResult.FAILED.isSuccessResult())
- assertFalse(UpdateOperationResult.IGNORED.isSuccessResult())
+ assertFalse(OperationStatus.FAILED.isSuccessResult())
}
@Test
@@ -25,9 +24,7 @@ class UpdateResultTests {
val updateResult =
UpdateResult(
notUpdated = emptyList(),
- updated = listOf(
- UpdatedDetails("attributeName", "urn:ngsi-ld:Entity:01".toUri(), UpdateOperationResult.UPDATED)
- )
+ updated = listOf("attributeName")
)
assertTrue(updateResult.isSuccessful())
@@ -37,12 +34,8 @@ class UpdateResultTests {
fun `it should find a failed update result if there is one not updated attribute`() {
val updateResult =
UpdateResult(
- notUpdated = listOf(
- NotUpdatedDetails("attributeName", "attribute is malformed")
- ),
- updated = listOf(
- UpdatedDetails("attributeName", "urn:ngsi-ld:Entity:01".toUri(), UpdateOperationResult.UPDATED)
- )
+ notUpdated = listOf(NotUpdatedDetails("attributeName", "attribute is malformed")),
+ updated = listOf("attributeName")
)
assertFalse(updateResult.isSuccessful())
@@ -52,11 +45,10 @@ class UpdateResultTests {
fun `it should find a failed update result if an attribute update has failed`() {
val updateResult =
UpdateResult(
- notUpdated = emptyList(),
- updated = listOf(
- UpdatedDetails("attributeName", "urn:ngsi-ld:Entity:01".toUri(), UpdateOperationResult.UPDATED),
- UpdatedDetails("attributeName", "urn:ngsi-ld:Entity:01".toUri(), UpdateOperationResult.FAILED)
- )
+ notUpdated = listOf(
+ NotUpdatedDetails("failedAttributeName", "attribute does not exist")
+ ),
+ updated = listOf("succeededAttributeName")
)
assertFalse(updateResult.isSuccessful())
diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityAttributeServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityAttributeServiceTests.kt
index 654d4a8b2..75ca618e1 100644
--- a/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityAttributeServiceTests.kt
+++ b/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityAttributeServiceTests.kt
@@ -4,7 +4,9 @@ import arrow.core.right
import com.egm.stellio.search.entity.model.Attribute
import com.egm.stellio.search.entity.model.AttributeMetadata
import com.egm.stellio.search.entity.model.Entity
-import com.egm.stellio.search.entity.model.UpdateOperationResult
+import com.egm.stellio.search.entity.model.FailedAttributeOperationResult
+import com.egm.stellio.search.entity.model.OperationStatus
+import com.egm.stellio.search.entity.model.getSucceededOperations
import com.egm.stellio.search.support.EMPTY_JSON_PAYLOAD
import com.egm.stellio.search.support.WithKafkaContainer
import com.egm.stellio.search.support.WithTimescaleContainer
@@ -392,12 +394,12 @@ class EntityAttributeServiceTests : WithTimescaleContainer, WithKafkaContainer()
createdAt,
null,
null
- ).shouldSucceedWith { updateResult ->
- val updatedDetails = updateResult.updated
- assertEquals(6, updatedDetails.size)
- assertEquals(4, updatedDetails.filter { it.updateOperationResult == UpdateOperationResult.UPDATED }.size)
- assertEquals(2, updatedDetails.filter { it.updateOperationResult == UpdateOperationResult.APPENDED }.size)
- val newAttributes = updatedDetails.filter { it.updateOperationResult == UpdateOperationResult.APPENDED }
+ ).shouldSucceedWith { operationResults ->
+ val successfulOperations = operationResults.getSucceededOperations()
+ assertEquals(6, successfulOperations.size)
+ assertEquals(4, successfulOperations.filter { it.operationStatus == OperationStatus.UPDATED }.size)
+ assertEquals(2, successfulOperations.filter { it.operationStatus == OperationStatus.APPENDED }.size)
+ val newAttributes = successfulOperations.filter { it.operationStatus == OperationStatus.APPENDED }
.map { it.attributeName }
assertTrue(newAttributes.containsAll(listOf(OUTGOING_PROPERTY, TEMPERATURE_PROPERTY)))
}
@@ -459,10 +461,10 @@ class EntityAttributeServiceTests : WithTimescaleContainer, WithKafkaContainer()
createdAt,
observedAt,
null
- ).shouldSucceedWith { updateResult ->
- val updatedDetails = updateResult.updated
- assertEquals(1, updatedDetails.size)
- assertEquals(1, updatedDetails.filter { it.updateOperationResult == UpdateOperationResult.UPDATED }.size)
+ ).shouldSucceedWith { operationResults ->
+ val successfulOperations = operationResults.getSucceededOperations()
+ assertEquals(1, successfulOperations.size)
+ assertEquals(1, successfulOperations.filter { it.operationStatus == OperationStatus.UPDATED }.size)
}
coVerify(exactly = 1) {
@@ -499,10 +501,10 @@ class EntityAttributeServiceTests : WithTimescaleContainer, WithKafkaContainer()
createdAt,
null,
null
- ).shouldSucceedWith { updateResult ->
- val updatedDetails = updateResult.updated
- assertEquals(1, updatedDetails.size)
- assertEquals(1, updatedDetails.filter { it.updateOperationResult == UpdateOperationResult.DELETED }.size)
+ ).shouldSucceedWith { operationResults ->
+ val successfulOperations = operationResults.getSucceededOperations()
+ assertEquals(1, successfulOperations.size)
+ assertEquals(1, successfulOperations.filter { it.operationStatus == OperationStatus.DELETED }.size)
}
coVerify(exactly = 1) {
@@ -545,10 +547,10 @@ class EntityAttributeServiceTests : WithTimescaleContainer, WithKafkaContainer()
expandedAttributes,
createdAt,
null
- ).shouldSucceedWith { updateResult ->
- val updatedDetails = updateResult.updated
- assertEquals(1, updatedDetails.size)
- assertEquals(1, updatedDetails.filter { it.updateOperationResult == UpdateOperationResult.DELETED }.size)
+ ).shouldSucceedWith { operationResults ->
+ val successfulOperations = operationResults.getSucceededOperations()
+ assertEquals(1, successfulOperations.size)
+ assertEquals(1, successfulOperations.filter { it.operationStatus == OperationStatus.DELETED }.size)
}
coVerify(exactly = 1) {
@@ -589,10 +591,8 @@ class EntityAttributeServiceTests : WithTimescaleContainer, WithKafkaContainer()
expandedAttribute,
createdAt,
null
- ).shouldSucceedWith { updateResult ->
- val updatedDetails = updateResult.updated
- assertEquals(1, updatedDetails.size)
- assertEquals(1, updatedDetails.filter { it.updateOperationResult == UpdateOperationResult.DELETED }.size)
+ ).shouldSucceedWith { operationResult ->
+ assertEquals(OperationStatus.DELETED, operationResult.operationStatus)
}
coVerify(exactly = 1) {
@@ -671,11 +671,9 @@ class EntityAttributeServiceTests : WithTimescaleContainer, WithKafkaContainer()
expandedAttribute,
replacedAt,
null
- ).shouldSucceedWith {
- assertTrue(it.updated.isEmpty())
- assertEquals(1, it.notUpdated.size)
- val notUpdatedDetails = it.notUpdated.first()
- assertEquals(NGSILD_DEFAULT_VOCAB + "unknown", notUpdatedDetails.attributeName)
+ ).shouldSucceedWith { operationResult ->
+ assertInstanceOf(FailedAttributeOperationResult::class.java, operationResult)
+ assertEquals(NGSILD_DEFAULT_VOCAB + "unknown", operationResult.attributeName)
}
}
diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityEventServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityEventServiceTests.kt
index dd2914563..907eabb63 100644
--- a/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityEventServiceTests.kt
+++ b/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityEventServiceTests.kt
@@ -2,12 +2,10 @@ package com.egm.stellio.search.entity.service
import arrow.core.right
import com.egm.stellio.search.entity.model.Entity
-import com.egm.stellio.search.entity.model.UpdateOperationResult
-import com.egm.stellio.search.entity.model.UpdateResult
-import com.egm.stellio.search.entity.model.UpdatedDetails
+import com.egm.stellio.search.entity.model.OperationStatus
+import com.egm.stellio.search.entity.model.SucceededAttributeOperationResult
import com.egm.stellio.search.support.EMPTY_PAYLOAD
import com.egm.stellio.shared.model.AttributeAppendEvent
-import com.egm.stellio.shared.model.AttributeDeleteAllInstancesEvent
import com.egm.stellio.shared.model.AttributeDeleteEvent
import com.egm.stellio.shared.model.AttributeReplaceEvent
import com.egm.stellio.shared.model.AttributeUpdateEvent
@@ -16,8 +14,8 @@ import com.egm.stellio.shared.model.EntityEvent
import com.egm.stellio.shared.model.EventsType
import com.egm.stellio.shared.model.ExpandedAttribute
import com.egm.stellio.shared.model.ExpandedAttributeInstance
+import com.egm.stellio.shared.model.ExpandedEntity
import com.egm.stellio.shared.model.ExpandedTerm
-import com.egm.stellio.shared.model.toExpandedAttributes
import com.egm.stellio.shared.util.AQUAC_COMPOUND_CONTEXT
import com.egm.stellio.shared.util.JsonLdUtils.expandAttribute
import com.egm.stellio.shared.util.JsonLdUtils.expandAttributes
@@ -32,6 +30,7 @@ import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
+import io.r2dbc.postgresql.codec.Json
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
@@ -135,7 +134,7 @@ class EntityEventServiceTests {
}
every { kafkaTemplate.send(any(), any(), any()) } returns CompletableFuture()
- entityEventService.publishEntityDeleteEvent(null, entity).join()
+ entityEventService.publishEntityDeleteEvent(null, entity, ExpandedEntity(emptyMap())).join()
verify { kafkaTemplate.send("cim.entity._CatchAll", breedingServiceUri.toString(), any()) }
}
@@ -151,12 +150,13 @@ class EntityEventServiceTests {
entityEventService.publishAttributeChangeEvents(
"sub",
breedingServiceUri,
- expandedAttribute.toExpandedAttributes(),
- UpdateResult(
- listOf(UpdatedDetails(fishNumberProperty, null, UpdateOperationResult.APPENDED)),
- emptyList()
- ),
- true
+ listOf(
+ SucceededAttributeOperationResult(
+ attributeName = fishNumberProperty,
+ operationStatus = OperationStatus.APPENDED,
+ newExpandedValue = expandedAttribute.second[0]
+ )
+ )
).join()
verify {
@@ -186,12 +186,13 @@ class EntityEventServiceTests {
entityEventService.publishAttributeChangeEvents(
null,
breedingServiceUri,
- expandedAttribute.toExpandedAttributes(),
- UpdateResult(
- listOf(UpdatedDetails(fishNumberProperty, null, UpdateOperationResult.REPLACED)),
- emptyList()
- ),
- true
+ listOf(
+ SucceededAttributeOperationResult(
+ attributeName = fishNumberProperty,
+ operationStatus = OperationStatus.REPLACED,
+ newExpandedValue = expandedAttribute.second[0]
+ )
+ )
).join()
verify {
@@ -222,24 +223,24 @@ class EntityEventServiceTests {
}
""".trimIndent()
val jsonLdAttributes = expandAttributes(attributesPayload, listOf(AQUAC_COMPOUND_CONTEXT))
- val appendResult = UpdateResult(
- listOf(
- UpdatedDetails(fishNumberProperty, null, UpdateOperationResult.APPENDED),
- UpdatedDetails(fishNameProperty, fishName1DatasetUri, UpdateOperationResult.REPLACED)
+ val operationResult = listOf(
+ SucceededAttributeOperationResult(
+ attributeName = fishNumberProperty,
+ operationStatus = OperationStatus.APPENDED,
+ newExpandedValue = jsonLdAttributes[fishNumberProperty]!![0]
),
- emptyList()
+ SucceededAttributeOperationResult(
+ attributeName = fishNameProperty,
+ datasetId = fishName1DatasetUri,
+ operationStatus = OperationStatus.REPLACED,
+ newExpandedValue = jsonLdAttributes[fishNameProperty]!![0]
+ )
)
coEvery { entityQueryService.retrieve(breedingServiceUri) } returns entity.right()
every { entity.types } returns listOf(breedingServiceType)
- entityEventService.publishAttributeChangeEvents(
- null,
- breedingServiceUri,
- jsonLdAttributes,
- appendResult,
- true
- ).join()
+ entityEventService.publishAttributeChangeEvents(null, breedingServiceUri, operationResult).join()
verify {
entityEventService["publishEntityEvent"](
@@ -287,12 +288,18 @@ class EntityEventServiceTests {
}
""".trimIndent()
val jsonLdAttributes = expandAttributes(attributesPayload, listOf(AQUAC_COMPOUND_CONTEXT))
- val updateResult = UpdateResult(
- updated = arrayListOf(
- UpdatedDetails(fishNameProperty, fishName1DatasetUri, UpdateOperationResult.REPLACED),
- UpdatedDetails(fishNumberProperty, null, UpdateOperationResult.REPLACED)
+ val operationResult = listOf(
+ SucceededAttributeOperationResult(
+ attributeName = fishNumberProperty,
+ operationStatus = OperationStatus.REPLACED,
+ newExpandedValue = jsonLdAttributes[fishNumberProperty]!![0]
),
- notUpdated = arrayListOf()
+ SucceededAttributeOperationResult(
+ attributeName = fishNameProperty,
+ datasetId = fishName1DatasetUri,
+ operationStatus = OperationStatus.REPLACED,
+ newExpandedValue = jsonLdAttributes[fishNameProperty]!![0]
+ )
)
coEvery { entityQueryService.retrieve(breedingServiceUri) } returns entity.right()
@@ -301,9 +308,7 @@ class EntityEventServiceTests {
entityEventService.publishAttributeChangeEvents(
null,
breedingServiceUri,
- jsonLdAttributes,
- updateResult,
- true
+ operationResult
).join()
verify {
@@ -346,24 +351,25 @@ class EntityEventServiceTests {
}
""".trimIndent()
val jsonLdAttributes = expandAttributes(attributePayload, listOf(AQUAC_COMPOUND_CONTEXT))
- val updateResult = UpdateResult(
- updated = arrayListOf(
- UpdatedDetails(fishNameProperty, fishName1DatasetUri, UpdateOperationResult.REPLACED),
- UpdatedDetails(fishNameProperty, fishName2DatasetUri, UpdateOperationResult.REPLACED)
+ val operationResult = listOf(
+ SucceededAttributeOperationResult(
+ attributeName = fishNameProperty,
+ datasetId = fishName1DatasetUri,
+ operationStatus = OperationStatus.REPLACED,
+ newExpandedValue = jsonLdAttributes[fishNameProperty]!![0]
),
- notUpdated = arrayListOf()
+ SucceededAttributeOperationResult(
+ attributeName = fishNameProperty,
+ datasetId = fishName2DatasetUri,
+ operationStatus = OperationStatus.REPLACED,
+ newExpandedValue = jsonLdAttributes[fishNameProperty]!![1]
+ )
)
coEvery { entityQueryService.retrieve(breedingServiceUri) } returns entity.right()
every { entity.types } returns listOf(breedingServiceType)
- entityEventService.publishAttributeChangeEvents(
- null,
- breedingServiceUri,
- jsonLdAttributes,
- updateResult,
- true
- ).join()
+ entityEventService.publishAttributeChangeEvents(null, breedingServiceUri, operationResult).join()
verify {
entityEventService["publishEntityEvent"](
@@ -398,8 +404,13 @@ class EntityEventServiceTests {
fishNameAttributeFragment,
listOf(AQUAC_COMPOUND_CONTEXT)
)
- val updatedDetails = listOf(
- UpdatedDetails(fishNameProperty, fishName1DatasetUri, UpdateOperationResult.UPDATED)
+ val operationResult = listOf(
+ SucceededAttributeOperationResult(
+ attributeName = fishNameProperty,
+ datasetId = fishName1DatasetUri,
+ operationStatus = OperationStatus.UPDATED,
+ newExpandedValue = expandedAttribute.second[0]
+ )
)
coEvery { entityQueryService.retrieve(breedingServiceUri) } returns entity.right()
@@ -408,9 +419,7 @@ class EntityEventServiceTests {
entityEventService.publishAttributeChangeEvents(
null,
breedingServiceUri,
- expandedAttribute.toExpandedAttributes(),
- UpdateResult(updatedDetails, emptyList()),
- false
+ operationResult
).join()
verify {
@@ -429,39 +438,47 @@ class EntityEventServiceTests {
}
@Test
- fun `it should publish ATTRIBUTE_DELETE_ALL_INSTANCE event if all instances of an attribute are deleted`() =
- runTest {
- val entity = mockk(relaxed = true)
-
- coEvery { entityQueryService.retrieve(breedingServiceUri) } returns entity.right()
- every { entity.types } returns listOf(breedingServiceType)
+ fun `it should publish ATTRIBUTE_DELETE event if an attribute has been deleted as part of an update `() = runTest {
+ val entity = mockk(relaxed = true).apply {
+ every { payload } returns Json.of("{}")
+ every { types } returns listOf(breedingServiceType)
+ }
+ coEvery { entityQueryService.retrieve(breedingServiceUri) } returns entity.right()
- entityEventService.publishAttributeDeleteEvent(
- null,
- breedingServiceUri,
- fishNameProperty,
- null,
- true
- ).join()
+ entityEventService.publishAttributeChangeEvents(
+ null,
+ breedingServiceUri,
+ listOf(
+ SucceededAttributeOperationResult(
+ fishNameProperty,
+ fishName1DatasetUri,
+ OperationStatus.DELETED,
+ emptyMap()
+ )
+ )
+ ).join()
- verify {
- entityEventService["publishEntityEvent"](
- match { entityEvent ->
- listOf(entityEvent).all {
- it.operationType == EventsType.ATTRIBUTE_DELETE_ALL_INSTANCES &&
- it.entityId == breedingServiceUri &&
- it.entityTypes == listOf(breedingServiceType) &&
- it.attributeName == fishNameProperty &&
- it.contexts.isEmpty()
- }
+ verify {
+ entityEventService["publishEntityEvent"](
+ match { entityEvent ->
+ listOf(entityEvent).all {
+ it.operationType == EventsType.ATTRIBUTE_DELETE &&
+ it.entityId == breedingServiceUri &&
+ it.entityTypes == listOf(breedingServiceType) &&
+ it.attributeName == fishNameProperty &&
+ it.datasetId == fishName1DatasetUri &&
+ it.contexts.isEmpty()
}
- )
- }
+ }
+ )
}
+ }
@Test
fun `it should publish ATTRIBUTE_DELETE event if an instance of an attribute is deleted`() = runTest {
- val entity = mockk(relaxed = true)
+ val entity = mockk(relaxed = true).apply {
+ every { payload } returns Json.of("{}")
+ }
coEvery { entityQueryService.retrieve(breedingServiceUri) } returns entity.right()
every { entity.types } returns listOf(breedingServiceType)
@@ -469,9 +486,12 @@ class EntityEventServiceTests {
entityEventService.publishAttributeDeleteEvent(
null,
breedingServiceUri,
- fishNameProperty,
- fishName1DatasetUri,
- false
+ SucceededAttributeOperationResult(
+ fishNameProperty,
+ fishName1DatasetUri,
+ OperationStatus.DELETED,
+ emptyMap()
+ )
).join()
verify {
diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityServiceTests.kt
index 54b86b4c5..5c079f2b8 100644
--- a/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityServiceTests.kt
+++ b/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityServiceTests.kt
@@ -5,11 +5,9 @@ import arrow.core.right
import arrow.core.toOption
import com.egm.stellio.search.authorization.service.AuthorizationService
import com.egm.stellio.search.common.util.deserializeAsMap
-import com.egm.stellio.search.entity.model.EMPTY_UPDATE_RESULT
import com.egm.stellio.search.entity.model.Entity
-import com.egm.stellio.search.entity.model.UpdateOperationResult
-import com.egm.stellio.search.entity.model.UpdateResult
-import com.egm.stellio.search.entity.model.UpdatedDetails
+import com.egm.stellio.search.entity.model.OperationStatus
+import com.egm.stellio.search.entity.model.SucceededAttributeOperationResult
import com.egm.stellio.search.support.WithKafkaContainer
import com.egm.stellio.search.support.WithTimescaleContainer
import com.egm.stellio.shared.model.AccessDeniedException
@@ -28,6 +26,7 @@ import com.egm.stellio.shared.util.JsonLdUtils.expandAttribute
import com.egm.stellio.shared.util.JsonLdUtils.expandAttributes
import com.egm.stellio.shared.util.JsonUtils.deserializeExpandedPayload
import com.egm.stellio.shared.util.OUTGOING_PROPERTY
+import com.egm.stellio.shared.util.loadAndExpandDeletedEntity
import com.egm.stellio.shared.util.loadAndPrepareSampleData
import com.egm.stellio.shared.util.loadMinimalEntity
import com.egm.stellio.shared.util.loadSampleData
@@ -187,7 +186,7 @@ class EntityServiceTests : WithTimescaleContainer, WithKafkaContainer() {
now
)
- entityService.deleteEntityPayload(entity01Uri, ngsiLdDateTime())
+ entityService.deleteEntityPayload(entity01Uri, ngsiLdDateTime(), loadAndExpandDeletedEntity(entity01Uri))
.shouldSucceedWith {
assertEquals(entity01Uri, it.entityId)
assertNotNull(it.payload)
@@ -215,7 +214,7 @@ class EntityServiceTests : WithTimescaleContainer, WithKafkaContainer() {
now
)
- entityService.deleteEntityPayload(entity01Uri, ngsiLdDateTime())
+ entityService.deleteEntityPayload(entity01Uri, ngsiLdDateTime(), loadAndExpandDeletedEntity(entity01Uri))
.shouldSucceedWith {
assertEquals(entity01Uri, it.entityId)
assertNotNull(it.payload)
@@ -238,9 +237,10 @@ class EntityServiceTests : WithTimescaleContainer, WithKafkaContainer() {
now
).shouldSucceed()
- entityService.deleteEntityPayload(entity01Uri, ngsiLdDateTime()).shouldSucceed()
+ entityService.deleteEntityPayload(entity01Uri, ngsiLdDateTime(), loadAndExpandDeletedEntity(entity01Uri))
+ .shouldSucceed()
- entityQueryService.retrieve(entity01Uri)
+ entityQueryService.retrieve(entity01Uri, true)
.shouldSucceedWith { entity ->
val payload = entity.payload.deserializeAsMap()
assertThat(payload)
@@ -258,9 +258,8 @@ class EntityServiceTests : WithTimescaleContainer, WithKafkaContainer() {
} returns Unit.right()
coEvery {
entityAttributeService.mergeAttributes(any(), any(), any(), any(), any(), any())
- } returns UpdateResult(
- listOf(UpdatedDetails(INCOMING_PROPERTY, null, UpdateOperationResult.APPENDED)),
- emptyList()
+ } returns listOf(
+ SucceededAttributeOperationResult(INCOMING_PROPERTY, null, OperationStatus.APPENDED, emptyMap()),
).right()
coEvery { entityAttributeService.getForEntity(any(), any(), any()) } returns emptyList()
coEvery { authorizationService.createOwnerRight(any(), any()) } returns Unit.right()
@@ -324,9 +323,8 @@ class EntityServiceTests : WithTimescaleContainer, WithKafkaContainer() {
} returns Unit.right()
coEvery {
entityAttributeService.mergeAttributes(any(), any(), any(), any(), any(), any())
- } returns UpdateResult(
- listOf(UpdatedDetails(INCOMING_PROPERTY, null, UpdateOperationResult.APPENDED)),
- emptyList()
+ } returns listOf(
+ SucceededAttributeOperationResult(INCOMING_PROPERTY, null, OperationStatus.APPENDED, emptyMap())
).right()
coEvery { entityAttributeService.getForEntity(any(), any(), any()) } returns emptyList()
coEvery { authorizationService.createOwnerRight(any(), any()) } returns Unit.right()
@@ -367,13 +365,9 @@ class EntityServiceTests : WithTimescaleContainer, WithKafkaContainer() {
} returns Unit.right()
coEvery {
entityAttributeService.mergeAttributes(any(), any(), any(), any(), any(), any())
- } returns UpdateResult(
- listOf(UpdatedDetails(INCOMING_PROPERTY, null, UpdateOperationResult.APPENDED)),
- emptyList()
+ } returns listOf(
+ SucceededAttributeOperationResult(INCOMING_PROPERTY, null, OperationStatus.APPENDED, emptyMap())
).right()
- coEvery {
- entityAttributeService.partialUpdateAttribute(any(), any(), any(), any())
- } returns EMPTY_UPDATE_RESULT.right()
coEvery { entityAttributeService.getForEntity(any(), any(), any()) } returns emptyList()
coEvery { authorizationService.createOwnerRight(any(), any()) } returns Unit.right()
@@ -468,9 +462,10 @@ class EntityServiceTests : WithTimescaleContainer, WithKafkaContainer() {
coEvery { entityAttributeService.getForEntity(any(), any(), any()) } returns emptyList()
coEvery {
entityAttributeService.replaceAttribute(any(), any(), any(), any(), any())
- } returns UpdateResult(
- updated = listOf(UpdatedDetails(INCOMING_PROPERTY, null, UpdateOperationResult.REPLACED)),
- notUpdated = emptyList()
+ } returns SucceededAttributeOperationResult(
+ attributeName = INCOMING_PROPERTY,
+ operationStatus = OperationStatus.REPLACED,
+ newExpandedValue = emptyMap()
).right()
val (jsonLdEntity, ngsiLdEntity) = loadSampleData().sampleDataToNgsiLdEntity().shouldSucceedAndResult()
@@ -485,8 +480,7 @@ class EntityServiceTests : WithTimescaleContainer, WithKafkaContainer() {
.shouldSucceedWith {
it.updated.size == 1 &&
it.notUpdated.isEmpty() &&
- it.updated[0].attributeName == INCOMING_PROPERTY &&
- it.updated[0].updateOperationResult == UpdateOperationResult.REPLACED
+ it.updated[0] == INCOMING_PROPERTY
}
}
@@ -503,11 +497,9 @@ class EntityServiceTests : WithTimescaleContainer, WithKafkaContainer() {
entityService.updateTypes(beehiveTestCId, listOf(BEEHIVE_TYPE, APIARY_TYPE), ngsiLdDateTime(), false)
.shouldSucceedWith {
- assertTrue(it.isSuccessful())
- assertEquals(1, it.updated.size)
- val updatedDetails = it.updated[0]
- assertEquals(JSONLD_TYPE, updatedDetails.attributeName)
- assertEquals(UpdateOperationResult.APPENDED, updatedDetails.updateOperationResult)
+ assertInstanceOf(SucceededAttributeOperationResult::class.java, it)
+ assertEquals(JSONLD_TYPE, it.attributeName)
+ assertEquals(OperationStatus.APPENDED, it.operationStatus)
}
entityQueryService.retrieve(beehiveTestCId)
diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/entity/web/EntityHandlerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/entity/web/EntityHandlerTests.kt
index aefc5d192..3b89c8169 100644
--- a/search-service/src/test/kotlin/com/egm/stellio/search/entity/web/EntityHandlerTests.kt
+++ b/search-service/src/test/kotlin/com/egm/stellio/search/entity/web/EntityHandlerTests.kt
@@ -10,9 +10,7 @@ import com.egm.stellio.search.csr.service.ContextSourceCaller
import com.egm.stellio.search.csr.service.ContextSourceRegistrationService
import com.egm.stellio.search.entity.model.EntitiesQueryFromGet
import com.egm.stellio.search.entity.model.NotUpdatedDetails
-import com.egm.stellio.search.entity.model.UpdateOperationResult
import com.egm.stellio.search.entity.model.UpdateResult
-import com.egm.stellio.search.entity.model.UpdatedDetails
import com.egm.stellio.search.entity.service.EntityQueryService
import com.egm.stellio.search.entity.service.EntityService
import com.egm.stellio.search.entity.service.LinkedEntityService
@@ -1290,7 +1288,7 @@ class EntityHandlerTests {
NGSILDWarning.HEADER_NAME,
"199 urn:ngsi-ld:ContextSourceRegistration:test \"message with line breaks\"",
"199 urn:ngsi-ld:ContextSourceRegistration:test \"message\""
- ).expectHeader().valueEquals(RESULTS_COUNT_HEADER, "0",)
+ ).expectHeader().valueEquals(RESULTS_COUNT_HEADER, "0")
coVerify(exactly = 2) { contextSourceRegistrationService.updateContextSourceStatus(any(), false) }
}
@@ -1407,13 +1405,7 @@ class EntityHandlerTests {
val jsonLdFile = ClassPathResource("/ngsild/aquac/fragments/BreedingService_newProperty.json")
val entityId = "urn:ngsi-ld:BreedingService:0214".toUri()
val appendResult = UpdateResult(
- listOf(
- UpdatedDetails(
- fishNumberAttribute,
- null,
- UpdateOperationResult.APPENDED
- )
- ),
+ listOf(fishNumberAttribute),
emptyList()
)
@@ -1444,13 +1436,7 @@ class EntityHandlerTests {
val jsonLdFile = ClassPathResource("/ngsild/aquac/fragments/BreedingService_twoNewProperties.json")
val entityId = "urn:ngsi-ld:BreedingService:0214".toUri()
val appendResult = UpdateResult(
- listOf(
- UpdatedDetails(
- fishNumberAttribute,
- null,
- UpdateOperationResult.APPENDED
- )
- ),
+ listOf(fishNumberAttribute),
listOf(NotUpdatedDetails(fishSizeAttribute, "overwrite disallowed"))
)
@@ -1489,7 +1475,7 @@ class EntityHandlerTests {
val jsonLdFile = ClassPathResource("/ngsild/aquac/fragments/BreedingService_newType.json")
val entityId = "urn:ngsi-ld:BreedingService:0214".toUri()
val appendTypeResult = UpdateResult(
- listOf(UpdatedDetails(JSONLD_TYPE, null, UpdateOperationResult.APPENDED)),
+ listOf(JSONLD_TYPE),
emptyList()
)
@@ -1519,18 +1505,16 @@ class EntityHandlerTests {
fun `append entity attribute should return a 207 if types or attributes could not be appended`() {
val jsonLdFile = ClassPathResource("/ngsild/aquac/fragments/BreedingService_newInvalidTypeAndAttribute.json")
val entityId = "urn:ngsi-ld:BreedingService:0214".toUri()
- val appendTypeResult = UpdateResult(
- emptyList(),
- listOf(NotUpdatedDetails(JSONLD_TYPE, "Append operation has unexpectedly failed"))
- )
- val appendResult = UpdateResult(
- listOf(UpdatedDetails(fishNumberAttribute, null, UpdateOperationResult.APPENDED)),
- listOf(NotUpdatedDetails(fishSizeAttribute, "overwrite disallowed"))
- )
coEvery {
entityService.appendAttributes(any(), any(), any(), any())
- } returns appendTypeResult.mergeWith(appendResult).right()
+ } returns UpdateResult(
+ updated = listOf(fishNumberAttribute),
+ notUpdated = listOf(
+ NotUpdatedDetails(JSONLD_TYPE, "Append operation has unexpectedly failed"),
+ NotUpdatedDetails(fishSizeAttribute, "overwrite disallowed")
+ )
+ ).right()
webClient.post()
.uri("/ngsi-ld/v1/entities/$entityId/attrs")
@@ -1649,10 +1633,8 @@ class EntityHandlerTests {
val entityId = "urn:ngsi-ld:DeadFishes:019BN".toUri()
val attrId = "fishNumber"
val updateResult = UpdateResult(
- updated = arrayListOf(
- UpdatedDetails(fishNumberAttribute, "urn:ngsi-ld:Dataset:1".toUri(), UpdateOperationResult.UPDATED)
- ),
- notUpdated = arrayListOf()
+ updated = listOf(fishNumberAttribute),
+ notUpdated = emptyList()
)
coEvery {
@@ -1755,18 +1737,7 @@ class EntityHandlerTests {
val jsonLdFile = ClassPathResource("/ngsild/aquac/fragments/DeadFishes_mergeEntity.json")
val entityId = "urn:ngsi-ld:DeadFishes:019BN".toUri()
val updateResult = UpdateResult(
- updated = arrayListOf(
- UpdatedDetails(
- fishNumberAttribute,
- null,
- UpdateOperationResult.REPLACED
- ),
- UpdatedDetails(
- fishSizeAttribute,
- null,
- UpdateOperationResult.APPENDED
- )
- ),
+ updated = listOf(fishNumberAttribute, fishSizeAttribute),
notUpdated = emptyList()
)
@@ -1792,18 +1763,7 @@ class EntityHandlerTests {
val jsonLdFile = ClassPathResource("/ngsild/aquac/fragments/DeadFishes_mergeEntity.json")
val entityId = "urn:ngsi-ld:DeadFishes:019BN".toUri()
val updateResult = UpdateResult(
- updated = arrayListOf(
- UpdatedDetails(
- fishNumberAttribute,
- null,
- UpdateOperationResult.REPLACED
- ),
- UpdatedDetails(
- fishSizeAttribute,
- null,
- UpdateOperationResult.APPENDED
- )
- ),
+ updated = listOf(fishNumberAttribute, fishSizeAttribute),
notUpdated = emptyList()
)
@@ -1931,13 +1891,7 @@ class EntityHandlerTests {
val jsonLdFile = ClassPathResource("/ngsild/aquac/fragments/DeadFishes_updateEntityAttribute.json")
val entityId = "urn:ngsi-ld:DeadFishes:019BN".toUri()
val updateResult = UpdateResult(
- updated = arrayListOf(
- UpdatedDetails(
- fishNumberAttribute,
- null,
- UpdateOperationResult.REPLACED
- )
- ),
+ updated = listOf(fishNumberAttribute),
notUpdated = emptyList()
)
@@ -1969,10 +1923,8 @@ class EntityHandlerTests {
coEvery {
entityService.updateAttributes(any(), any(), any())
} returns UpdateResult(
- updated = arrayListOf(
- UpdatedDetails(fishNumberAttribute, null, UpdateOperationResult.REPLACED)
- ),
- notUpdated = arrayListOf(notUpdatedAttribute)
+ updated = listOf(fishNumberAttribute),
+ notUpdated = listOf(notUpdatedAttribute)
).right()
webClient.patch()
@@ -2354,9 +2306,7 @@ class EntityHandlerTests {
coEvery {
entityService.replaceAttribute(any(), any(), any())
} returns UpdateResult(
- updated = arrayListOf(
- UpdatedDetails(INCOMING_PROPERTY, null, UpdateOperationResult.REPLACED)
- ),
+ updated = listOf(INCOMING_PROPERTY),
notUpdated = emptyList()
).right()
diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/AttributeInstanceServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/AttributeInstanceServiceTests.kt
index 5cca2a169..37f9eef69 100644
--- a/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/AttributeInstanceServiceTests.kt
+++ b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/AttributeInstanceServiceTests.kt
@@ -742,7 +742,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer
AttributeInstanceService(databaseClient, searchProperties),
recordPrivateCalls = true
)
- val deletedAt = ngsiLdDateTime()
+ val deletedAt = ZonedDateTime.parse("2025-01-02T11:20:30.000001Z")
val attributeValues = mapOf(
NGSILD_DELETED_AT_PROPERTY to listOf(
mapOf(
@@ -767,14 +767,14 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer
verify {
attributeInstanceService["create"](
match {
- it.time == deletedAt &&
+ it.time.toString() == "2025-01-02T11:20:30.000001Z" &&
it.value == "urn:ngsi-ld:null" &&
it.measuredValue == null &&
it.payload.asString().matchContent(
"""
{
"https://uri.etsi.org/ngsi-ld/deletedAt":[{
- "@value":"$deletedAt",
+ "@value":"2025-01-02T11:20:30.000001Z",
"@type":"https://uri.etsi.org/ngsi-ld/DateTime"
}],
"https://uri.etsi.org/ngsi-ld/hasValue":[{
diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/TemporalQueryServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/TemporalQueryServiceTests.kt
index 21303c96b..2a3cc15d9 100644
--- a/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/TemporalQueryServiceTests.kt
+++ b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/TemporalQueryServiceTests.kt
@@ -83,7 +83,7 @@ class TemporalQueryServiceTests {
@Test
fun `it should return an API exception if the entity does not exist`() = runTest {
coEvery {
- entityQueryService.checkEntityExistence(any())
+ entityQueryService.retrieve(any(), any())
} returns ResourceNotFoundException(entityNotFoundMessage(entityUri.toString())).left()
temporalQueryService.queryTemporalEntity(
@@ -116,10 +116,9 @@ class TemporalQueryServiceTests {
)
}
- coEvery { entityQueryService.checkEntityExistence(any()) } returns Unit.right()
+ coEvery { entityQueryService.retrieve(any()) } returns gimmeEntityPayload().right()
coEvery { authorizationService.userCanReadEntity(any(), any()) } returns Unit.right()
coEvery { entityAttributeService.getForEntity(any(), any(), any(), any()) } returns attributes
- coEvery { entityQueryService.retrieve(any()) } returns gimmeEntityPayload().right()
coEvery { scopeService.retrieveHistory(any(), any()) } returns emptyList().right()
coEvery {
attributeInstanceService.search(any(), any>())
@@ -147,7 +146,7 @@ class TemporalQueryServiceTests {
)
coVerify {
- entityQueryService.checkEntityExistence(entityUri)
+ entityQueryService.retrieve(entityUri)
authorizationService.userCanReadEntity(entityUri, None)
entityAttributeService.getForEntity(entityUri, emptySet(), emptySet(), false)
attributeInstanceService.search(
diff --git a/shared/config/detekt/baseline.xml b/shared/config/detekt/baseline.xml
index c359f0fb8..849a3ca3e 100644
--- a/shared/config/detekt/baseline.xml
+++ b/shared/config/detekt/baseline.xml
@@ -7,7 +7,6 @@
LongMethod:QueryUtils.kt$private fun transformQQueryToSqlJsonPath( mainAttributePath: List<ExpandedTerm>, trailingAttributePath: List<ExpandedTerm>, operator: String, value: String )
LongParameterList:ApiResponses.kt$( body: String, count: Int, resourceUrl: String, paginationQuery: PaginationQuery, requestParams: MultiValueMap<String, String>, mediaType: MediaType, contexts: List<String> )
LongParameterList:ApiResponses.kt$( entities: Any, count: Int, resourceUrl: String, paginationQuery: PaginationQuery, requestParams: MultiValueMap<String, String>, mediaType: MediaType, contexts: List<String> )
- SpreadOperator:EntityEvent.kt$EntityEvent$( *[ JsonSubTypes.Type(value = EntityCreateEvent::class), JsonSubTypes.Type(value = EntityReplaceEvent::class), JsonSubTypes.Type(value = EntityDeleteEvent::class), JsonSubTypes.Type(value = AttributeAppendEvent::class), JsonSubTypes.Type(value = AttributeReplaceEvent::class), JsonSubTypes.Type(value = AttributeUpdateEvent::class), JsonSubTypes.Type(value = AttributeDeleteEvent::class), JsonSubTypes.Type(value = AttributeDeleteAllInstancesEvent::class) ] )
TooManyFunctions:JsonLdUtils.kt$JsonLdUtils
diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/model/CompactedEntity.kt b/shared/src/main/kotlin/com/egm/stellio/shared/model/CompactedEntity.kt
index 6eb784a24..cf9e8d562 100644
--- a/shared/src/main/kotlin/com/egm/stellio/shared/model/CompactedEntity.kt
+++ b/shared/src/main/kotlin/com/egm/stellio/shared/model/CompactedEntity.kt
@@ -6,6 +6,7 @@ import com.egm.stellio.shared.model.AttributeCompactedType.LANGUAGEPROPERTY
import com.egm.stellio.shared.model.AttributeCompactedType.PROPERTY
import com.egm.stellio.shared.model.AttributeCompactedType.RELATIONSHIP
import com.egm.stellio.shared.model.AttributeCompactedType.VOCABPROPERTY
+import com.egm.stellio.shared.model.AttributeCompactedType.entries
import com.egm.stellio.shared.queryparameter.QueryParameter
import com.egm.stellio.shared.util.FEATURES_PROPERTY_TERM
import com.egm.stellio.shared.util.FEATURE_COLLECTION_TYPE
@@ -23,6 +24,7 @@ import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_VOCAB_TERM
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_CREATED_AT_TERM
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_DATASET_ID_TERM
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_DATASET_TERM
+import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_DELETED_AT_TERM
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_ENTITY_TERM
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_GEOPROPERTY_TERM
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_JSONPROPERTY_TERM
@@ -39,8 +41,7 @@ import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_VOCABPROPERTY_TERM
import com.egm.stellio.shared.util.PROPERTIES_PROPERTY_TERM
import com.egm.stellio.shared.util.toUri
import java.net.URI
-import java.util.Locale
-import kotlin.collections.Map
+import java.util.*
typealias CompactedEntity = Map
typealias CompactedAttributeInstance = Map
@@ -220,7 +221,8 @@ fun CompactedEntity.withoutSysAttrs(sysAttrToKeep: String?): Map {
}
return this.filter {
- !sysAttrsToRemove.contains(it.key)
+ // deletedAt has to be kept at entity level (but not in attributes), see 5.8.6
+ !sysAttrsToRemove.minus(NGSILD_DELETED_AT_TERM).contains(it.key)
}.mapValues {
when (it.value) {
is Map<*, *> -> removeSysAttrsFromAttrInstance(it.value as Map<*, *>)
diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/model/EntityEvent.kt b/shared/src/main/kotlin/com/egm/stellio/shared/model/EntityEvent.kt
index 1de2e0332..9005ce8ae 100644
--- a/shared/src/main/kotlin/com/egm/stellio/shared/model/EntityEvent.kt
+++ b/shared/src/main/kotlin/com/egm/stellio/shared/model/EntityEvent.kt
@@ -9,15 +9,14 @@ import java.net.URI
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "operationType")
@JsonSubTypes(
- *[
+ value = [
JsonSubTypes.Type(value = EntityCreateEvent::class),
JsonSubTypes.Type(value = EntityReplaceEvent::class),
JsonSubTypes.Type(value = EntityDeleteEvent::class),
JsonSubTypes.Type(value = AttributeAppendEvent::class),
JsonSubTypes.Type(value = AttributeReplaceEvent::class),
JsonSubTypes.Type(value = AttributeUpdateEvent::class),
- JsonSubTypes.Type(value = AttributeDeleteEvent::class),
- JsonSubTypes.Type(value = AttributeDeleteAllInstancesEvent::class)
+ JsonSubTypes.Type(value = AttributeDeleteEvent::class)
]
)
sealed class EntityEvent(
@@ -74,10 +73,11 @@ data class EntityDeleteEvent(
override val entityId: URI,
override val entityTypes: List,
// null only when in the case of an IAM event (previous state is not known)
- val deletedEntity: String?,
+ val previousEntity: String?,
+ val updatedEntity: String,
override val contexts: List
) : EntityEvent(EventsType.ENTITY_DELETE, sub, tenantName, entityId, entityTypes, contexts) {
- override fun getEntity() = this.deletedEntity
+ override fun getEntity() = this.previousEntity
}
@JsonTypeName("ATTRIBUTE_APPEND")
@@ -88,7 +88,6 @@ data class AttributeAppendEvent(
override val entityTypes: List,
val attributeName: ExpandedTerm,
val datasetId: URI?,
- val overwrite: Boolean = true,
val operationPayload: String,
val updatedEntity: String,
override val contexts: List
@@ -144,20 +143,6 @@ data class AttributeDeleteEvent(
override fun getAttribute() = this.attributeName
}
-@JsonTypeName("ATTRIBUTE_DELETE_ALL_INSTANCES")
-data class AttributeDeleteAllInstancesEvent(
- override val sub: String?,
- override val tenantName: String = DEFAULT_TENANT_NAME,
- override val entityId: URI,
- override val entityTypes: List,
- val attributeName: ExpandedTerm,
- val updatedEntity: String,
- override val contexts: List
-) : EntityEvent(EventsType.ATTRIBUTE_DELETE_ALL_INSTANCES, sub, tenantName, entityId, entityTypes, contexts) {
- override fun getEntity() = this.updatedEntity
- override fun getAttribute() = this.attributeName
-}
-
enum class EventsType {
ENTITY_CREATE,
ENTITY_REPLACE,
@@ -165,8 +150,7 @@ enum class EventsType {
ATTRIBUTE_APPEND,
ATTRIBUTE_REPLACE,
ATTRIBUTE_UPDATE,
- ATTRIBUTE_DELETE,
- ATTRIBUTE_DELETE_ALL_INSTANCES
+ ATTRIBUTE_DELETE
}
fun unhandledOperationType(operationType: EventsType): String = "Entity event $operationType not handled."
diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/model/ExpandedMembers.kt b/shared/src/main/kotlin/com/egm/stellio/shared/model/ExpandedMembers.kt
index 7e53f6226..08242e58f 100644
--- a/shared/src/main/kotlin/com/egm/stellio/shared/model/ExpandedMembers.kt
+++ b/shared/src/main/kotlin/com/egm/stellio/shared/model/ExpandedMembers.kt
@@ -11,6 +11,7 @@ import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_CREATED_AT_PROPERTY
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_DATASET_ID_PROPERTY
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_DATE_TIME_TYPE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_DATE_TYPE
+import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_DELETED_AT_PROPERTY
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_MODIFIED_AT_PROPERTY
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_PROPERTY_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_RELATIONSHIP_OBJECT
@@ -91,8 +92,9 @@ fun ExpandedAttributeInstances.getSingleEntry(): ExpandedAttributeInstance {
fun ExpandedAttributeInstance.addSysAttrs(
withSysAttrs: Boolean,
createdAt: ZonedDateTime,
- modifiedAt: ZonedDateTime?
-): Map =
+ modifiedAt: ZonedDateTime? = null,
+ deletedAt: ZonedDateTime? = null
+): ExpandedAttributeInstance =
if (withSysAttrs)
this.plus(NGSILD_CREATED_AT_PROPERTY to buildNonReifiedTemporalValue(createdAt))
.let {
@@ -100,6 +102,11 @@ fun ExpandedAttributeInstance.addSysAttrs(
it.plus(NGSILD_MODIFIED_AT_PROPERTY to buildNonReifiedTemporalValue(modifiedAt))
else it
}
+ .let {
+ if (deletedAt != null)
+ it.plus(NGSILD_DELETED_AT_PROPERTY to buildNonReifiedTemporalValue(deletedAt))
+ else it
+ }
else this
/**
diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/util/JsonLdUtils.kt b/shared/src/main/kotlin/com/egm/stellio/shared/util/JsonLdUtils.kt
index ffaeb7ad3..58681c694 100644
--- a/shared/src/main/kotlin/com/egm/stellio/shared/util/JsonLdUtils.kt
+++ b/shared/src/main/kotlin/com/egm/stellio/shared/util/JsonLdUtils.kt
@@ -120,7 +120,7 @@ object JsonLdUtils {
const val NGSILD_CREATED_AT_TERM = "createdAt"
const val NGSILD_MODIFIED_AT_TERM = "modifiedAt"
- val NGSILD_SYSATTRS_TERMS = setOf(NGSILD_CREATED_AT_TERM, NGSILD_MODIFIED_AT_TERM)
+ val NGSILD_SYSATTRS_TERMS = setOf(NGSILD_CREATED_AT_TERM, NGSILD_MODIFIED_AT_TERM, NGSILD_DELETED_AT_TERM)
const val NGSILD_CREATED_AT_PROPERTY = "https://uri.etsi.org/ngsi-ld/$NGSILD_CREATED_AT_TERM"
const val NGSILD_MODIFIED_AT_PROPERTY = "https://uri.etsi.org/ngsi-ld/$NGSILD_MODIFIED_AT_TERM"
val NGSILD_SYSATTRS_PROPERTIES = setOf(NGSILD_CREATED_AT_PROPERTY, NGSILD_MODIFIED_AT_PROPERTY)
diff --git a/shared/src/test/kotlin/com/egm/stellio/shared/model/ExpandedMembersTests.kt b/shared/src/test/kotlin/com/egm/stellio/shared/model/ExpandedMembersTests.kt
index 51e49eee4..d91d68a38 100644
--- a/shared/src/test/kotlin/com/egm/stellio/shared/model/ExpandedMembersTests.kt
+++ b/shared/src/test/kotlin/com/egm/stellio/shared/model/ExpandedMembersTests.kt
@@ -2,6 +2,7 @@ package com.egm.stellio.shared.model
import com.egm.stellio.shared.util.JsonLdUtils
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_CREATED_AT_PROPERTY
+import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_DELETED_AT_PROPERTY
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_MODIFIED_AT_PROPERTY
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_RELATIONSHIP_OBJECT
import com.egm.stellio.shared.util.JsonLdUtils.buildExpandedPropertyValue
@@ -30,6 +31,7 @@ class ExpandedMembersTests {
assertThat(attrPayloadWithSysAttrs)
.containsKey(NGSILD_CREATED_AT_PROPERTY)
.doesNotContainKey(NGSILD_MODIFIED_AT_PROPERTY)
+ .doesNotContainKey(NGSILD_DELETED_AT_PROPERTY)
}
@Test
@@ -41,6 +43,20 @@ class ExpandedMembersTests {
assertThat(attrPayloadWithSysAttrs)
.containsKey(NGSILD_CREATED_AT_PROPERTY)
.containsKey(NGSILD_MODIFIED_AT_PROPERTY)
+ .doesNotContainKey(NGSILD_DELETED_AT_PROPERTY)
+ }
+
+ @Test
+ fun `it should add createdAt, modifiedAt and deletedAt information into an attribute`() {
+ val attrPayload = mapOf("attribute" to buildExpandedPropertyValue(12.0))
+
+ val attrPayloadWithSysAttrs =
+ attrPayload.addSysAttrs(true, ngsiLdDateTime(), ngsiLdDateTime(), ngsiLdDateTime())
+
+ assertThat(attrPayloadWithSysAttrs)
+ .containsKey(NGSILD_CREATED_AT_PROPERTY)
+ .containsKey(NGSILD_MODIFIED_AT_PROPERTY)
+ .containsKey(NGSILD_DELETED_AT_PROPERTY)
}
@Test
diff --git a/shared/src/test/kotlin/com/egm/stellio/shared/util/JsonUtilsTests.kt b/shared/src/test/kotlin/com/egm/stellio/shared/util/JsonUtilsTests.kt
index ddad6db9b..5c3563fcb 100644
--- a/shared/src/test/kotlin/com/egm/stellio/shared/util/JsonUtilsTests.kt
+++ b/shared/src/test/kotlin/com/egm/stellio/shared/util/JsonUtilsTests.kt
@@ -1,6 +1,5 @@
package com.egm.stellio.shared.util
-import com.egm.stellio.shared.model.AttributeDeleteAllInstancesEvent
import com.egm.stellio.shared.model.AttributeDeleteEvent
import com.egm.stellio.shared.model.AttributeReplaceEvent
import com.egm.stellio.shared.model.AttributeUpdateEvent
@@ -19,6 +18,7 @@ import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
+import java.time.ZonedDateTime
class JsonUtilsTests {
@@ -114,14 +114,6 @@ class JsonUtilsTests {
Assertions.assertTrue(parsedEvent is AttributeDeleteEvent)
}
- @Test
- fun `it should parse an event of type ATTRIBUTE_DELETE_ALL_INSTANCES`() {
- val parsedEvent = deserializeAs(
- loadSampleData("events/entity/attributeDeleteAllInstancesEvent.json")
- )
- Assertions.assertTrue(parsedEvent is AttributeDeleteAllInstancesEvent)
- }
-
@Test
fun `it should serialize an event of type ENTITY_CREATE`() = runTest {
val event = mapper.writeValueAsString(
@@ -146,6 +138,13 @@ class JsonUtilsTests {
entityId,
listOf(BEEHIVE_TYPE),
serializeObject(expandJsonLdFragment(entityPayload, APIC_COMPOUND_CONTEXTS)),
+ serializeObject(
+ loadAndExpandDeletedEntity(
+ entityId,
+ ZonedDateTime.parse("2024-12-23T17:01:02Z"),
+ APIC_COMPOUND_CONTEXTS
+ ).members
+ ),
emptyList()
)
)
diff --git a/shared/src/testFixtures/kotlin/com/egm/stellio/shared/util/TestUtils.kt b/shared/src/testFixtures/kotlin/com/egm/stellio/shared/util/TestUtils.kt
index 167737ce4..69723ecd9 100644
--- a/shared/src/testFixtures/kotlin/com/egm/stellio/shared/util/TestUtils.kt
+++ b/shared/src/testFixtures/kotlin/com/egm/stellio/shared/util/TestUtils.kt
@@ -14,6 +14,7 @@ import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdEntity
import com.egm.stellio.shared.util.JsonUtils.deserializeAsMap
import org.springframework.core.io.ClassPathResource
import java.net.URI
+import java.time.ZonedDateTime
fun loadSampleData(filename: String = "beehive.jsonld"): String {
val sampleData = ClassPathResource("/ngsild/$filename")
@@ -86,6 +87,21 @@ suspend fun loadAndExpandMinimalEntity(
contexts
)
+suspend fun loadAndExpandDeletedEntity(
+ entityId: URI,
+ deletedAt: ZonedDateTime? = ngsiLdDateTime(),
+ contexts: List = APIC_COMPOUND_CONTEXTS
+): ExpandedEntity =
+ expandJsonLdEntity(
+ """
+ {
+ "id": "$entityId",
+ "deletedAt": "$deletedAt"
+ }
+ """.trimIndent(),
+ contexts
+ )
+
suspend fun String.sampleDataToNgsiLdEntity(): Either> {
val expandedEntity = expandJsonLdEntity(this)
return when (val ngsiLdEntity = expandedEntity.toNgsiLdEntity()) {
diff --git a/shared/src/testFixtures/resources/ngsild/events/authorization/RightAddOnEntity.json b/shared/src/testFixtures/resources/ngsild/events/authorization/RightAddOnEntity.json
index 119a873df..bc05edeab 100644
--- a/shared/src/testFixtures/resources/ngsild/events/authorization/RightAddOnEntity.json
+++ b/shared/src/testFixtures/resources/ngsild/events/authorization/RightAddOnEntity.json
@@ -8,6 +8,5 @@
"contexts": [
"https://easy-global-market.github.io/ngsild-api-data-models/authorization/jsonld-contexts/authorization-compound.jsonld"
],
- "overwrite": true,
"operationType": "ATTRIBUTE_APPEND"
}
diff --git a/shared/src/testFixtures/resources/ngsild/events/authorization/SpecificAccessPolicyAddOnEntity.json b/shared/src/testFixtures/resources/ngsild/events/authorization/SpecificAccessPolicyAddOnEntity.json
index 1fd771f3d..624312332 100644
--- a/shared/src/testFixtures/resources/ngsild/events/authorization/SpecificAccessPolicyAddOnEntity.json
+++ b/shared/src/testFixtures/resources/ngsild/events/authorization/SpecificAccessPolicyAddOnEntity.json
@@ -8,6 +8,5 @@
"contexts": [
"https://easy-global-market.github.io/ngsild-api-data-models/authorization/jsonld-contexts/authorization-compound.jsonld"
],
- "overwrite": true,
"operationType": "ATTRIBUTE_APPEND"
}
diff --git a/shared/src/testFixtures/resources/ngsild/events/authorization/UserDeleteEvent.json b/shared/src/testFixtures/resources/ngsild/events/authorization/UserDeleteEvent.json
index 918acee3a..349e09b01 100644
--- a/shared/src/testFixtures/resources/ngsild/events/authorization/UserDeleteEvent.json
+++ b/shared/src/testFixtures/resources/ngsild/events/authorization/UserDeleteEvent.json
@@ -3,6 +3,7 @@
"tenantName": "urn:ngsi-ld:tenant:default",
"entityId": "urn:ngsi-ld:User:6ad19fe0-fc11-4024-85f2-931c6fa6f7e0",
"entityTypes": ["User"],
+ "updatedEntity": "{ \"https://uri.etsi.org/ngsi-ld/deletedAt\": [{ \"@type\": \"https://uri.etsi.org/ngsi-ld/DateTime\",\"@value\": \"2024-12-29T00:00:00Z\"} ], \"@id\": \"urn:ngsi-ld:User:6ad19fe0-fc11-4024-85f2-931c6fa6f7e0\" }]",
"contexts": [
"https://easy-global-market.github.io/ngsild-api-data-models/authorization/jsonld-contexts/authorization-compound.jsonld"
]
diff --git a/shared/src/testFixtures/resources/ngsild/events/entity/attributeDeleteAllInstancesEvent.json b/shared/src/testFixtures/resources/ngsild/events/entity/attributeDeleteAllInstancesEvent.json
deleted file mode 100644
index f74eabc89..000000000
--- a/shared/src/testFixtures/resources/ngsild/events/entity/attributeDeleteAllInstancesEvent.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "tenantName": "urn:ngsi-ld:tenant:default",
- "entityId" : "urn:ngsi-ld:BeeHive:01",
- "entityTypes" : [ "https://ontology.eglobalmark.com/apic#BeeHive" ],
- "attributeName" : "https://ontology.eglobalmark.com/apic#temperature",
- "updatedEntity" : "{\"@id\":\"urn:ngsi-ld:BeeHive:01\",\"@type\":[\"https://ontology.eglobalmark.com/apic#BeeHive\"],\"https://ontology.eglobalmark.com/apic#humidity\":[{\"@type\":[\"https://uri.etsi.org/ngsi-ld/Property\"],\"https://ontology.eglobalmark.com/egm#observedBy\":[{\"@type\":[\"https://uri.etsi.org/ngsi-ld/Relationship\"],\"https://uri.etsi.org/ngsi-ld/createdAt\":[{\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\",\"@value\":\"2022-02-13T08:02:50.011609Z\"}],\"https://uri.etsi.org/ngsi-ld/hasObject\":[{\"@id\":\"urn:ngsi-ld:Sensor:02\"}]}],\"https://uri.etsi.org/ngsi-ld/createdAt\":[{\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\",\"@value\":\"2022-02-13T08:02:49.982113Z\"}],\"https://uri.etsi.org/ngsi-ld/hasValue\":[{\"@value\":60}],\"https://uri.etsi.org/ngsi-ld/observedAt\":[{\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\",\"@value\":\"2019-10-26T21:32:52.986010Z\"}],\"https://uri.etsi.org/ngsi-ld/unitCode\":[{\"@value\":\"P1\"}]}],\"https://ontology.eglobalmark.com/apic#luminosity\":[{\"@type\":[\"https://uri.etsi.org/ngsi-ld/Property\"],\"https://ontology.eglobalmark.com/egm#observedBy\":[{\"@type\":[\"https://uri.etsi.org/ngsi-ld/Relationship\"],\"https://uri.etsi.org/ngsi-ld/createdAt\":[{\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\",\"@value\":\"2022-02-13T08:02:57.017715Z\"}],\"https://uri.etsi.org/ngsi-ld/hasObject\":[{\"@id\":\"urn:ngsi-ld:Sensor:02\"}]}],\"https://uri.etsi.org/ngsi-ld/createdAt\":[{\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\",\"@value\":\"2022-02-13T08:02:56.987108Z\"}],\"https://uri.etsi.org/ngsi-ld/hasValue\":[{\"@value\":120}],\"https://uri.etsi.org/ngsi-ld/observedAt\":[{\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\",\"@value\":\"2022-02-02T21:32:52.986010Z\"}],\"https://uri.etsi.org/ngsi-ld/unitCode\":[{\"@value\":\"LUX\"}]}],\"https://ontology.eglobalmark.com/egm#belongs\":[{\"@type\":[\"https://uri.etsi.org/ngsi-ld/Relationship\"],\"https://uri.etsi.org/ngsi-ld/createdAt\":[{\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\",\"@value\":\"2022-02-13T08:02:49.793453Z\"}],\"https://uri.etsi.org/ngsi-ld/hasObject\":[{\"@id\":\"urn:ngsi-ld:Apiary:01\"}]}],\"https://ontology.eglobalmark.com/egm#createdBy\":[{\"@type\":[\"https://uri.etsi.org/ngsi-ld/Relationship\"],\"https://uri.etsi.org/ngsi-ld/createdAt\":[{\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\",\"@value\":\"2022-02-13T08:07:08.429628Z\"}],\"https://uri.etsi.org/ngsi-ld/hasObject\":[{\"@id\":\"urn:ngsi-ld:Craftman:01\"}]}],\"https://ontology.eglobalmark.com/egm#managedBy\":[{\"@type\":[\"https://uri.etsi.org/ngsi-ld/Relationship\"],\"https://uri.etsi.org/ngsi-ld/createdAt\":[{\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\",\"@value\":\"2022-02-13T14:00:45.114035Z\"}],\"https://uri.etsi.org/ngsi-ld/hasObject\":[{\"@id\":\"urn:ngsi-ld:Beekeeper:02\"}],\"https://uri.etsi.org/ngsi-ld/modifiedAt\":[{\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\",\"@value\":\"2022-02-13T14:10:16.963869Z\"}]}],\"https://schema.org/name\":[{\"@type\":[\"https://uri.etsi.org/ngsi-ld/Property\"],\"https://uri.etsi.org/ngsi-ld/createdAt\":[{\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\",\"@value\":\"2022-02-13T14:14:03.758379Z\"}],\"https://uri.etsi.org/ngsi-ld/hasValue\":[{\"@value\":\"Beehive - Biot\"}]}],\"https://uri.etsi.org/ngsi-ld/createdAt\":[{\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\",\"@value\":\"2022-02-13T08:02:49.359316Z\"}],\"https://uri.etsi.org/ngsi-ld/location\":[{\"@type\":[\"https://uri.etsi.org/ngsi-ld/GeoProperty\"],\"https://uri.etsi.org/ngsi-ld/hasValue\":[{\"@value\":\"POINT (24.30623 60.07966)\"}]}],\"https://uri.etsi.org/ngsi-ld/modifiedAt\":[{\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\",\"@value\":\"2022-02-13T14:14:03.812339Z\"}]}",
- "contexts" : [],
- "operationType" : "ATTRIBUTE_DELETE_ALL_INSTANCES"
-}
diff --git a/shared/src/testFixtures/resources/ngsild/events/entity/entityDeleteEvent.json b/shared/src/testFixtures/resources/ngsild/events/entity/entityDeleteEvent.json
index ed92fc60c..5b381132a 100644
--- a/shared/src/testFixtures/resources/ngsild/events/entity/entityDeleteEvent.json
+++ b/shared/src/testFixtures/resources/ngsild/events/entity/entityDeleteEvent.json
@@ -3,7 +3,8 @@
"tenantName": "urn:ngsi-ld:tenant:default",
"entityId" : "urn:ngsi-ld:BeeHive:01",
"entityTypes" : [ "https://ontology.eglobalmark.com/apic#BeeHive" ],
- "deletedEntity":"{\"@id\":\"urn:ngsi-ld:BeeHive:01\",\"@type\":[\"https://ontology.eglobalmark.com/apic#BeeHive\"],\"https://ontology.eglobalmark.com/apic#humidity\":[{\"@type\":[\"https://uri.etsi.org/ngsi-ld/Property\"],\"https://ontology.eglobalmark.com/egm#observedBy\":[{\"@type\":[\"https://uri.etsi.org/ngsi-ld/Relationship\"],\"https://uri.etsi.org/ngsi-ld/createdAt\":[{\"@value\":\"2022-02-12T08:36:59.455870Z\",\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\"}],\"https://uri.etsi.org/ngsi-ld/hasObject\":[{\"@id\":\"urn:ngsi-ld:Sensor:02\"}]}],\"https://uri.etsi.org/ngsi-ld/createdAt\":[{\"@value\":\"2022-02-12T08:36:59.448205Z\",\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\"}],\"https://uri.etsi.org/ngsi-ld/hasValue\":[{\"@value\":60}],\"https://uri.etsi.org/ngsi-ld/observedAt\":[{\"@value\":\"2019-10-26T21:32:52.986010Z\",\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\"}],\"https://uri.etsi.org/ngsi-ld/unitCode\":[{\"@value\":\"P1\"}]}],\"https://ontology.eglobalmark.com/apic#temperature\":[{\"@type\":[\"https://uri.etsi.org/ngsi-ld/Property\"],\"https://ontology.eglobalmark.com/egm#observedBy\":[{\"@type\":[\"https://uri.etsi.org/ngsi-ld/Relationship\"],\"https://uri.etsi.org/ngsi-ld/createdAt\":[{\"@value\":\"2022-02-12T08:36:59.473904Z\",\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\"}],\"https://uri.etsi.org/ngsi-ld/hasObject\":[{\"@id\":\"urn:ngsi-ld:Sensor:01\"}]}],\"https://uri.etsi.org/ngsi-ld/createdAt\":[{\"@value\":\"2022-02-12T08:36:59.465937Z\",\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\"}],\"https://uri.etsi.org/ngsi-ld/hasValue\":[{\"@value\":22.2}],\"https://uri.etsi.org/ngsi-ld/observedAt\":[{\"@value\":\"2019-10-26T21:32:52.986010Z\",\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\"}],\"https://uri.etsi.org/ngsi-ld/unitCode\":[{\"@value\":\"CEL\"}]}],\"https://ontology.eglobalmark.com/egm#belongs\":[{\"@type\":[\"https://uri.etsi.org/ngsi-ld/Relationship\"],\"https://uri.etsi.org/ngsi-ld/createdAt\":[{\"@value\":\"2022-02-12T08:36:59.389815Z\",\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\"}],\"https://uri.etsi.org/ngsi-ld/hasObject\":[{\"@id\":\"urn:ngsi-ld:Apiary:01\"}]}],\"https://ontology.eglobalmark.com/egm#managedBy\":[{\"@type\":[\"https://uri.etsi.org/ngsi-ld/Relationship\"],\"https://uri.etsi.org/ngsi-ld/createdAt\":[{\"@value\":\"2022-02-12T08:36:59.417938Z\",\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\"}],\"https://uri.etsi.org/ngsi-ld/hasObject\":[{\"@id\":\"urn:ngsi-ld:Beekeeper:01\"}]}],\"https://uri.etsi.org/ngsi-ld/createdAt\":[{\"@value\":\"2022-02-12T08:36:59.179446Z\",\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\"}],\"https://uri.etsi.org/ngsi-ld/location\":[{\"@type\":[\"https://uri.etsi.org/ngsi-ld/GeoProperty\"],\"https://uri.etsi.org/ngsi-ld/hasValue\":[{\"@value\":\"POINT (24.30623 60.07966)\"}]}],\"https://uri.etsi.org/ngsi-ld/modifiedAt\":[{\"@value\":\"2022-02-12T08:36:59.218595Z\",\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\"}]}",
+ "previousEntity":"{\"@id\":\"urn:ngsi-ld:BeeHive:01\",\"@type\":[\"https://ontology.eglobalmark.com/apic#BeeHive\"],\"https://ontology.eglobalmark.com/apic#humidity\":[{\"@type\":[\"https://uri.etsi.org/ngsi-ld/Property\"],\"https://ontology.eglobalmark.com/egm#observedBy\":[{\"@type\":[\"https://uri.etsi.org/ngsi-ld/Relationship\"],\"https://uri.etsi.org/ngsi-ld/createdAt\":[{\"@value\":\"2022-02-12T08:36:59.455870Z\",\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\"}],\"https://uri.etsi.org/ngsi-ld/hasObject\":[{\"@id\":\"urn:ngsi-ld:Sensor:02\"}]}],\"https://uri.etsi.org/ngsi-ld/createdAt\":[{\"@value\":\"2022-02-12T08:36:59.448205Z\",\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\"}],\"https://uri.etsi.org/ngsi-ld/hasValue\":[{\"@value\":60}],\"https://uri.etsi.org/ngsi-ld/observedAt\":[{\"@value\":\"2019-10-26T21:32:52.986010Z\",\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\"}],\"https://uri.etsi.org/ngsi-ld/unitCode\":[{\"@value\":\"P1\"}]}],\"https://ontology.eglobalmark.com/apic#temperature\":[{\"@type\":[\"https://uri.etsi.org/ngsi-ld/Property\"],\"https://ontology.eglobalmark.com/egm#observedBy\":[{\"@type\":[\"https://uri.etsi.org/ngsi-ld/Relationship\"],\"https://uri.etsi.org/ngsi-ld/createdAt\":[{\"@value\":\"2022-02-12T08:36:59.473904Z\",\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\"}],\"https://uri.etsi.org/ngsi-ld/hasObject\":[{\"@id\":\"urn:ngsi-ld:Sensor:01\"}]}],\"https://uri.etsi.org/ngsi-ld/createdAt\":[{\"@value\":\"2022-02-12T08:36:59.465937Z\",\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\"}],\"https://uri.etsi.org/ngsi-ld/hasValue\":[{\"@value\":22.2}],\"https://uri.etsi.org/ngsi-ld/observedAt\":[{\"@value\":\"2019-10-26T21:32:52.986010Z\",\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\"}],\"https://uri.etsi.org/ngsi-ld/unitCode\":[{\"@value\":\"CEL\"}]}],\"https://ontology.eglobalmark.com/egm#belongs\":[{\"@type\":[\"https://uri.etsi.org/ngsi-ld/Relationship\"],\"https://uri.etsi.org/ngsi-ld/createdAt\":[{\"@value\":\"2022-02-12T08:36:59.389815Z\",\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\"}],\"https://uri.etsi.org/ngsi-ld/hasObject\":[{\"@id\":\"urn:ngsi-ld:Apiary:01\"}]}],\"https://ontology.eglobalmark.com/egm#managedBy\":[{\"@type\":[\"https://uri.etsi.org/ngsi-ld/Relationship\"],\"https://uri.etsi.org/ngsi-ld/createdAt\":[{\"@value\":\"2022-02-12T08:36:59.417938Z\",\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\"}],\"https://uri.etsi.org/ngsi-ld/hasObject\":[{\"@id\":\"urn:ngsi-ld:Beekeeper:01\"}]}],\"https://uri.etsi.org/ngsi-ld/createdAt\":[{\"@value\":\"2022-02-12T08:36:59.179446Z\",\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\"}],\"https://uri.etsi.org/ngsi-ld/location\":[{\"@type\":[\"https://uri.etsi.org/ngsi-ld/GeoProperty\"],\"https://uri.etsi.org/ngsi-ld/hasValue\":[{\"@value\":\"POINT (24.30623 60.07966)\"}]}],\"https://uri.etsi.org/ngsi-ld/modifiedAt\":[{\"@value\":\"2022-02-12T08:36:59.218595Z\",\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\"}]}",
+ "updatedEntity":"{\"@id\":\"urn:ngsi-ld:BeeHive:01\",\"https://uri.etsi.org/ngsi-ld/deletedAt\":[{\"@value\":\"2024-12-23T17:01:02Z\",\"@type\":\"https://uri.etsi.org/ngsi-ld/DateTime\"}]}",
"contexts" : [],
"operationType" : "ENTITY_DELETE"
}
diff --git a/subscription-service/src/main/kotlin/com/egm/stellio/subscription/listener/EntityEventListenerService.kt b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/listener/EntityEventListenerService.kt
index d4891cd73..d815ec65e 100644
--- a/subscription-service/src/main/kotlin/com/egm/stellio/subscription/listener/EntityEventListenerService.kt
+++ b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/listener/EntityEventListenerService.kt
@@ -4,7 +4,6 @@ import arrow.core.Either
import arrow.core.raise.either
import com.egm.stellio.shared.model.APIException
import com.egm.stellio.shared.model.AttributeAppendEvent
-import com.egm.stellio.shared.model.AttributeDeleteAllInstancesEvent
import com.egm.stellio.shared.model.AttributeDeleteEvent
import com.egm.stellio.shared.model.AttributeReplaceEvent
import com.egm.stellio.shared.model.AttributeUpdateEvent
@@ -19,7 +18,6 @@ import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_EXPANDED_ENTITY_CORE_MEMBE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_SYSATTRS_PROPERTIES
import com.egm.stellio.shared.util.JsonUtils.deserializeAs
import com.egm.stellio.shared.util.JsonUtils.deserializeAsMap
-import com.egm.stellio.shared.util.JsonUtils.serializeObject
import com.egm.stellio.shared.web.NGSILD_TENANT_HEADER
import com.egm.stellio.subscription.model.NotificationTrigger
import com.egm.stellio.subscription.service.NotificationService
@@ -63,51 +61,45 @@ class EntityEventListenerService(
is EntityCreateEvent -> handleEntityEvent(
tenantName,
entityEvent.operationPayload.getUpdatedAttributes(),
- entityEvent.getEntity(),
+ Pair(entityEvent.getEntity(), entityEvent.getEntity()),
NotificationTrigger.ENTITY_CREATED
)
is EntityReplaceEvent -> entityEvent.operationPayload.getUpdatedAttributes().forEach { attribute ->
handleEntityEvent(
tenantName,
setOf(attribute),
- entityEvent.getEntity(),
+ Pair(entityEvent.getEntity(), entityEvent.getEntity()),
NotificationTrigger.ATTRIBUTE_CREATED
)
}
is EntityDeleteEvent -> handleEntityEvent(
tenantName,
- entityEvent.deletedEntity?.getUpdatedAttributes() ?: emptySet(),
- entityEvent.getEntity() ?: serializeObject(emptyMap()),
+ emptySet(),
+ Pair(entityEvent.getEntity()!!, entityEvent.updatedEntity),
NotificationTrigger.ENTITY_DELETED
)
is AttributeAppendEvent -> handleEntityEvent(
tenantName,
setOf(entityEvent.attributeName),
- entityEvent.getEntity(),
+ Pair(entityEvent.getEntity(), entityEvent.getEntity()),
NotificationTrigger.ATTRIBUTE_CREATED
)
is AttributeReplaceEvent -> handleEntityEvent(
tenantName,
setOf(entityEvent.attributeName),
- entityEvent.getEntity(),
+ Pair(entityEvent.getEntity(), entityEvent.getEntity()),
NotificationTrigger.ATTRIBUTE_UPDATED
)
is AttributeUpdateEvent -> handleEntityEvent(
tenantName,
setOf(entityEvent.attributeName),
- entityEvent.getEntity(),
+ Pair(entityEvent.getEntity(), entityEvent.getEntity()),
NotificationTrigger.ATTRIBUTE_UPDATED
)
is AttributeDeleteEvent -> handleEntityEvent(
tenantName,
setOf(entityEvent.attributeName),
- entityEvent.getEntity(),
- NotificationTrigger.ATTRIBUTE_DELETED
- )
- is AttributeDeleteAllInstancesEvent -> handleEntityEvent(
- tenantName,
- setOf(entityEvent.attributeName),
- entityEvent.getEntity(),
+ Pair(entityEvent.getEntity(), entityEvent.getEntity()),
NotificationTrigger.ATTRIBUTE_DELETED
)
}
@@ -119,14 +111,15 @@ class EntityEventListenerService(
private suspend fun handleEntityEvent(
tenantName: String,
updatedAttributes: Set,
- entityPayload: String,
+ previousAndUpdatedPayloads: Pair,
notificationTrigger: NotificationTrigger
): Either = either {
logger.debug("Attributes considered in the event: {}", updatedAttributes)
- val expandedEntity = ExpandedEntity(entityPayload.deserializeAsMap())
+ val expandedEntityForMatching = ExpandedEntity(previousAndUpdatedPayloads.first.deserializeAsMap())
+ val expandedEntityForNotification = ExpandedEntity(previousAndUpdatedPayloads.second.deserializeAsMap())
mono {
notificationService.notifyMatchingSubscribers(
- expandedEntity,
+ Pair(expandedEntityForMatching, expandedEntityForNotification),
updatedAttributes,
notificationTrigger
)
@@ -139,10 +132,8 @@ class EntityEventListenerService(
else
logger.error("Error when trying to notifiy subscribers: {}", it.message, it)
}, { results ->
- val totalNotifications = results.size
- val succeeded = results.count { it.third }
- val failed = results.count { !it.third }
- logger.debug("Notified $totalNotifications subscribers (success : $succeeded / failure : $failed)")
+ val (succeeded, failed) = results.partition { it.third }.let { Pair(it.first.size, it.second.size) }
+ logger.debug("Notified ${succeeded + failed} subscribers (success : $succeeded / failure : $failed)")
})
}
}
diff --git a/subscription-service/src/main/kotlin/com/egm/stellio/subscription/service/NotificationService.kt b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/service/NotificationService.kt
index 1a054858c..ddda29ca1 100644
--- a/subscription-service/src/main/kotlin/com/egm/stellio/subscription/service/NotificationService.kt
+++ b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/service/NotificationService.kt
@@ -38,13 +38,17 @@ class NotificationService(
private val logger = LoggerFactory.getLogger(javaClass)
suspend fun notifyMatchingSubscribers(
- expandedEntity: ExpandedEntity,
+ previousAndUpdatedExpandedEntities: Pair,
updatedAttributes: Set,
notificationTrigger: NotificationTrigger
): Either>> = either {
- subscriptionService.getMatchingSubscriptions(expandedEntity, updatedAttributes, notificationTrigger).bind()
+ subscriptionService.getMatchingSubscriptions(
+ previousAndUpdatedExpandedEntities.first,
+ updatedAttributes,
+ notificationTrigger
+ ).bind()
.map {
- val filteredEntity = expandedEntity.filterAttributes(
+ val filteredEntity = previousAndUpdatedExpandedEntities.second.filterAttributes(
it.notification.attributes?.toSet().orEmpty(),
it.datasetId?.toSet().orEmpty()
)
diff --git a/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/NotificationServiceTests.kt b/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/NotificationServiceTests.kt
index 90ea23eee..83dc4fd46 100644
--- a/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/NotificationServiceTests.kt
+++ b/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/NotificationServiceTests.kt
@@ -136,7 +136,7 @@ class NotificationServiceTests {
)
notificationService.notifyMatchingSubscribers(
- expandedEntity,
+ Pair(expandedEntity, expandedEntity),
setOf(NGSILD_NAME_PROPERTY),
ATTRIBUTE_UPDATED
).shouldSucceedWith {
@@ -179,7 +179,7 @@ class NotificationServiceTests {
)
notificationService.notifyMatchingSubscribers(
- expandedEntity,
+ Pair(expandedEntity, expandedEntity),
setOf(NGSILD_NAME_PROPERTY),
ATTRIBUTE_UPDATED
).shouldSucceedWith {
@@ -224,7 +224,7 @@ class NotificationServiceTests {
)
notificationService.notifyMatchingSubscribers(
- expandedEntity,
+ Pair(expandedEntity, expandedEntity),
setOf(NGSILD_NAME_PROPERTY),
ATTRIBUTE_UPDATED
).shouldSucceedWith { notificationResults ->
@@ -265,7 +265,7 @@ class NotificationServiceTests {
)
notificationService.notifyMatchingSubscribers(
- expandedEntity,
+ Pair(expandedEntity, expandedEntity),
setOf(NGSILD_NAME_TERM),
ATTRIBUTE_UPDATED
).shouldSucceedWith { notificationResults ->
@@ -295,7 +295,7 @@ class NotificationServiceTests {
)
notificationService.notifyMatchingSubscribers(
- expandedEntity,
+ Pair(expandedEntity, expandedEntity),
setOf(NGSILD_NAME_PROPERTY),
ATTRIBUTE_UPDATED
).shouldSucceedWith {
@@ -333,7 +333,7 @@ class NotificationServiceTests {
)
notificationService.notifyMatchingSubscribers(
- expandedEntity,
+ Pair(expandedEntity, expandedEntity),
setOf(NGSILD_NAME_PROPERTY),
ATTRIBUTE_DELETED
).shouldSucceedWith {
@@ -379,7 +379,7 @@ class NotificationServiceTests {
)
notificationService.notifyMatchingSubscribers(
- expandedEntity,
+ Pair(expandedEntity, expandedEntity),
setOf(NGSILD_NAME_PROPERTY),
ATTRIBUTE_CREATED
).shouldSucceedWith { results ->
@@ -540,7 +540,7 @@ class NotificationServiceTests {
)
notificationService.notifyMatchingSubscribers(
- expandedEntity,
+ Pair(expandedEntity, expandedEntity),
setOf(NGSILD_NAME_PROPERTY),
ATTRIBUTE_UPDATED
).shouldSucceedWith {
@@ -582,7 +582,7 @@ class NotificationServiceTests {
)
notificationService.notifyMatchingSubscribers(
- expandedEntity,
+ Pair(expandedEntity, expandedEntity),
setOf(NGSILD_NAME_PROPERTY),
ATTRIBUTE_UPDATED
).shouldSucceedWith {
@@ -641,7 +641,7 @@ class NotificationServiceTests {
)
notificationService.notifyMatchingSubscribers(
- expandedEntity,
+ Pair(expandedEntity, expandedEntity),
setOf(FRIENDLYNAME_LANGUAGEPROPERTY),
ATTRIBUTE_UPDATED
).shouldSucceedWith {
diff --git a/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/SubscriptionServiceTests.kt b/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/SubscriptionServiceTests.kt
index e886eba82..1e22ab14b 100644
--- a/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/SubscriptionServiceTests.kt
+++ b/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/SubscriptionServiceTests.kt
@@ -70,7 +70,7 @@ import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.TestPropertySource
import java.net.URI
import java.time.ZonedDateTime
-import java.util.UUID
+import java.util.*
import kotlin.time.Duration
@SpringBootTest
@@ -347,8 +347,8 @@ class SubscriptionServiceTests : WithTimescaleContainer, WithKafkaContainer() {
it.notification.endpoint.uri == URI("http://localhost:8084") &&
it.notification.endpoint.accept == Endpoint.AcceptType.JSON &&
it.entities != null &&
- it.entities!!.size == 1 &&
- it.entities!!.all { entitySelector -> entitySelector.typeSelection == BEEHIVE_TYPE } &&
+ it.entities.size == 1 &&
+ it.entities.all { entitySelector -> entitySelector.typeSelection == BEEHIVE_TYPE } &&
it.watchedAttributes == null &&
it.isActive
}
@@ -383,10 +383,10 @@ class SubscriptionServiceTests : WithTimescaleContainer, WithKafkaContainer() {
it.id == "urn:ngsi-ld:Subscription:1".toUri() &&
it.subscriptionName == "A subscription with all possible members" &&
it.description == "A possible description" && it.entities != null &&
- it.entities!!.size == 3 &&
- it.entities!!.all { it.typeSelection == BEEHIVE_TYPE } &&
- it.entities!!.any { it.id == "urn:ngsi-ld:Beehive:1234567890".toUri() } &&
- it.entities!!.any { it.idPattern == "urn:ngsi-ld:Beehive:1234*" } &&
+ it.entities.size == 3 &&
+ it.entities.all { it.typeSelection == BEEHIVE_TYPE } &&
+ it.entities.any { it.id == "urn:ngsi-ld:Beehive:1234567890".toUri() } &&
+ it.entities.any { it.idPattern == "urn:ngsi-ld:Beehive:1234*" } &&
it.watchedAttributes == listOf(INCOMING_PROPERTY) &&
it.notificationTrigger == listOf(
ENTITY_CREATED.notificationTrigger,
@@ -395,11 +395,11 @@ class SubscriptionServiceTests : WithTimescaleContainer, WithKafkaContainer() {
) &&
it.timeInterval == null &&
it.q == "foodQuantity<150;foodName=='dietary fibres'" && it.geoQ != null &&
- it.geoQ!!.georel == "within" &&
- it.geoQ!!.geometry == "Polygon" &&
- it.geoQ!!.coordinates ==
+ it.geoQ.georel == "within" &&
+ it.geoQ.geometry == "Polygon" &&
+ it.geoQ.coordinates ==
"[[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]]" &&
- it.geoQ!!.geoproperty == NGSILD_LOCATION_PROPERTY &&
+ it.geoQ.geoproperty == NGSILD_LOCATION_PROPERTY &&
it.scopeQ == "/Nantes/+" &&
it.notification.attributes == listOf(INCOMING_PROPERTY, OUTGOING_PROPERTY) &&
it.notification.format == FormatType.NORMALIZED &&
@@ -438,9 +438,9 @@ class SubscriptionServiceTests : WithTimescaleContainer, WithKafkaContainer() {
assertThat(persistedSubscription)
.matches {
it.notification.lastNotification != null &&
- it.notification.lastNotification!!.isEqual(notifiedAt) &&
+ it.notification.lastNotification.isEqual(notifiedAt) &&
it.notification.lastSuccess != null &&
- it.notification.lastSuccess!!.isEqual(notifiedAt) &&
+ it.notification.lastSuccess.isEqual(notifiedAt) &&
it.notification.lastFailure == null &&
it.notification.timesSent == 1 &&
it.notification.status == StatusType.OK
@@ -788,6 +788,27 @@ class SubscriptionServiceTests : WithTimescaleContainer, WithKafkaContainer() {
}
}
+ @Test
+ fun `it should retrieve a subscription with entityDeleted trigger matched with an entity delete event`() =
+ runTest {
+ val subscription = gimmeSubscriptionFromMembers(
+ mapOf(
+ "entities" to listOf(mapOf("type" to BEEHIVE_COMPACT_TYPE)),
+ "notificationTrigger" to listOf(ENTITY_DELETED.notificationTrigger)
+ )
+ )
+ subscriptionService.create(subscription, mockUserSub).shouldSucceed()
+
+ val expandedEntity = loadAndExpandMinimalEntity("urn:ngsi-ld:Beehive:1234567890", BEEHIVE_COMPACT_TYPE)
+ subscriptionService.getMatchingSubscriptions(
+ expandedEntity,
+ emptySet(),
+ ENTITY_DELETED
+ ).shouldSucceedWith {
+ assertEquals(1, it.size)
+ }
+ }
+
@Test
fun `it should update a subscription`() = runTest {
val subscription = loadAndDeserializeSubscription("subscription_minimal_entities.json")
@@ -821,9 +842,9 @@ class SubscriptionServiceTests : WithTimescaleContainer, WithKafkaContainer() {
it.watchedAttributes!! == listOf(INCOMING_PROPERTY, TEMPERATURE_PROPERTY) &&
it.scopeQ == "/A/#,/B" &&
it.geoQ!!.georel == "equals" &&
- it.geoQ!!.geometry == "Point" &&
- it.geoQ!!.coordinates == "[100.0, 0.0]" &&
- it.geoQ!!.geoproperty == "https://uri.etsi.org/ngsi-ld/observationSpace" &&
+ it.geoQ.geometry == "Point" &&
+ it.geoQ.coordinates == "[100.0, 0.0]" &&
+ it.geoQ.geoproperty == "https://uri.etsi.org/ngsi-ld/observationSpace" &&
it.throttling == 50 &&
it.lang == "fr-CH,fr"
}
@@ -883,21 +904,21 @@ class SubscriptionServiceTests : WithTimescaleContainer, WithKafkaContainer() {
assertThat(updatedSubscription)
.matches {
it.entities != null &&
- it.entities!!.contains(
+ it.entities.contains(
EntitySelector(
id = "urn:ngsi-ld:Beehive:123".toUri(),
idPattern = null,
typeSelection = BEEHIVE_TYPE
)
) &&
- it.entities!!.contains(
+ it.entities.contains(
EntitySelector(
id = null,
idPattern = "urn:ngsi-ld:Beehive:12*",
typeSelection = BEEHIVE_TYPE
)
) &&
- it.entities!!.size == 2
+ it.entities.size == 2
}
}