From c74214a54e9f0b98c36d0a59de8c4923304c370f Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Mon, 23 Dec 2024 18:18:48 +0100 Subject: [PATCH 01/16] feat(WIP): bootstrap support for deleted entities in subscription --- .../egm/stellio/search/entity/model/Entity.kt | 28 +++++---- .../entity/service/EntityAttributeService.kt | 7 +-- .../entity/service/EntityEventService.kt | 13 ++-- .../search/entity/service/EntityService.kt | 52 ++++++++++------ .../entity/service/EntityEventServiceTests.kt | 2 +- shared/config/detekt/baseline.xml | 1 - .../egm/stellio/shared/model/EntityEvent.kt | 7 ++- .../egm/stellio/shared/util/JsonUtilsTests.kt | 8 +++ .../com/egm/stellio/shared/util/TestUtils.kt | 16 +++++ .../events/entity/entityDeleteEvent.json | 3 +- .../listener/EntityEventListenerService.kt | 32 +++++----- .../service/NotificationService.kt | 10 +++- .../service/SubscriptionServiceTests.kt | 60 +++++++++++++------ 13 files changed, 153 insertions(+), 86 deletions(-) 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..388fc8a32 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 @@ -47,17 +47,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( + entityId: URI, + deletedAt: ZonedDateTime + ): ExpandedEntity = + ExpandedEntity( + members = mapOf( + JSONLD_ID to entityId, + 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/service/EntityAttributeService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityAttributeService.kt index 72dc47c7b..6d3bf4f41 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 @@ -327,9 +327,8 @@ class EntityAttributeService( ): Either = either { if (attributesToDelete.isEmpty()) return Unit.right() val attributesToDeleteWithPayload = attributesToDelete.map { - Triple( + Pair( it, - deletedAt, JsonLdUtils.expandAttribute( it.attributeName, it.attributeType.toNullCompactedRepresentation(), @@ -347,7 +346,7 @@ class EntityAttributeService( WHERE temporal_entity_attribute.id = new.uuid """.trimIndent() ) - .bind("values", attributesToDeleteWithPayload.map { arrayOf(it.first.id, it.second, it.third.toJson()) }) + .bind("values", attributesToDeleteWithPayload.map { arrayOf(it.first.id, deletedAt, it.second.toJson()) }) .allToMappedList { Triple( toUuid(it["id"]), @@ -356,7 +355,7 @@ class EntityAttributeService( ) } - attributesToDeleteWithPayload.forEach { (attribute, deletedAt, expandedAttributePayload) -> + attributesToDeleteWithPayload.forEach { (attribute, expandedAttributePayload) -> attributeInstanceService.addDeletedAttributeInstance( attributeUuid = attribute.id, value = attribute.attributeType.toNullValue(), 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..ac6966807 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 @@ -17,6 +17,7 @@ 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.ExpandedEntity import com.egm.stellio.shared.model.ExpandedTerm import com.egm.stellio.shared.model.getAttributeFromExpandedAttributes import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_TYPE @@ -84,18 +85,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() ) ) 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..c239f992f 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 @@ -568,36 +568,48 @@ class EntityService( authorizationService.userCanAdminEntity(entityId, sub.toOption()).bind() val deletedAt = ngsiLdDateTime() - val entity = deleteEntityPayload(entityId, deletedAt).bind() + // TODO retrieve entity when checking for existence? + val currentEntity = entityQueryService.retrieve(entityId).bind() + val deletedEntityPayload = currentEntity.toExpandedDeletedEntity(entityId, 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 @@ -605,16 +617,19 @@ class EntityService( entityQueryService.checkEntityExistence(entityId, true).bind() authorizationService.userCanAdminEntity(entityId, sub.toOption()).bind() - val entity = permanentyDeleteEntityPayload(entityId).bind() + // TODO retrieve entity when checking for existence? + val currentEntity = entityQueryService.retrieve(entityId).bind() + val deletedEntityPayload = currentEntity.toExpandedDeletedEntity(entityId, ngsiLdDateTime()) + val previousEntity = permanentyDeleteEntityPayload(entityId).bind() entityAttributeService.permanentlyDeleteAttributes(entityId).bind() authorizationService.removeRightsOnEntity(entityId).bind() - entityEventService.publishEntityDeleteEvent(sub, entity) + 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 +641,6 @@ class EntityService( it.rowToEntity() } .bind() - entity } @Transactional 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..5685bac72 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 @@ -135,7 +135,7 @@ class EntityEventServiceTests { } every { kafkaTemplate.send(any(), any(), any()) } returns CompletableFuture() - entityEventService.publishEntityDeleteEvent(null, entity).join() + entityEventService.publishEntityDeleteEvent(null, entity, deletedEntityPayload).join() verify { kafkaTemplate.send("cim.entity._CatchAll", breedingServiceUri.toString(), any()) } } 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/EntityEvent.kt b/shared/src/main/kotlin/com/egm/stellio/shared/model/EntityEvent.kt index 1de2e0332..998dc2cd4 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,7 +9,7 @@ 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), @@ -74,10 +74,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") 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..827d1dff3 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 @@ -19,6 +19,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 { @@ -146,6 +147,13 @@ class JsonUtilsTests { entityId, listOf(BEEHIVE_TYPE), serializeObject(expandJsonLdFragment(entityPayload, APIC_COMPOUND_CONTEXTS)), + serializeObject( + loadAndExpandDeletedEntity( + entityId.toString(), + 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..deace35ce 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( + id: String, + deletedAt: ZonedDateTime? = ngsiLdDateTime(), + contexts: List = APIC_COMPOUND_CONTEXTS +): ExpandedEntity = + expandJsonLdEntity( + """ + { + "id": "$id", + "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/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..a626ab954 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 @@ -19,7 +19,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 +62,51 @@ 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(), + Pair(entityEvent.getEntity(), entityEvent.getEntity()), NotificationTrigger.ATTRIBUTE_DELETED ) is AttributeDeleteAllInstancesEvent -> handleEntityEvent( tenantName, setOf(entityEvent.attributeName), - entityEvent.getEntity(), + Pair(entityEvent.getEntity(), entityEvent.getEntity()), NotificationTrigger.ATTRIBUTE_DELETED ) } @@ -119,14 +118,15 @@ class EntityEventListenerService( private suspend fun handleEntityEvent( tenantName: String, updatedAttributes: Set, - entityPayload: String, + entityPayload: Pair, notificationTrigger: NotificationTrigger ): Either = either { logger.debug("Attributes considered in the event: {}", updatedAttributes) - val expandedEntity = ExpandedEntity(entityPayload.deserializeAsMap()) + val expandedEntityForMatching = ExpandedEntity(entityPayload.first.deserializeAsMap()) + val expandedEntityForNotification = ExpandedEntity(entityPayload.second.deserializeAsMap()) mono { notificationService.notifyMatchingSubscribers( - expandedEntity, + Pair(expandedEntityForMatching, expandedEntityForNotification), updatedAttributes, notificationTrigger ) @@ -139,10 +139,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..21e407860 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, + expandedEntities: Pair, updatedAttributes: Set, notificationTrigger: NotificationTrigger ): Either>> = either { - subscriptionService.getMatchingSubscriptions(expandedEntity, updatedAttributes, notificationTrigger).bind() + subscriptionService.getMatchingSubscriptions( + expandedEntities.first, + updatedAttributes, + notificationTrigger + ).bind() .map { - val filteredEntity = expandedEntity.filterAttributes( + val filteredEntity = expandedEntities.second.filterAttributes( it.notification.attributes?.toSet().orEmpty(), it.datasetId?.toSet().orEmpty() ) 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..5a0f66e37 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 @@ -28,6 +28,7 @@ import com.egm.stellio.shared.util.SENSOR_COMPACT_TYPE import com.egm.stellio.shared.util.SENSOR_TYPE import com.egm.stellio.shared.util.TEMPERATURE_COMPACT_PROPERTY import com.egm.stellio.shared.util.TEMPERATURE_PROPERTY +import com.egm.stellio.shared.util.loadAndExpandDeletedEntity import com.egm.stellio.shared.util.loadAndExpandMinimalEntity import com.egm.stellio.shared.util.ngsiLdDateTime import com.egm.stellio.shared.util.shouldFail @@ -70,7 +71,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 +348,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 +384,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 +396,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 +439,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 +789,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 = loadAndExpandDeletedEntity("urn:ngsi-ld:Beehive:1234567890") + 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 +843,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 +905,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 } } From de0ebc244854f325355f4cea846e1fe855b372dc Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Sun, 29 Dec 2024 11:59:06 +0100 Subject: [PATCH 02/16] feat: support for deletedAt in subscriptions / not fully tested --- .../web/EntityAccessControlHandler.kt | 15 +- .../search/entity/model/UpdateResult.kt | 80 +++++++---- .../entity/service/EntityAttributeService.kt | 131 +++++++++--------- .../entity/service/EntityEventService.kt | 59 ++++---- .../search/entity/service/EntityService.kt | 22 ++- .../egm/stellio/search/scope/ScopeService.kt | 22 ++- .../listener/ObservationEventListenerTests.kt | 10 +- .../search/entity/model/UpdateResultTests.kt | 18 +-- .../service/EntityAttributeServiceTests.kt | 16 +-- .../entity/service/EntityEventServiceTests.kt | 70 +++------- .../entity/service/EntityServiceTests.kt | 22 +-- .../search/entity/web/EntityHandlerTests.kt | 26 ++-- .../egm/stellio/shared/model/EntityEvent.kt | 20 +-- .../stellio/shared/model/ExpandedMembers.kt | 11 +- .../egm/stellio/shared/util/JsonLdUtils.kt | 2 +- .../egm/stellio/shared/util/JsonUtilsTests.kt | 11 +- .../com/egm/stellio/shared/util/TestUtils.kt | 4 +- .../events/authorization/UserDeleteEvent.json | 1 + .../attributeDeleteAllInstancesEvent.json | 9 -- .../listener/EntityEventListenerService.kt | 13 +- .../service/NotificationService.kt | 6 +- .../service/NotificationServiceTests.kt | 20 +-- .../service/SubscriptionServiceTests.kt | 3 +- 23 files changed, 282 insertions(+), 309 deletions(-) delete mode 100644 shared/src/testFixtures/resources/ngsild/events/entity/attributeDeleteAllInstancesEvent.json 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..b9f410365 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,9 +4,10 @@ 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.OperationStatus +import com.egm.stellio.search.entity.model.SucceededAttributeOperationResult import com.egm.stellio.search.entity.model.updateResultFromDetailedResult import com.egm.stellio.search.entity.util.composeEntitiesQueryFromGet import com.egm.stellio.shared.config.ApplicationProperties @@ -257,19 +258,19 @@ 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() ) } ) 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..4cf2ede1e 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,9 +1,13 @@ 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 notUpdated: List @@ -12,7 +16,7 @@ data class UpdateResult( @JsonIgnore fun isSuccessful(): Boolean = notUpdated.isEmpty() && - updated.all { it.updateOperationResult.isSuccessResult() } + updated.all { it.operationStatus.isSuccessResult() } @JsonIgnore fun mergeWith(other: UpdateResult): UpdateResult = @@ -28,37 +32,50 @@ data class UpdateResult( val EMPTY_UPDATE_RESULT: UpdateResult = UpdateResult(emptyList(), emptyList()) +/** + * NotUpdatedDetails as defined in 5.2.19 + */ data class NotUpdatedDetails( val attributeName: String, val reason: String ) +/** + * Reflects the updated member of an UpdateResult object with additional info used internally only + */ data class UpdatedDetails( @JsonValue val attributeName: String, @JsonIgnore val datasetId: URI?, @JsonIgnore - val updateOperationResult: UpdateOperationResult + 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 - ) -} +/** + * 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 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 +83,22 @@ enum class UpdateOperationResult { IGNORED, FAILED; - fun isSuccessResult(): Boolean = listOf(APPENDED, REPLACED, UPDATED, DELETED).contains(this) -} - -fun updateResultFromDetailedResult(updateStatuses: List): UpdateResult { - val updated = updateStatuses.filter { it.isSuccessfullyUpdated() } - .map { UpdatedDetails(it.attributeName, it.datasetId, it.updateOperationResult) } - - val notUpdated = updateStatuses.filter { !it.isSuccessfullyUpdated() } - .map { NotUpdatedDetails(it.attributeName, it.errorMessage!!) } + fun isSuccessResult(): Boolean = getSuccessStatuses().contains(this) - return UpdateResult(updated, notUpdated) + companion object { + fun getSuccessStatuses(): List = listOf(APPENDED, REPLACED, UPDATED, DELETED, IGNORED) + } } + +fun updateResultFromDetailedResult(updateStatuses: List): UpdateResult = + updateStatuses.map { + when (it) { + is SucceededAttributeOperationResult -> UpdatedDetails(it.attributeName, it.datasetId, it.operationStatus) + is FailedAttributeOperationResult -> NotUpdatedDetails(it.attributeName, it.errorMessage) + } + }.let { + UpdateResult( + it.filterIsInstance(), + it.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 6d3bf4f41..f40678805 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 @@ -23,8 +23,9 @@ 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.EntitiesQuery -import com.egm.stellio.search.entity.model.UpdateAttributeResult -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.SucceededAttributeOperationResult import com.egm.stellio.search.entity.model.UpdateResult import com.egm.stellio.search.entity.model.updateResultFromDetailedResult import com.egm.stellio.search.entity.util.guessAttributeValueType @@ -48,6 +49,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 +85,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( @@ -310,22 +311,31 @@ 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 { Pair( it, @@ -337,21 +347,24 @@ class EntityAttributeService( ) } - 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, deletedAt, it.second.toJson()) }) - .allToMappedList { - Triple( - toUuid(it["id"]), - Attribute.AttributeType.valueOf(it["attribute_type"] as String), - it["attribute_name"] as String + .allToMappedList { row -> + mapOf( + toUuid(row["id"]) to Triple( + toZonedDateTime(row["created_at"]), + toOptionalZonedDateTime(row["modified_at"]), + toZonedDateTime(row["deleted_at"]) + ) ) } @@ -363,6 +376,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 @@ -608,20 +626,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( @@ -632,10 +650,11 @@ class EntityAttributeService( attributePayload, sub ).map { - UpdateAttributeResult( + SucceededAttributeOperationResult( ngsiLdAttribute.name, ngsiLdAttributeInstance.datasetId, - UpdateOperationResult.REPLACED + OperationStatus.REPLACED, + attributePayload ) }.bind() } @@ -671,10 +690,11 @@ class EntityAttributeService( attributePayload, sub ).map { - UpdateAttributeResult( + SucceededAttributeOperationResult( ngsiLdAttribute.name, ngsiLdAttributeInstance.datasetId, - UpdateOperationResult.APPENDED + OperationStatus.APPENDED, + attributePayload ) }.bind() } else if (hasNgsiLdNullValue(attributePayload, currentAttribute.attributeType)) { @@ -684,13 +704,7 @@ class EntityAttributeService( ngsiLdAttributeInstance.datasetId, false, createdAt - ).map { - UpdateAttributeResult( - ngsiLdAttribute.name, - ngsiLdAttributeInstance.datasetId, - UpdateOperationResult.DELETED - ) - }.bind() + ).bind().first() } else { partialUpdateAttribute( entityUri, @@ -698,10 +712,11 @@ class EntityAttributeService( createdAt, sub ).map { - UpdateAttributeResult( + SucceededAttributeOperationResult( ngsiLdAttribute.name, ngsiLdAttributeInstance.datasetId, - UpdateOperationResult.REPLACED + OperationStatus.REPLACED, + attributePayload ) }.bind() } @@ -721,12 +736,12 @@ class EntityAttributeService( 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)) { @@ -736,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() @@ -767,14 +776,15 @@ class EntityAttributeService( ) attributeInstanceService.create(attributeInstance).bind() - UpdateAttributeResult( + SucceededAttributeOperationResult( attributeName, datasetId, - UpdateOperationResult.UPDATED + OperationStatus.UPDATED, + updatedAttributeInstance ) } - updateResultFromDetailedResult(listOf(updateAttributeResult)) + updateResultFromDetailedResult(listOf(attributeOperationResult)) } @Transactional @@ -850,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)) @@ -863,13 +874,7 @@ class EntityAttributeService( ngsiLdAttributeInstance.datasetId, false, createdAt - ).map { - UpdateAttributeResult( - ngsiLdAttribute.name, - ngsiLdAttributeInstance.datasetId, - UpdateOperationResult.DELETED - ) - }.bind() + ).bind().first() else mergeAttribute( currentAttribute, @@ -880,10 +885,11 @@ class EntityAttributeService( attributePayload, sub ).map { - UpdateAttributeResult( + SucceededAttributeOperationResult( ngsiLdAttribute.name, ngsiLdAttributeInstance.datasetId, - UpdateOperationResult.UPDATED + OperationStatus.UPDATED, + attributePayload ) }.bind() } @@ -903,12 +909,12 @@ class EntityAttributeService( 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 { @@ -921,14 +927,15 @@ class EntityAttributeService( sub ).bind() - UpdateAttributeResult( + SucceededAttributeOperationResult( ngsiLdAttribute.name, ngsiLdAttributeInstance.datasetId, - UpdateOperationResult.REPLACED + OperationStatus.REPLACED, + expandedAttribute.second.first() ) } - updateResultFromDetailedResult(listOf(updateAttributeResult)) + updateResultFromDetailedResult(listOf(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 ac6966807..cc0abb971 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,12 @@ 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.OperationStatus +import com.egm.stellio.search.entity.model.SucceededAttributeOperationResult import com.egm.stellio.search.entity.model.UpdateResult import com.egm.stellio.search.entity.model.UpdatedDetails 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 @@ -21,6 +21,7 @@ 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 @@ -144,8 +145,8 @@ class EntityEventService( serializedAttribute: Pair, overwrite: Boolean ) { - when (updatedDetails.updateOperationResult) { - UpdateOperationResult.APPENDED -> + when (updatedDetails.operationStatus) { + OperationStatus.APPENDED -> publishEntityEvent( AttributeAppendEvent( sub, @@ -161,7 +162,7 @@ class EntityEventService( ) ) - UpdateOperationResult.REPLACED -> + OperationStatus.REPLACED -> publishEntityEvent( AttributeReplaceEvent( sub, @@ -176,7 +177,7 @@ class EntityEventService( ) ) - UpdateOperationResult.UPDATED -> + OperationStatus.UPDATED -> publishEntityEvent( AttributeUpdateEvent( sub, @@ -191,7 +192,7 @@ class EntityEventService( ) ) - UpdateOperationResult.DELETED -> + OperationStatus.DELETED -> publishEntityEvent( AttributeDeleteEvent( sub, @@ -207,7 +208,7 @@ class EntityEventService( else -> logger.warn( - "Received an unexpected result (${updatedDetails.updateOperationResult} " + + "Received an unexpected result (${updatedDetails.operationStatus} " + "for entity $entityId and attribute ${updatedDetails.attributeName}" ) } @@ -216,11 +217,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( @@ -230,31 +230,20 @@ 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() - ) + val entityPayloadWithDeletedAttribute = it.second.deserializeAsMap() + .plus(mapOf(attributeName to attributeOperationResult.newExpandedValue)) + publishEntityEvent( + AttributeDeleteEvent( + sub, + tenantName, + entityId, + it.first, + attributeName, + attributeOperationResult.datasetId, + serializeObject(entityPayloadWithDeletedAttribute), + emptyList() ) + ) }.logAttributeEvent("Attribute Delete", entityId, tenantName) } } 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 c239f992f..874dd7825 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 @@ -15,13 +15,13 @@ 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.Entity +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.util.prepareAttributes @@ -318,9 +318,10 @@ class EntityService( .map { updateResultFromDetailedResult( listOf( - UpdateAttributeResult( + SucceededAttributeOperationResult( attributeName = JSONLD_TYPE, - updateOperationResult = UpdateOperationResult.APPENDED + operationStatus = OperationStatus.APPENDED, + newExpandedValue = mapOf(JSONLD_TYPE to updatedTypes.toList()) ) ) ) @@ -653,7 +654,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( @@ -676,13 +677,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/scope/ScopeService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/scope/ScopeService.kt index 8cb37778b..906ee26f0 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,10 +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.AttributeOperationResult import com.egm.stellio.search.entity.model.NotUpdatedDetails +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.SucceededAttributeOperationResult import com.egm.stellio.search.entity.model.UpdateResult import com.egm.stellio.search.entity.model.updateResultFromDetailedResult import com.egm.stellio.search.temporal.model.AttributeInstance.TemporalProperty @@ -333,9 +334,10 @@ class ScopeService( .map { updateResultFromDetailedResult( listOf( - UpdateAttributeResult( + SucceededAttributeOperationResult( attributeName = NGSILD_SCOPE_PROPERTY, - updateOperationResult = UpdateOperationResult.APPENDED + operationStatus = OperationStatus.APPENDED, + newExpandedValue = mapOf(NGSILD_SCOPE_PROPERTY to scopes.toList()) ) ) ) @@ -348,12 +350,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 +373,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/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..7329b5b5d 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,7 +2,7 @@ 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 @@ -83,7 +83,7 @@ class ObservationEventListenerTests { UpdatedDetails( TEMPERATURE_PROPERTY, expectedTemperatureDatasetId, - UpdateOperationResult.UPDATED + OperationStatus.UPDATED ) ), notUpdated = arrayListOf() @@ -111,7 +111,7 @@ class ObservationEventListenerTests { it.updated.size == 1 && it.updated[0].attributeName == TEMPERATURE_PROPERTY && it.updated[0].datasetId == expectedTemperatureDatasetId && - it.updated[0].updateOperationResult == UpdateOperationResult.UPDATED + it.updated[0].operationStatus == OperationStatus.UPDATED }, eq(false) ) @@ -145,7 +145,7 @@ class ObservationEventListenerTests { UpdatedDetails( TEMPERATURE_PROPERTY, expectedTemperatureDatasetId, - UpdateOperationResult.APPENDED + OperationStatus.APPENDED ) ), emptyList() @@ -175,7 +175,7 @@ class ObservationEventListenerTests { }, match { it.updated.size == 1 && - it.updated[0].updateOperationResult == UpdateOperationResult.APPENDED && + it.updated[0].operationStatus == OperationStatus.APPENDED && it.updated[0].attributeName == TEMPERATURE_PROPERTY && it.updated[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..f43b26e7a 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 @@ -9,15 +9,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 @@ -26,7 +26,7 @@ class UpdateResultTests { UpdateResult( notUpdated = emptyList(), updated = listOf( - UpdatedDetails("attributeName", "urn:ngsi-ld:Entity:01".toUri(), UpdateOperationResult.UPDATED) + UpdatedDetails("attributeName", "urn:ngsi-ld:Entity:01".toUri(), OperationStatus.UPDATED) ) ) @@ -41,7 +41,7 @@ class UpdateResultTests { NotUpdatedDetails("attributeName", "attribute is malformed") ), updated = listOf( - UpdatedDetails("attributeName", "urn:ngsi-ld:Entity:01".toUri(), UpdateOperationResult.UPDATED) + UpdatedDetails("attributeName", "urn:ngsi-ld:Entity:01".toUri(), OperationStatus.UPDATED) ) ) @@ -54,8 +54,8 @@ class UpdateResultTests { 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) + UpdatedDetails("attributeName", "urn:ngsi-ld:Entity:01".toUri(), OperationStatus.UPDATED), + UpdatedDetails("attributeName", "urn:ngsi-ld:Entity:01".toUri(), OperationStatus.FAILED) ) ) 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..fb913583b 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,7 @@ 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.OperationStatus import com.egm.stellio.search.support.EMPTY_JSON_PAYLOAD import com.egm.stellio.search.support.WithKafkaContainer import com.egm.stellio.search.support.WithTimescaleContainer @@ -395,9 +395,9 @@ class EntityAttributeServiceTests : WithTimescaleContainer, WithKafkaContainer() ).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 } + assertEquals(4, updatedDetails.filter { it.operationStatus == OperationStatus.UPDATED }.size) + assertEquals(2, updatedDetails.filter { it.operationStatus == OperationStatus.APPENDED }.size) + val newAttributes = updatedDetails.filter { it.operationStatus == OperationStatus.APPENDED } .map { it.attributeName } assertTrue(newAttributes.containsAll(listOf(OUTGOING_PROPERTY, TEMPERATURE_PROPERTY))) } @@ -462,7 +462,7 @@ class EntityAttributeServiceTests : WithTimescaleContainer, WithKafkaContainer() ).shouldSucceedWith { updateResult -> val updatedDetails = updateResult.updated assertEquals(1, updatedDetails.size) - assertEquals(1, updatedDetails.filter { it.updateOperationResult == UpdateOperationResult.UPDATED }.size) + assertEquals(1, updatedDetails.filter { it.operationStatus == OperationStatus.UPDATED }.size) } coVerify(exactly = 1) { @@ -502,7 +502,7 @@ class EntityAttributeServiceTests : WithTimescaleContainer, WithKafkaContainer() ).shouldSucceedWith { updateResult -> val updatedDetails = updateResult.updated assertEquals(1, updatedDetails.size) - assertEquals(1, updatedDetails.filter { it.updateOperationResult == UpdateOperationResult.DELETED }.size) + assertEquals(1, updatedDetails.filter { it.operationStatus == OperationStatus.DELETED }.size) } coVerify(exactly = 1) { @@ -548,7 +548,7 @@ class EntityAttributeServiceTests : WithTimescaleContainer, WithKafkaContainer() ).shouldSucceedWith { updateResult -> val updatedDetails = updateResult.updated assertEquals(1, updatedDetails.size) - assertEquals(1, updatedDetails.filter { it.updateOperationResult == UpdateOperationResult.DELETED }.size) + assertEquals(1, updatedDetails.filter { it.operationStatus == OperationStatus.DELETED }.size) } coVerify(exactly = 1) { @@ -592,7 +592,7 @@ class EntityAttributeServiceTests : WithTimescaleContainer, WithKafkaContainer() ).shouldSucceedWith { updateResult -> val updatedDetails = updateResult.updated assertEquals(1, updatedDetails.size) - assertEquals(1, updatedDetails.filter { it.updateOperationResult == UpdateOperationResult.DELETED }.size) + assertEquals(1, updatedDetails.filter { it.operationStatus == OperationStatus.DELETED }.size) } coVerify(exactly = 1) { 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 5685bac72..23468b419 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,12 @@ 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.OperationStatus +import com.egm.stellio.search.entity.model.SucceededAttributeOperationResult import com.egm.stellio.search.entity.model.UpdateResult import com.egm.stellio.search.entity.model.UpdatedDetails 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,6 +16,7 @@ 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 @@ -32,6 +33,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 +137,7 @@ class EntityEventServiceTests { } every { kafkaTemplate.send(any(), any(), any()) } returns CompletableFuture() - entityEventService.publishEntityDeleteEvent(null, entity, deletedEntityPayload).join() + entityEventService.publishEntityDeleteEvent(null, entity, ExpandedEntity(emptyMap())).join() verify { kafkaTemplate.send("cim.entity._CatchAll", breedingServiceUri.toString(), any()) } } @@ -153,7 +155,7 @@ class EntityEventServiceTests { breedingServiceUri, expandedAttribute.toExpandedAttributes(), UpdateResult( - listOf(UpdatedDetails(fishNumberProperty, null, UpdateOperationResult.APPENDED)), + listOf(UpdatedDetails(fishNumberProperty, null, OperationStatus.APPENDED)), emptyList() ), true @@ -188,7 +190,7 @@ class EntityEventServiceTests { breedingServiceUri, expandedAttribute.toExpandedAttributes(), UpdateResult( - listOf(UpdatedDetails(fishNumberProperty, null, UpdateOperationResult.REPLACED)), + listOf(UpdatedDetails(fishNumberProperty, null, OperationStatus.REPLACED)), emptyList() ), true @@ -224,8 +226,8 @@ class EntityEventServiceTests { val jsonLdAttributes = expandAttributes(attributesPayload, listOf(AQUAC_COMPOUND_CONTEXT)) val appendResult = UpdateResult( listOf( - UpdatedDetails(fishNumberProperty, null, UpdateOperationResult.APPENDED), - UpdatedDetails(fishNameProperty, fishName1DatasetUri, UpdateOperationResult.REPLACED) + UpdatedDetails(fishNumberProperty, null, OperationStatus.APPENDED), + UpdatedDetails(fishNameProperty, fishName1DatasetUri, OperationStatus.REPLACED) ), emptyList() ) @@ -289,8 +291,8 @@ class EntityEventServiceTests { val jsonLdAttributes = expandAttributes(attributesPayload, listOf(AQUAC_COMPOUND_CONTEXT)) val updateResult = UpdateResult( updated = arrayListOf( - UpdatedDetails(fishNameProperty, fishName1DatasetUri, UpdateOperationResult.REPLACED), - UpdatedDetails(fishNumberProperty, null, UpdateOperationResult.REPLACED) + UpdatedDetails(fishNameProperty, fishName1DatasetUri, OperationStatus.REPLACED), + UpdatedDetails(fishNumberProperty, null, OperationStatus.REPLACED) ), notUpdated = arrayListOf() ) @@ -348,8 +350,8 @@ class EntityEventServiceTests { val jsonLdAttributes = expandAttributes(attributePayload, listOf(AQUAC_COMPOUND_CONTEXT)) val updateResult = UpdateResult( updated = arrayListOf( - UpdatedDetails(fishNameProperty, fishName1DatasetUri, UpdateOperationResult.REPLACED), - UpdatedDetails(fishNameProperty, fishName2DatasetUri, UpdateOperationResult.REPLACED) + UpdatedDetails(fishNameProperty, fishName1DatasetUri, OperationStatus.REPLACED), + UpdatedDetails(fishNameProperty, fishName2DatasetUri, OperationStatus.REPLACED) ), notUpdated = arrayListOf() ) @@ -399,7 +401,7 @@ class EntityEventServiceTests { listOf(AQUAC_COMPOUND_CONTEXT) ) val updatedDetails = listOf( - UpdatedDetails(fishNameProperty, fishName1DatasetUri, UpdateOperationResult.UPDATED) + UpdatedDetails(fishNameProperty, fishName1DatasetUri, OperationStatus.UPDATED) ) coEvery { entityQueryService.retrieve(breedingServiceUri) } returns entity.right() @@ -428,40 +430,11 @@ 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) - - entityEventService.publishAttributeDeleteEvent( - null, - breedingServiceUri, - fishNameProperty, - null, - true - ).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() - } - } - ) - } - } - @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 +442,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..be5c7396e 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 @@ -7,7 +7,7 @@ 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.OperationStatus import com.egm.stellio.search.entity.model.UpdateResult import com.egm.stellio.search.entity.model.UpdatedDetails import com.egm.stellio.search.support.WithKafkaContainer @@ -28,6 +28,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 +188,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 +216,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,7 +239,8 @@ class EntityServiceTests : WithTimescaleContainer, WithKafkaContainer() { now ).shouldSucceed() - entityService.deleteEntityPayload(entity01Uri, ngsiLdDateTime()).shouldSucceed() + entityService.deleteEntityPayload(entity01Uri, ngsiLdDateTime(), loadAndExpandDeletedEntity(entity01Uri)) + .shouldSucceed() entityQueryService.retrieve(entity01Uri) .shouldSucceedWith { entity -> @@ -259,7 +261,7 @@ class EntityServiceTests : WithTimescaleContainer, WithKafkaContainer() { coEvery { entityAttributeService.mergeAttributes(any(), any(), any(), any(), any(), any()) } returns UpdateResult( - listOf(UpdatedDetails(INCOMING_PROPERTY, null, UpdateOperationResult.APPENDED)), + listOf(UpdatedDetails(INCOMING_PROPERTY, null, OperationStatus.APPENDED)), emptyList() ).right() coEvery { entityAttributeService.getForEntity(any(), any(), any()) } returns emptyList() @@ -325,7 +327,7 @@ class EntityServiceTests : WithTimescaleContainer, WithKafkaContainer() { coEvery { entityAttributeService.mergeAttributes(any(), any(), any(), any(), any(), any()) } returns UpdateResult( - listOf(UpdatedDetails(INCOMING_PROPERTY, null, UpdateOperationResult.APPENDED)), + listOf(UpdatedDetails(INCOMING_PROPERTY, null, OperationStatus.APPENDED)), emptyList() ).right() coEvery { entityAttributeService.getForEntity(any(), any(), any()) } returns emptyList() @@ -368,7 +370,7 @@ class EntityServiceTests : WithTimescaleContainer, WithKafkaContainer() { coEvery { entityAttributeService.mergeAttributes(any(), any(), any(), any(), any(), any()) } returns UpdateResult( - listOf(UpdatedDetails(INCOMING_PROPERTY, null, UpdateOperationResult.APPENDED)), + listOf(UpdatedDetails(INCOMING_PROPERTY, null, OperationStatus.APPENDED)), emptyList() ).right() coEvery { @@ -469,7 +471,7 @@ class EntityServiceTests : WithTimescaleContainer, WithKafkaContainer() { coEvery { entityAttributeService.replaceAttribute(any(), any(), any(), any(), any()) } returns UpdateResult( - updated = listOf(UpdatedDetails(INCOMING_PROPERTY, null, UpdateOperationResult.REPLACED)), + updated = listOf(UpdatedDetails(INCOMING_PROPERTY, null, OperationStatus.REPLACED)), notUpdated = emptyList() ).right() @@ -486,7 +488,7 @@ class EntityServiceTests : WithTimescaleContainer, WithKafkaContainer() { it.updated.size == 1 && it.notUpdated.isEmpty() && it.updated[0].attributeName == INCOMING_PROPERTY && - it.updated[0].updateOperationResult == UpdateOperationResult.REPLACED + it.updated[0].operationStatus == OperationStatus.REPLACED } } @@ -507,7 +509,7 @@ class EntityServiceTests : WithTimescaleContainer, WithKafkaContainer() { assertEquals(1, it.updated.size) val updatedDetails = it.updated[0] assertEquals(JSONLD_TYPE, updatedDetails.attributeName) - assertEquals(UpdateOperationResult.APPENDED, updatedDetails.updateOperationResult) + assertEquals(OperationStatus.APPENDED, updatedDetails.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..f8c5abc89 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,7 +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.OperationStatus import com.egm.stellio.search.entity.model.UpdateResult import com.egm.stellio.search.entity.model.UpdatedDetails import com.egm.stellio.search.entity.service.EntityQueryService @@ -1411,7 +1411,7 @@ class EntityHandlerTests { UpdatedDetails( fishNumberAttribute, null, - UpdateOperationResult.APPENDED + OperationStatus.APPENDED ) ), emptyList() @@ -1448,7 +1448,7 @@ class EntityHandlerTests { UpdatedDetails( fishNumberAttribute, null, - UpdateOperationResult.APPENDED + OperationStatus.APPENDED ) ), listOf(NotUpdatedDetails(fishSizeAttribute, "overwrite disallowed")) @@ -1489,7 +1489,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(UpdatedDetails(JSONLD_TYPE, null, OperationStatus.APPENDED)), emptyList() ) @@ -1524,7 +1524,7 @@ class EntityHandlerTests { listOf(NotUpdatedDetails(JSONLD_TYPE, "Append operation has unexpectedly failed")) ) val appendResult = UpdateResult( - listOf(UpdatedDetails(fishNumberAttribute, null, UpdateOperationResult.APPENDED)), + listOf(UpdatedDetails(fishNumberAttribute, null, OperationStatus.APPENDED)), listOf(NotUpdatedDetails(fishSizeAttribute, "overwrite disallowed")) ) @@ -1650,7 +1650,7 @@ class EntityHandlerTests { val attrId = "fishNumber" val updateResult = UpdateResult( updated = arrayListOf( - UpdatedDetails(fishNumberAttribute, "urn:ngsi-ld:Dataset:1".toUri(), UpdateOperationResult.UPDATED) + UpdatedDetails(fishNumberAttribute, "urn:ngsi-ld:Dataset:1".toUri(), OperationStatus.UPDATED) ), notUpdated = arrayListOf() ) @@ -1759,12 +1759,12 @@ class EntityHandlerTests { UpdatedDetails( fishNumberAttribute, null, - UpdateOperationResult.REPLACED + OperationStatus.REPLACED ), UpdatedDetails( fishSizeAttribute, null, - UpdateOperationResult.APPENDED + OperationStatus.APPENDED ) ), notUpdated = emptyList() @@ -1796,12 +1796,12 @@ class EntityHandlerTests { UpdatedDetails( fishNumberAttribute, null, - UpdateOperationResult.REPLACED + OperationStatus.REPLACED ), UpdatedDetails( fishSizeAttribute, null, - UpdateOperationResult.APPENDED + OperationStatus.APPENDED ) ), notUpdated = emptyList() @@ -1935,7 +1935,7 @@ class EntityHandlerTests { UpdatedDetails( fishNumberAttribute, null, - UpdateOperationResult.REPLACED + OperationStatus.REPLACED ) ), notUpdated = emptyList() @@ -1970,7 +1970,7 @@ class EntityHandlerTests { entityService.updateAttributes(any(), any(), any()) } returns UpdateResult( updated = arrayListOf( - UpdatedDetails(fishNumberAttribute, null, UpdateOperationResult.REPLACED) + UpdatedDetails(fishNumberAttribute, null, OperationStatus.REPLACED) ), notUpdated = arrayListOf(notUpdatedAttribute) ).right() @@ -2355,7 +2355,7 @@ class EntityHandlerTests { entityService.replaceAttribute(any(), any(), any()) } returns UpdateResult( updated = arrayListOf( - UpdatedDetails(INCOMING_PROPERTY, null, UpdateOperationResult.REPLACED) + UpdatedDetails(INCOMING_PROPERTY, null, OperationStatus.REPLACED) ), notUpdated = emptyList() ).right() 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 998dc2cd4..290a611f0 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 @@ -16,8 +16,7 @@ import java.net.URI 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( @@ -145,20 +144,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, @@ -166,8 +151,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/util/JsonUtilsTests.kt b/shared/src/test/kotlin/com/egm/stellio/shared/util/JsonUtilsTests.kt index 827d1dff3..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 @@ -115,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( @@ -149,7 +140,7 @@ class JsonUtilsTests { serializeObject(expandJsonLdFragment(entityPayload, APIC_COMPOUND_CONTEXTS)), serializeObject( loadAndExpandDeletedEntity( - entityId.toString(), + entityId, ZonedDateTime.parse("2024-12-23T17:01:02Z"), APIC_COMPOUND_CONTEXTS ).members 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 deace35ce..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 @@ -88,14 +88,14 @@ suspend fun loadAndExpandMinimalEntity( ) suspend fun loadAndExpandDeletedEntity( - id: String, + entityId: URI, deletedAt: ZonedDateTime? = ngsiLdDateTime(), contexts: List = APIC_COMPOUND_CONTEXTS ): ExpandedEntity = expandJsonLdEntity( """ { - "id": "$id", + "id": "$entityId", "deletedAt": "$deletedAt" } """.trimIndent(), 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/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 a626ab954..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 @@ -103,12 +102,6 @@ class EntityEventListenerService( Pair(entityEvent.getEntity(), entityEvent.getEntity()), NotificationTrigger.ATTRIBUTE_DELETED ) - is AttributeDeleteAllInstancesEvent -> handleEntityEvent( - tenantName, - setOf(entityEvent.attributeName), - Pair(entityEvent.getEntity(), entityEvent.getEntity()), - NotificationTrigger.ATTRIBUTE_DELETED - ) } }.onFailure { logger.warn("Entity event processing has failed: ${it.message}", it) @@ -118,12 +111,12 @@ class EntityEventListenerService( private suspend fun handleEntityEvent( tenantName: String, updatedAttributes: Set, - entityPayload: Pair, + previousAndUpdatedPayloads: Pair, notificationTrigger: NotificationTrigger ): Either = either { logger.debug("Attributes considered in the event: {}", updatedAttributes) - val expandedEntityForMatching = ExpandedEntity(entityPayload.first.deserializeAsMap()) - val expandedEntityForNotification = ExpandedEntity(entityPayload.second.deserializeAsMap()) + val expandedEntityForMatching = ExpandedEntity(previousAndUpdatedPayloads.first.deserializeAsMap()) + val expandedEntityForNotification = ExpandedEntity(previousAndUpdatedPayloads.second.deserializeAsMap()) mono { notificationService.notifyMatchingSubscribers( Pair(expandedEntityForMatching, expandedEntityForNotification), 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 21e407860..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,17 +38,17 @@ class NotificationService( private val logger = LoggerFactory.getLogger(javaClass) suspend fun notifyMatchingSubscribers( - expandedEntities: Pair, + previousAndUpdatedExpandedEntities: Pair, updatedAttributes: Set, notificationTrigger: NotificationTrigger ): Either>> = either { subscriptionService.getMatchingSubscriptions( - expandedEntities.first, + previousAndUpdatedExpandedEntities.first, updatedAttributes, notificationTrigger ).bind() .map { - val filteredEntity = expandedEntities.second.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 5a0f66e37..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 @@ -28,7 +28,6 @@ import com.egm.stellio.shared.util.SENSOR_COMPACT_TYPE import com.egm.stellio.shared.util.SENSOR_TYPE import com.egm.stellio.shared.util.TEMPERATURE_COMPACT_PROPERTY import com.egm.stellio.shared.util.TEMPERATURE_PROPERTY -import com.egm.stellio.shared.util.loadAndExpandDeletedEntity import com.egm.stellio.shared.util.loadAndExpandMinimalEntity import com.egm.stellio.shared.util.ngsiLdDateTime import com.egm.stellio.shared.util.shouldFail @@ -800,7 +799,7 @@ class SubscriptionServiceTests : WithTimescaleContainer, WithKafkaContainer() { ) subscriptionService.create(subscription, mockUserSub).shouldSucceed() - val expandedEntity = loadAndExpandDeletedEntity("urn:ngsi-ld:Beehive:1234567890") + val expandedEntity = loadAndExpandMinimalEntity("urn:ngsi-ld:Beehive:1234567890", BEEHIVE_COMPACT_TYPE) subscriptionService.getMatchingSubscriptions( expandedEntity, emptySet(), From 6cf6f711944888cc2b391debd3eb0d178809f7e9 Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Sun, 29 Dec 2024 16:59:09 +0100 Subject: [PATCH 03/16] feat: allow existence checks and entity retrieval in one call --- .../search/entity/service/EntityQueryService.kt | 10 +++++----- .../egm/stellio/search/entity/service/EntityService.kt | 8 ++------ .../search/temporal/service/TemporalQueryService.kt | 5 ++--- .../search/entity/service/EntityServiceTests.kt | 2 +- .../temporal/service/TemporalQueryServiceTests.kt | 7 +++---- 5 files changed, 13 insertions(+), 19 deletions(-) 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 874dd7825..69c7400ad 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 @@ -565,12 +565,10 @@ 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() - // TODO retrieve entity when checking for existence? - val currentEntity = entityQueryService.retrieve(entityId).bind() val deletedEntityPayload = currentEntity.toExpandedDeletedEntity(entityId, deletedAt) val previousEntity = deleteEntityPayload(entityId, deletedAt, deletedEntityPayload).bind() entityAttributeService.deleteAttributes(entityId, deletedAt).bind() @@ -615,11 +613,9 @@ class EntityService( @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() - // TODO retrieve entity when checking for existence? - val currentEntity = entityQueryService.retrieve(entityId).bind() val deletedEntityPayload = currentEntity.toExpandedDeletedEntity(entityId, ngsiLdDateTime()) val previousEntity = permanentyDeleteEntityPayload(entityId).bind() entityAttributeService.permanentlyDeleteAttributes(entityId).bind() 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/service/EntityServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityServiceTests.kt index be5c7396e..e2ef89702 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 @@ -242,7 +242,7 @@ class EntityServiceTests : WithTimescaleContainer, WithKafkaContainer() { entityService.deleteEntityPayload(entity01Uri, ngsiLdDateTime(), loadAndExpandDeletedEntity(entity01Uri)) .shouldSucceed() - entityQueryService.retrieve(entity01Uri) + entityQueryService.retrieve(entity01Uri, true) .shouldSucceedWith { entity -> val payload = entity.payload.deserializeAsMap() assertThat(payload) 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( From 6c689fdb0b6ca7ec9c86152ef7ca109e399016a2 Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Mon, 30 Dec 2024 18:30:46 +0100 Subject: [PATCH 04/16] feat: refactor attribute operation result model --- search-service/config/detekt/baseline.xml | 3 +- .../listener/ObservationEventListener.kt | 26 ++-- .../stellio/search/entity/model/Attribute.kt | 12 +- .../egm/stellio/search/entity/model/Entity.kt | 1 + .../search/entity/model/UpdateResult.kt | 30 ++--- .../entity/service/EntityAttributeService.kt | 82 ++++++------ .../entity/service/EntityEventService.kt | 93 +++++++------- .../search/entity/service/EntityService.kt | 118 ++++++++---------- .../stellio/search/entity/util/EntityUtils.kt | 1 + .../egm/stellio/search/scope/ScopeService.kt | 43 +++---- .../listener/ObservationEventListenerTests.kt | 46 +++---- .../search/entity/model/UpdateResultTests.kt | 20 ++- .../service/EntityAttributeServiceTests.kt | 52 ++++---- .../entity/service/EntityEventServiceTests.kt | 111 ++++++++-------- .../entity/service/EntityServiceTests.kt | 40 +++--- .../search/entity/web/EntityHandlerTests.kt | 79 +++--------- .../egm/stellio/shared/model/EntityEvent.kt | 1 - 17 files changed, 334 insertions(+), 424 deletions(-) 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/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..9e74fd1f8 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,12 +28,13 @@ 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 import java.net.URI import java.time.ZonedDateTime -import java.util.UUID +import java.util.* data class Attribute( @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 388fc8a32..f3f31b4f3 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 ) { 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 4cf2ede1e..1238d6954 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 @@ -15,19 +15,7 @@ data class UpdateResult( @JsonIgnore fun isSuccessful(): Boolean = - notUpdated.isEmpty() && - updated.all { it.operationStatus.isSuccessResult() } - - @JsonIgnore - fun mergeWith(other: UpdateResult): UpdateResult = - UpdateResult( - updated = this.updated.plus(other.updated), - notUpdated = this.notUpdated.plus(other.notUpdated) - ) - - @JsonIgnore - fun hasSuccessfulUpdate(): Boolean = - this.updated.isNotEmpty() + notUpdated.isEmpty() } val EMPTY_UPDATE_RESULT: UpdateResult = UpdateResult(emptyList(), emptyList()) @@ -41,15 +29,11 @@ data class NotUpdatedDetails( ) /** - * Reflects the updated member of an UpdateResult object with additional info used internally only + * UpdatedDetails as defined in 5.2.18 */ data class UpdatedDetails( @JsonValue - val attributeName: String, - @JsonIgnore - val datasetId: URI?, - @JsonIgnore - val operationStatus: OperationStatus + val attributeName: String ) /** @@ -90,10 +74,16 @@ enum class OperationStatus { } } +fun List.hasSuccessfulResult(): Boolean = + this.any { it is SucceededAttributeOperationResult } + +fun List.getSucceededOperations(): List = + this.filterIsInstance() + fun updateResultFromDetailedResult(updateStatuses: List): UpdateResult = updateStatuses.map { when (it) { - is SucceededAttributeOperationResult -> UpdatedDetails(it.attributeName, it.datasetId, it.operationStatus) + is SucceededAttributeOperationResult -> UpdatedDetails(it.attributeName) is FailedAttributeOperationResult -> NotUpdatedDetails(it.attributeName, it.errorMessage) } }.let { 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 f40678805..553e79880 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,12 +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.FailedAttributeOperationResult 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.model.updateResultFromDetailedResult import com.egm.stellio.search.entity.util.guessAttributeValueType import com.egm.stellio.search.entity.util.hasNgsiLdNullValue import com.egm.stellio.search.entity.util.mergePatch @@ -205,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( @@ -341,7 +339,7 @@ class EntityAttributeService( it, JsonLdUtils.expandAttribute( it.attributeName, - it.attributeType.toNullCompactedRepresentation(), + it.attributeType.toNullCompactedRepresentation(it.datasetId), listOf(applicationProperties.contexts.core) ).second[0] ) @@ -605,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) @@ -659,7 +657,7 @@ class EntityAttributeService( }.bind() } } - }.fold({ it.left() }, { updateResultFromDetailedResult(it).right() }) + }.fold({ it.left() }, { it.right() }) @Transactional suspend fun updateAttributes( @@ -668,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) @@ -721,7 +719,7 @@ class EntityAttributeService( }.bind() } } - }.fold({ it.left() }, { updateResultFromDetailedResult(it).right() }) + }.fold({ it.left() }, { it.right() }) @Transactional suspend fun partialUpdateAttribute( @@ -729,7 +727,7 @@ 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) @@ -784,7 +782,7 @@ class EntityAttributeService( ) } - updateResultFromDetailedResult(listOf(attributeOperationResult)) + attributeOperationResult } @Transactional @@ -838,7 +836,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) @@ -893,7 +891,7 @@ class EntityAttributeService( ) }.bind() } - }.fold({ it.left() }, { updateResultFromDetailedResult(it).right() }) + }.fold({ it.left() }, { it.right() }) @Transactional suspend fun replaceAttribute( @@ -902,7 +900,7 @@ 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 @@ -935,7 +933,7 @@ class EntityAttributeService( ) } - updateResultFromDetailedResult(listOf(attributeOperationResult)) + 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 cc0abb971..302088712 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 @@ -4,8 +4,6 @@ import arrow.core.Either import com.egm.stellio.search.entity.model.Entity 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.model.UpdatedDetails import com.egm.stellio.shared.model.APIException import com.egm.stellio.shared.model.AttributeAppendEvent import com.egm.stellio.shared.model.AttributeDeleteEvent @@ -16,11 +14,9 @@ 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 @@ -109,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) @@ -137,15 +125,21 @@ class EntityEventService( } private fun publishAttributeChangeEvent( - updatedDetails: UpdatedDetails, sub: String?, tenantName: String, entityId: URI, entityTypesAndPayload: Pair, String>, - serializedAttribute: Pair, - overwrite: Boolean + attributeOperationResult: SucceededAttributeOperationResult ) { - when (updatedDetails.operationStatus) { + val attributeName = attributeOperationResult.attributeName + logger.debug( + "Sending {} event for attribute {} of entity {} in tenant {}", + attributeOperationResult.operationStatus, + attributeName, + entityId, + tenantName + ) + when (attributeOperationResult.operationStatus) { OperationStatus.APPENDED -> publishEntityEvent( AttributeAppendEvent( @@ -153,10 +147,9 @@ class EntityEventService( tenantName, entityId, entityTypesAndPayload.first, - serializedAttribute.first, - updatedDetails.datasetId, - overwrite, - serializedAttribute.second, + attributeOperationResult.attributeName, + attributeOperationResult.datasetId, + serializeObject(attributeOperationResult.newExpandedValue), entityTypesAndPayload.second, emptyList() ) @@ -169,9 +162,9 @@ class EntityEventService( tenantName, entityId, entityTypesAndPayload.first, - serializedAttribute.first, - updatedDetails.datasetId, - serializedAttribute.second, + attributeOperationResult.attributeName, + attributeOperationResult.datasetId, + serializeObject(attributeOperationResult.newExpandedValue), entityTypesAndPayload.second, emptyList() ) @@ -184,9 +177,9 @@ class EntityEventService( tenantName, entityId, entityTypesAndPayload.first, - serializedAttribute.first, - updatedDetails.datasetId, - serializedAttribute.second, + attributeOperationResult.attributeName, + attributeOperationResult.datasetId, + serializeObject(attributeOperationResult.newExpandedValue), entityTypesAndPayload.second, emptyList() ) @@ -199,17 +192,21 @@ class EntityEventService( tenantName, entityId, entityTypesAndPayload.first, - serializedAttribute.first, - updatedDetails.datasetId, - serializedAttribute.second, + attributeOperationResult.attributeName, + attributeOperationResult.datasetId, + injectDeletedAttribute( + entityTypesAndPayload.second, + attributeName, + attributeOperationResult.newExpandedValue + ), emptyList() ) ) else -> logger.warn( - "Received an unexpected result (${updatedDetails.operationStatus} " + - "for entity $entityId and attribute ${updatedDetails.attributeName}" + "Received an unexpected result (${attributeOperationResult.operationStatus} " + + "for entity $entityId and attribute ${attributeOperationResult.attributeName}" ) } } @@ -230,8 +227,6 @@ class EntityEventService( tenantName ) entity.onRight { - val entityPayloadWithDeletedAttribute = it.second.deserializeAsMap() - .plus(mapOf(attributeName to attributeOperationResult.newExpandedValue)) publishEntityEvent( AttributeDeleteEvent( sub, @@ -240,7 +235,7 @@ class EntityEventService( it.first, attributeName, attributeOperationResult.datasetId, - serializeObject(entityPayloadWithDeletedAttribute), + injectDeletedAttribute(it.second, attributeName, attributeOperationResult.newExpandedValue), emptyList() ) ) @@ -256,21 +251,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)) + deletedAttributeInstance: ExpandedAttributeInstance + ): String { + val entityPayload = entityPayload.deserializeAsMap().toMutableMap() + entityPayload.merge(attributeName, listOf(deletedAttributeInstance)) { 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/EntityService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityService.kt index 69c7400ad..8fece6595 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,8 +13,9 @@ 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 @@ -23,6 +24,8 @@ 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.SucceededAttributeOperationResult import com.egm.stellio.search.entity.model.UpdateResult +import com.egm.stellio.search.entity.model.getSucceededOperations +import com.egm.stellio.search.entity.model.hasSuccessfulResult import com.egm.stellio.search.entity.model.updateResultFromDetailedResult import com.egm.stellio.search.entity.util.prepareAttributes import com.egm.stellio.search.entity.util.rowToEntity @@ -37,7 +40,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 +156,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 +166,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 + updateResultFromDetailedResult(operationResult) } @Transactional @@ -264,7 +262,7 @@ class EntityService( coreAttrs: List>, modifiedAt: ZonedDateTime, operationType: OperationType - ): Either = either { + ): Either> = either { coreAttrs.map { (expandedTerm, expandedAttributeInstances) -> when (expandedTerm) { JSONLD_TYPE -> @@ -273,11 +271,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 +287,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,14 +321,10 @@ class EntityService( .bind("payload", Json.of(serializeObject(updatedPayload))) .execute() .map { - updateResultFromDetailedResult( - listOf( - SucceededAttributeOperationResult( - attributeName = JSONLD_TYPE, - operationStatus = OperationStatus.APPENDED, - newExpandedValue = mapOf(JSONLD_TYPE to updatedTypes.toList()) - ) - ) + SucceededAttributeOperationResult( + attributeName = JSONLD_TYPE, + operationStatus = OperationStatus.APPENDED, + newExpandedValue = mapOf(JSONLD_TYPE to updatedTypes.toList()) ) }.bind() } @@ -345,8 +346,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, @@ -355,24 +356,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 added - if (updateResult.hasSuccessfulUpdate()) { + if (operationResult.hasSuccessfulResult()) { val attributes = entityAttributeService.getForEntity(entityId, emptySet(), emptySet()) updateState(entityId, createdAt, attributes).bind() - } - if (updateResult.hasSuccessfulUpdate()) { entityEventService.publishAttributeChangeEvents( sub, entityId, - expandedAttributes, - updateResult, - true + operationResult.getSucceededOperations() ) } - updateResult + updateResultFromDetailedResult(operationResult) } @Transactional @@ -388,8 +385,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, @@ -397,24 +394,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 added - if (updateResult.hasSuccessfulUpdate()) { + if (operationResult.hasSuccessfulResult()) { val attributes = entityAttributeService.getForEntity(entityId, emptySet(), emptySet()) updateState(entityId, createdAt, attributes).bind() - } - if (updateResult.updated.isNotEmpty()) { entityEventService.publishAttributeChangeEvents( sub, entityId, - expandedAttributes, - updateResult, - true + operationResult.getSucceededOperations() ) } - updateResult + updateResultFromDetailedResult(operationResult) } @Transactional @@ -428,28 +421,25 @@ class EntityService( val modifiedAt = ngsiLdDateTime() - val updateResult = entityAttributeService.partialUpdateAttribute( + val operationResult = entityAttributeService.partialUpdateAttribute( entityId, expandedAttribute, modifiedAt, sub ).bind() - if (updateResult.isSuccessful()) { + if (operationResult.operationStatus.isSuccessResult()) { val attributes = entityAttributeService.getForEntity(entityId, emptySet(), emptySet()) updateState(entityId, modifiedAt, attributes).bind() - } - if (updateResult.updated.isNotEmpty()) entityEventService.publishAttributeChangeEvents( sub, entityId, - expandedAttribute.toExpandedAttributes(), - updateResult, - false + listOf(operationResult as SucceededAttributeOperationResult) ) + } - updateResult + updateResultFromDetailedResult(listOf(operationResult)) } @Transactional @@ -492,7 +482,7 @@ class EntityService( val ngsiLdAttribute = listOf(expandedAttribute).toMap().toNgsiLdAttributes().bind()[0] val replacedAt = ngsiLdDateTime() - val updateResult = entityAttributeService.replaceAttribute( + val operationResult = entityAttributeService.replaceAttribute( entityId, ngsiLdAttribute, expandedAttribute, @@ -501,21 +491,18 @@ class EntityService( ).bind() // update modifiedAt in entity if at least one attribute has been added - if (updateResult.hasSuccessfulUpdate()) { + if (operationResult.operationStatus.isSuccessResult()) { val attributes = entityAttributeService.getForEntity(entityId, emptySet(), emptySet()) updateState(entityId, replacedAt, attributes).bind() - } - if (updateResult.updated.isNotEmpty()) entityEventService.publishAttributeChangeEvents( sub, entityId, - expandedAttribute.toExpandedAttributes(), - updateResult, - false + listOf(operationResult as SucceededAttributeOperationResult) ) + } - updateResult + updateResultFromDetailedResult(listOf(operationResult)) } @Transactional @@ -616,12 +603,15 @@ class EntityService( val currentEntity = entityQueryService.retrieve(entityId, true).bind() authorizationService.userCanAdminEntity(entityId, sub.toOption()).bind() - val deletedEntityPayload = currentEntity.toExpandedDeletedEntity(entityId, ngsiLdDateTime()) val previousEntity = permanentyDeleteEntityPayload(entityId).bind() entityAttributeService.permanentlyDeleteAttributes(entityId).bind() authorizationService.removeRightsOnEntity(entityId).bind() - entityEventService.publishEntityDeleteEvent(sub, previousEntity, deletedEntityPayload) + if (currentEntity.deletedAt == null) { + // only send a notification if entity was not already previously deleted + val deletedEntityPayload = currentEntity.toExpandedDeletedEntity(entityId, ngsiLdDateTime()) + entityEventService.publishEntityDeleteEvent(sub, previousEntity, deletedEntityPayload) + } } @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 906ee26f0..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 @@ -17,12 +17,10 @@ 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.AttributeOperationResult -import com.egm.stellio.search.entity.model.NotUpdatedDetails +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.SucceededAttributeOperationResult -import com.egm.stellio.search.entity.model.UpdateResult -import com.egm.stellio.search.entity.model.updateResultFromDetailedResult 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 @@ -256,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() @@ -265,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 -> { @@ -292,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 @@ -303,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" ) } @@ -316,7 +311,7 @@ class ScopeService( scopes: List, modifiedAt: ZonedDateTime, payload: String - ): Either = either { + ): Either = either { databaseClient.sql( """ UPDATE entity_payload @@ -332,14 +327,10 @@ class ScopeService( .bind("payload", Json.of(payload)) .execute() .map { - updateResultFromDetailedResult( - listOf( - SucceededAttributeOperationResult( - attributeName = NGSILD_SCOPE_PROPERTY, - operationStatus = OperationStatus.APPENDED, - newExpandedValue = mapOf(NGSILD_SCOPE_PROPERTY to scopes.toList()) - ) - ) + SucceededAttributeOperationResult( + attributeName = NGSILD_SCOPE_PROPERTY, + operationStatus = OperationStatus.APPENDED, + newExpandedValue = mapOf(NGSILD_SCOPE_PROPERTY to scopes.toList()) ) }.bind() } 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 7329b5b5d..91a885f07 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 @@ -79,18 +79,12 @@ class ObservationEventListenerTests { coEvery { entityService.partialUpdateAttribute(any(), any(), any()) } returns UpdateResult( - updated = arrayListOf( - UpdatedDetails( - TEMPERATURE_PROPERTY, - expectedTemperatureDatasetId, - OperationStatus.UPDATED - ) - ), + updated = arrayListOf(UpdatedDetails(TEMPERATURE_PROPERTY)), notUpdated = arrayListOf() ).right() coEvery { - entityEventService.publishAttributeChangeEvents(any(), any(), any(), any(), any()) + entityEventService.publishAttributeChangeEvents(any(), any(), any()) } returns Job() observationEventListener.dispatchObservationMessage(observationEvent) @@ -106,14 +100,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].operationStatus == OperationStatus.UPDATED - }, - eq(false) + it.size == 1 && + it[0].attributeName == TEMPERATURE_PROPERTY && + it[0].datasetId == expectedTemperatureDatasetId && + it[0].operationStatus == OperationStatus.UPDATED + } ) } } @@ -141,19 +133,13 @@ class ObservationEventListenerTests { coEvery { entityService.appendAttributes(any(), any(), any(), any()) } returns UpdateResult( - listOf( - UpdatedDetails( - TEMPERATURE_PROPERTY, - expectedTemperatureDatasetId, - OperationStatus.APPENDED - ) - ), + listOf(UpdatedDetails(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 +157,11 @@ class ObservationEventListenerTests { null, eq(expectedEntityId), match { - it.containsKey(TEMPERATURE_PROPERTY) - }, - match { - it.updated.size == 1 && - it.updated[0].operationStatus == OperationStatus.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 f43b26e7a..0d36c1d72 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 @@ -25,9 +24,7 @@ class UpdateResultTests { val updateResult = UpdateResult( notUpdated = emptyList(), - updated = listOf( - UpdatedDetails("attributeName", "urn:ngsi-ld:Entity:01".toUri(), OperationStatus.UPDATED) - ) + updated = listOf(UpdatedDetails("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(), OperationStatus.UPDATED) - ) + notUpdated = listOf(NotUpdatedDetails("attributeName", "attribute is malformed")), + updated = listOf(UpdatedDetails("attributeName")) ) assertFalse(updateResult.isSuccessful()) @@ -52,10 +45,11 @@ class UpdateResultTests { fun `it should find a failed update result if an attribute update has failed`() { val updateResult = UpdateResult( - notUpdated = emptyList(), + notUpdated = listOf( + NotUpdatedDetails("failedAttributeName", "attribute does not exist") + ), updated = listOf( - UpdatedDetails("attributeName", "urn:ngsi-ld:Entity:01".toUri(), OperationStatus.UPDATED), - UpdatedDetails("attributeName", "urn:ngsi-ld:Entity:01".toUri(), OperationStatus.FAILED) + UpdatedDetails("succeededAttributeName") ) ) 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 fb913583b..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.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.operationStatus == OperationStatus.UPDATED }.size) - assertEquals(2, updatedDetails.filter { it.operationStatus == OperationStatus.APPENDED }.size) - val newAttributes = updatedDetails.filter { it.operationStatus == OperationStatus.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.operationStatus == OperationStatus.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.operationStatus == OperationStatus.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.operationStatus == OperationStatus.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.operationStatus == OperationStatus.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 23468b419..6c06cdda2 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 @@ -4,8 +4,6 @@ import arrow.core.right import com.egm.stellio.search.entity.model.Entity 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.model.UpdatedDetails import com.egm.stellio.search.support.EMPTY_PAYLOAD import com.egm.stellio.shared.model.AttributeAppendEvent import com.egm.stellio.shared.model.AttributeDeleteEvent @@ -18,7 +16,6 @@ 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 @@ -153,12 +150,13 @@ class EntityEventServiceTests { entityEventService.publishAttributeChangeEvents( "sub", breedingServiceUri, - expandedAttribute.toExpandedAttributes(), - UpdateResult( - listOf(UpdatedDetails(fishNumberProperty, null, OperationStatus.APPENDED)), - emptyList() - ), - true + listOf( + SucceededAttributeOperationResult( + attributeName = fishNumberProperty, + operationStatus = OperationStatus.APPENDED, + newExpandedValue = expandedAttribute.second[0] + ) + ) ).join() verify { @@ -188,12 +186,13 @@ class EntityEventServiceTests { entityEventService.publishAttributeChangeEvents( null, breedingServiceUri, - expandedAttribute.toExpandedAttributes(), - UpdateResult( - listOf(UpdatedDetails(fishNumberProperty, null, OperationStatus.REPLACED)), - emptyList() - ), - true + listOf( + SucceededAttributeOperationResult( + attributeName = fishNumberProperty, + operationStatus = OperationStatus.REPLACED, + newExpandedValue = expandedAttribute.second[0] + ) + ) ).join() verify { @@ -224,24 +223,24 @@ class EntityEventServiceTests { } """.trimIndent() val jsonLdAttributes = expandAttributes(attributesPayload, listOf(AQUAC_COMPOUND_CONTEXT)) - val appendResult = UpdateResult( - listOf( - UpdatedDetails(fishNumberProperty, null, OperationStatus.APPENDED), - UpdatedDetails(fishNameProperty, fishName1DatasetUri, OperationStatus.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"]( @@ -289,12 +288,18 @@ class EntityEventServiceTests { } """.trimIndent() val jsonLdAttributes = expandAttributes(attributesPayload, listOf(AQUAC_COMPOUND_CONTEXT)) - val updateResult = UpdateResult( - updated = arrayListOf( - UpdatedDetails(fishNameProperty, fishName1DatasetUri, OperationStatus.REPLACED), - UpdatedDetails(fishNumberProperty, null, OperationStatus.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() @@ -303,9 +308,7 @@ class EntityEventServiceTests { entityEventService.publishAttributeChangeEvents( null, breedingServiceUri, - jsonLdAttributes, - updateResult, - true + operationResult ).join() verify { @@ -348,24 +351,25 @@ class EntityEventServiceTests { } """.trimIndent() val jsonLdAttributes = expandAttributes(attributePayload, listOf(AQUAC_COMPOUND_CONTEXT)) - val updateResult = UpdateResult( - updated = arrayListOf( - UpdatedDetails(fishNameProperty, fishName1DatasetUri, OperationStatus.REPLACED), - UpdatedDetails(fishNameProperty, fishName2DatasetUri, OperationStatus.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"]( @@ -400,8 +404,13 @@ class EntityEventServiceTests { fishNameAttributeFragment, listOf(AQUAC_COMPOUND_CONTEXT) ) - val updatedDetails = listOf( - UpdatedDetails(fishNameProperty, fishName1DatasetUri, OperationStatus.UPDATED) + val operationResult = listOf( + SucceededAttributeOperationResult( + attributeName = fishNameProperty, + datasetId = fishName1DatasetUri, + operationStatus = OperationStatus.UPDATED, + newExpandedValue = expandedAttribute.second[0] + ) ) coEvery { entityQueryService.retrieve(breedingServiceUri) } returns entity.right() @@ -410,9 +419,7 @@ class EntityEventServiceTests { entityEventService.publishAttributeChangeEvents( null, breedingServiceUri, - expandedAttribute.toExpandedAttributes(), - UpdateResult(updatedDetails, emptyList()), - false + operationResult ).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 e2ef89702..8e9b5cbf5 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.OperationStatus -import com.egm.stellio.search.entity.model.UpdateResult -import com.egm.stellio.search.entity.model.UpdatedDetails +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 @@ -260,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, OperationStatus.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() @@ -326,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, OperationStatus.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() @@ -369,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, OperationStatus.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() @@ -470,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, OperationStatus.REPLACED)), - notUpdated = emptyList() + } returns SucceededAttributeOperationResult( + attributeName = INCOMING_PROPERTY, + operationStatus = OperationStatus.REPLACED, + newExpandedValue = emptyMap() ).right() val (jsonLdEntity, ngsiLdEntity) = loadSampleData().sampleDataToNgsiLdEntity().shouldSucceedAndResult() @@ -487,8 +480,7 @@ class EntityServiceTests : WithTimescaleContainer, WithKafkaContainer() { .shouldSucceedWith { it.updated.size == 1 && it.notUpdated.isEmpty() && - it.updated[0].attributeName == INCOMING_PROPERTY && - it.updated[0].operationStatus == OperationStatus.REPLACED + it.updated[0].attributeName == INCOMING_PROPERTY } } @@ -505,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(OperationStatus.APPENDED, updatedDetails.operationStatus) + 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 f8c5abc89..d61e4c259 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,7 +10,6 @@ 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.OperationStatus import com.egm.stellio.search.entity.model.UpdateResult import com.egm.stellio.search.entity.model.UpdatedDetails import com.egm.stellio.search.entity.service.EntityQueryService @@ -1407,13 +1406,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, - OperationStatus.APPENDED - ) - ), + listOf(UpdatedDetails(fishNumberAttribute)), emptyList() ) @@ -1444,13 +1437,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, - OperationStatus.APPENDED - ) - ), + listOf(UpdatedDetails(fishNumberAttribute)), listOf(NotUpdatedDetails(fishSizeAttribute, "overwrite disallowed")) ) @@ -1489,7 +1476,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, OperationStatus.APPENDED)), + listOf(UpdatedDetails(JSONLD_TYPE)), emptyList() ) @@ -1519,18 +1506,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, OperationStatus.APPENDED)), - listOf(NotUpdatedDetails(fishSizeAttribute, "overwrite disallowed")) - ) coEvery { entityService.appendAttributes(any(), any(), any(), any()) - } returns appendTypeResult.mergeWith(appendResult).right() + } returns UpdateResult( + updated = listOf(UpdatedDetails(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,9 +1634,7 @@ 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(), OperationStatus.UPDATED) - ), + updated = arrayListOf(UpdatedDetails(fishNumberAttribute)), notUpdated = arrayListOf() ) @@ -1756,16 +1739,8 @@ class EntityHandlerTests { val entityId = "urn:ngsi-ld:DeadFishes:019BN".toUri() val updateResult = UpdateResult( updated = arrayListOf( - UpdatedDetails( - fishNumberAttribute, - null, - OperationStatus.REPLACED - ), - UpdatedDetails( - fishSizeAttribute, - null, - OperationStatus.APPENDED - ) + UpdatedDetails(fishNumberAttribute), + UpdatedDetails(fishSizeAttribute) ), notUpdated = emptyList() ) @@ -1793,16 +1768,8 @@ class EntityHandlerTests { val entityId = "urn:ngsi-ld:DeadFishes:019BN".toUri() val updateResult = UpdateResult( updated = arrayListOf( - UpdatedDetails( - fishNumberAttribute, - null, - OperationStatus.REPLACED - ), - UpdatedDetails( - fishSizeAttribute, - null, - OperationStatus.APPENDED - ) + UpdatedDetails(fishNumberAttribute), + UpdatedDetails(fishSizeAttribute) ), notUpdated = emptyList() ) @@ -1931,13 +1898,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, - OperationStatus.REPLACED - ) - ), + updated = arrayListOf(UpdatedDetails(fishNumberAttribute)), notUpdated = emptyList() ) @@ -1969,9 +1930,7 @@ class EntityHandlerTests { coEvery { entityService.updateAttributes(any(), any(), any()) } returns UpdateResult( - updated = arrayListOf( - UpdatedDetails(fishNumberAttribute, null, OperationStatus.REPLACED) - ), + updated = arrayListOf(UpdatedDetails(fishNumberAttribute)), notUpdated = arrayListOf(notUpdatedAttribute) ).right() @@ -2354,9 +2313,7 @@ class EntityHandlerTests { coEvery { entityService.replaceAttribute(any(), any(), any()) } returns UpdateResult( - updated = arrayListOf( - UpdatedDetails(INCOMING_PROPERTY, null, OperationStatus.REPLACED) - ), + updated = arrayListOf(UpdatedDetails(INCOMING_PROPERTY)), notUpdated = emptyList() ).right() 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 290a611f0..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 @@ -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 From dc41f7d94ad8babfd6e8eecb53b3ef6052d95726 Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Mon, 30 Dec 2024 18:33:28 +0100 Subject: [PATCH 05/16] factorize some duplicate code in entity service --- .../search/entity/service/EntityService.kt | 61 +++++++------------ 1 file changed, 21 insertions(+), 40 deletions(-) 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 8fece6595..cfec315f6 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 @@ -357,17 +357,7 @@ class EntityService( ).bind() val operationResult = coreOperationResult.plus(attrsOperationResult) - // update modifiedAt in entity if at least one attribute has been added - if (operationResult.hasSuccessfulResult()) { - val attributes = entityAttributeService.getForEntity(entityId, emptySet(), emptySet()) - updateState(entityId, createdAt, attributes).bind() - - entityEventService.publishAttributeChangeEvents( - sub, - entityId, - operationResult.getSucceededOperations() - ) - } + handleSuccessOperationActions(operationResult, entityId, createdAt, sub).bind() updateResultFromDetailedResult(operationResult) } @@ -395,17 +385,7 @@ class EntityService( ).bind() val operationResult = coreOperationResult.plus(attrsOperationResult) - // update modifiedAt in entity if at least one attribute has been added - if (operationResult.hasSuccessfulResult()) { - val attributes = entityAttributeService.getForEntity(entityId, emptySet(), emptySet()) - updateState(entityId, createdAt, attributes).bind() - - entityEventService.publishAttributeChangeEvents( - sub, - entityId, - operationResult.getSucceededOperations() - ) - } + handleSuccessOperationActions(operationResult, entityId, createdAt, sub).bind() updateResultFromDetailedResult(operationResult) } @@ -426,20 +406,11 @@ class EntityService( expandedAttribute, modifiedAt, sub - ).bind() + ).bind().let { listOf(it) } - if (operationResult.operationStatus.isSuccessResult()) { - val attributes = entityAttributeService.getForEntity(entityId, emptySet(), emptySet()) - updateState(entityId, modifiedAt, attributes).bind() + handleSuccessOperationActions(operationResult, entityId, modifiedAt, sub).bind() - entityEventService.publishAttributeChangeEvents( - sub, - entityId, - listOf(operationResult as SucceededAttributeOperationResult) - ) - } - - updateResultFromDetailedResult(listOf(operationResult)) + updateResultFromDetailedResult(operationResult) } @Transactional @@ -488,21 +459,31 @@ class EntityService( expandedAttribute, replacedAt, sub - ).bind() + ).bind().let { listOf(it) } + + handleSuccessOperationActions(operationResult, entityId, replacedAt, sub).bind() + updateResultFromDetailedResult(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 (operationResult.operationStatus.isSuccessResult()) { + if (operationResult.hasSuccessfulResult()) { val attributes = entityAttributeService.getForEntity(entityId, emptySet(), emptySet()) - updateState(entityId, replacedAt, attributes).bind() + updateState(entityId, createdAt, attributes).bind() entityEventService.publishAttributeChangeEvents( sub, entityId, - listOf(operationResult as SucceededAttributeOperationResult) + operationResult.getSucceededOperations() ) } - - updateResultFromDetailedResult(listOf(operationResult)) } @Transactional From 3c3b1adf0115ca6abd1f7f0363f9e8a88037cec3 Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Tue, 31 Dec 2024 07:28:25 +0100 Subject: [PATCH 06/16] test --- .../search/entity/listener/ObservationEventListenerTests.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 91a885f07..07faaef34 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 @@ -8,7 +8,6 @@ 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 @@ -49,7 +48,7 @@ class ObservationEventListenerTests { 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 +56,7 @@ class ObservationEventListenerTests { coVerify { entityService.createEntity( - any(), + any(), any(), eq("0123456789-1234-5678-987654321") ) From 50f6d15cc49ee71df6c868504a0014e40f5cd855 Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Tue, 31 Dec 2024 09:06:36 +0100 Subject: [PATCH 07/16] minor refactoring and tests --- .../entity/service/EntityEventService.kt | 21 +++++------ .../entity/service/EntityEventServiceTests.kt | 37 +++++++++++++++++++ .../shared/model/ExpandedMembersTests.kt | 16 ++++++++ 3 files changed, 62 insertions(+), 12 deletions(-) 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 302088712..c67874b8a 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 @@ -132,6 +132,7 @@ class EntityEventService( attributeOperationResult: SucceededAttributeOperationResult ) { val attributeName = attributeOperationResult.attributeName + val (types, payload) = entityTypesAndPayload logger.debug( "Sending {} event for attribute {} of entity {} in tenant {}", attributeOperationResult.operationStatus, @@ -146,11 +147,11 @@ class EntityEventService( sub, tenantName, entityId, - entityTypesAndPayload.first, + types, attributeOperationResult.attributeName, attributeOperationResult.datasetId, serializeObject(attributeOperationResult.newExpandedValue), - entityTypesAndPayload.second, + payload, emptyList() ) ) @@ -161,11 +162,11 @@ class EntityEventService( sub, tenantName, entityId, - entityTypesAndPayload.first, + types, attributeOperationResult.attributeName, attributeOperationResult.datasetId, serializeObject(attributeOperationResult.newExpandedValue), - entityTypesAndPayload.second, + payload, emptyList() ) ) @@ -176,11 +177,11 @@ class EntityEventService( sub, tenantName, entityId, - entityTypesAndPayload.first, + types, attributeOperationResult.attributeName, attributeOperationResult.datasetId, serializeObject(attributeOperationResult.newExpandedValue), - entityTypesAndPayload.second, + payload, emptyList() ) ) @@ -191,14 +192,10 @@ class EntityEventService( sub, tenantName, entityId, - entityTypesAndPayload.first, + types, attributeOperationResult.attributeName, attributeOperationResult.datasetId, - injectDeletedAttribute( - entityTypesAndPayload.second, - attributeName, - attributeOperationResult.newExpandedValue - ), + injectDeletedAttribute(payload, attributeName, attributeOperationResult.newExpandedValue), emptyList() ) ) 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 6c06cdda2..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 @@ -437,6 +437,43 @@ class EntityEventServiceTests { } } + @Test + 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.publishAttributeChangeEvents( + null, + breedingServiceUri, + listOf( + SucceededAttributeOperationResult( + fishNameProperty, + fishName1DatasetUri, + OperationStatus.DELETED, + emptyMap() + ) + ) + ).join() + + 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).apply { 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 From 3ebeafa2f1147cf9f1c9a008a95b276d2f3113f4 Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Tue, 31 Dec 2024 09:52:50 +0100 Subject: [PATCH 08/16] fix: improve check on time --- .../search/temporal/service/AttributeInstanceServiceTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..46fc3c76d 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 @@ -767,7 +767,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer verify { attributeInstanceService["create"]( match { - it.time == deletedAt && + it.time.isEqual(deletedAt) && it.value == "urn:ngsi-ld:null" && it.measuredValue == null && it.payload.asString().matchContent( From 5b765d515c19cd9b803497ac25c336be0db4794e Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Tue, 31 Dec 2024 15:54:52 +0100 Subject: [PATCH 09/16] misc spec alignments --- .../kotlin/com/egm/stellio/search/entity/model/Entity.kt | 2 +- .../egm/stellio/search/entity/service/EntityService.kt | 4 ++-- .../temporal/service/AttributeInstanceServiceTests.kt | 3 ++- .../com/egm/stellio/shared/model/CompactedEntity.kt | 8 +++++--- 4 files changed, 10 insertions(+), 7 deletions(-) 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 f3f31b4f3..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 @@ -49,12 +49,12 @@ data class Entity( } fun toExpandedDeletedEntity( - entityId: URI, 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 { 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 cfec315f6..6a667d570 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 @@ -537,7 +537,7 @@ class EntityService( authorizationService.userCanAdminEntity(entityId, sub.toOption()).bind() val deletedAt = ngsiLdDateTime() - val deletedEntityPayload = currentEntity.toExpandedDeletedEntity(entityId, deletedAt) + 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() @@ -590,7 +590,7 @@ class EntityService( if (currentEntity.deletedAt == null) { // only send a notification if entity was not already previously deleted - val deletedEntityPayload = currentEntity.toExpandedDeletedEntity(entityId, ngsiLdDateTime()) + val deletedEntityPayload = currentEntity.toExpandedDeletedEntity(ngsiLdDateTime()) entityEventService.publishEntityDeleteEvent(sub, previousEntity, deletedEntityPayload) } } 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 46fc3c76d..ebac9c943 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 @@ -61,6 +61,7 @@ import com.egm.stellio.shared.util.ngsiLdDateTime import com.egm.stellio.shared.util.shouldFail import com.egm.stellio.shared.util.shouldSucceed import com.egm.stellio.shared.util.shouldSucceedWith +import com.egm.stellio.shared.util.toNgsiLdFormat import com.egm.stellio.shared.util.toUri import io.mockk.spyk import io.mockk.verify @@ -774,7 +775,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer """ { "https://uri.etsi.org/ngsi-ld/deletedAt":[{ - "@value":"$deletedAt", + "@value":"${deletedAt.toNgsiLdFormat()}", "@type":"https://uri.etsi.org/ngsi-ld/DateTime" }], "https://uri.etsi.org/ngsi-ld/hasValue":[{ 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<*, *>) From 949cf30cf38679751ae87052e8cbcf81ff16face Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Wed, 1 Jan 2025 17:42:11 +0100 Subject: [PATCH 10/16] chore: remove overwrite from test payloads --- .../resources/ngsild/events/authorization/RightAddOnEntity.json | 1 - .../events/authorization/SpecificAccessPolicyAddOnEntity.json | 1 - 2 files changed, 2 deletions(-) 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" } From 6304252cbb5feb1e8015cc6d9176c5242ac91def Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Wed, 1 Jan 2025 17:55:53 +0100 Subject: [PATCH 11/16] fix: update entity attributes should replace existing attribute --- .../search/entity/service/EntityAttributeService.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 553e79880..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 @@ -704,10 +704,12 @@ class EntityAttributeService( createdAt ).bind().first() } else { - partialUpdateAttribute( - entityUri, - Pair(ngsiLdAttribute.name, listOf(attributePayload)), + replaceAttribute( + currentAttribute, + ngsiLdAttribute, + attributeMetadata, createdAt, + attributePayload, sub ).map { SucceededAttributeOperationResult( From a147584f2c9d96162658bb01af664e713efbd9c5 Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Thu, 2 Jan 2025 06:55:00 +0100 Subject: [PATCH 12/16] fix: flaky time comparisons --- .../temporal/service/AttributeInstanceServiceTests.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 ebac9c943..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 @@ -61,7 +61,6 @@ import com.egm.stellio.shared.util.ngsiLdDateTime import com.egm.stellio.shared.util.shouldFail import com.egm.stellio.shared.util.shouldSucceed import com.egm.stellio.shared.util.shouldSucceedWith -import com.egm.stellio.shared.util.toNgsiLdFormat import com.egm.stellio.shared.util.toUri import io.mockk.spyk import io.mockk.verify @@ -743,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( @@ -768,14 +767,14 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer verify { attributeInstanceService["create"]( match { - it.time.isEqual(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.toNgsiLdFormat()}", + "@value":"2025-01-02T11:20:30.000001Z", "@type":"https://uri.etsi.org/ngsi-ld/DateTime" }], "https://uri.etsi.org/ngsi-ld/hasValue":[{ From f5d3517840389196cc2e7ec1536bd401ac213add Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Thu, 2 Jan 2025 07:12:42 +0100 Subject: [PATCH 13/16] try enforcing verifications --- .../search/entity/listener/ObservationEventListenerTests.kt | 3 +++ 1 file changed, 3 insertions(+) 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 07faaef34..50710abdc 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 @@ -122,6 +122,9 @@ class ObservationEventListenerTests { observationEventListener.dispatchObservationMessage(observationEvent) + coVerify { + entityService.partialUpdateAttribute(any(), any(), any()) + } verify { entityEventService wasNot called } } From f418af49913f67a2beb8a1dee41e6eba49221e68 Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Thu, 2 Jan 2025 08:07:20 +0100 Subject: [PATCH 14/16] yet another try --- .../entity/listener/ObservationEventListenerTests.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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 50710abdc..485a44001 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 @@ -7,20 +7,19 @@ 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.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 @@ -43,6 +42,11 @@ class ObservationEventListenerTests { 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") @@ -138,8 +142,6 @@ class ObservationEventListenerTests { listOf(UpdatedDetails(TEMPERATURE_PROPERTY)), emptyList() ).right() - val mockedExpandedEntity = mockkClass(ExpandedEntity::class, relaxed = true) - every { mockedExpandedEntity.types } returns listOf(BEEHIVE_TYPE) coEvery { entityEventService.publishAttributeChangeEvents(any(), any(), any()) } returns Job() From 4690cf2ad5dfaab87a1407063d2f67813309ffca Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Fri, 3 Jan 2025 17:20:51 +0100 Subject: [PATCH 15/16] chore: handle PR comments --- .../web/EntityAccessControlHandler.kt | 4 +- .../stellio/search/entity/model/Attribute.kt | 2 +- .../search/entity/model/UpdateResult.kt | 40 ++++++++----------- .../entity/service/EntityEventService.kt | 4 +- .../search/entity/service/EntityService.kt | 11 +++-- .../listener/ObservationEventListenerTests.kt | 7 ++-- .../search/entity/model/UpdateResultTests.kt | 8 ++-- .../entity/service/EntityServiceTests.kt | 2 +- .../search/entity/web/EntityHandlerTests.kt | 33 ++++++--------- 9 files changed, 47 insertions(+), 64 deletions(-) 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 b9f410365..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 @@ -8,7 +8,7 @@ import com.egm.stellio.search.entity.model.FailedAttributeOperationResult import com.egm.stellio.search.entity.model.NotUpdatedDetails import com.egm.stellio.search.entity.model.OperationStatus import com.egm.stellio.search.entity.model.SucceededAttributeOperationResult -import com.egm.stellio.search.entity.model.updateResultFromDetailedResult +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 @@ -275,7 +275,7 @@ class EntityAccessControlHandler( } ) } - 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/model/Attribute.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/Attribute.kt index 9e74fd1f8..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 @@ -34,7 +34,7 @@ import io.r2dbc.postgresql.codec.Json import org.springframework.data.annotation.Id import java.net.URI import java.time.ZonedDateTime -import java.util.* +import java.util.UUID data class Attribute( @Id 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 1238d6954..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 @@ -2,20 +2,35 @@ 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() + + companion object { + + 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()) @@ -28,14 +43,6 @@ data class NotUpdatedDetails( val reason: String ) -/** - * UpdatedDetails as defined in 5.2.18 - */ -data class UpdatedDetails( - @JsonValue - val attributeName: String -) - /** * Internal structure used to convey the result of an operation (update, delete...) */ @@ -79,16 +86,3 @@ fun List.hasSuccessfulResult(): Boolean = fun List.getSucceededOperations(): List = this.filterIsInstance() - -fun updateResultFromDetailedResult(updateStatuses: List): UpdateResult = - updateStatuses.map { - when (it) { - is SucceededAttributeOperationResult -> UpdatedDetails(it.attributeName) - is FailedAttributeOperationResult -> NotUpdatedDetails(it.attributeName, it.errorMessage) - } - }.let { - UpdateResult( - it.filterIsInstance(), - it.filterIsInstance() - ) - } 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 c67874b8a..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 @@ -251,10 +251,10 @@ class EntityEventService( internal fun injectDeletedAttribute( entityPayload: String, attributeName: ExpandedTerm, - deletedAttributeInstance: ExpandedAttributeInstance + attributeInstance: ExpandedAttributeInstance ): String { val entityPayload = entityPayload.deserializeAsMap().toMutableMap() - entityPayload.merge(attributeName, listOf(deletedAttributeInstance)) { currentValue, newValue -> + entityPayload.merge(attributeName, listOf(attributeInstance)) { currentValue, newValue -> (currentValue as List).plus(newValue as List) } 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 6a667d570..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 @@ -26,7 +26,6 @@ import com.egm.stellio.search.entity.model.SucceededAttributeOperationResult import com.egm.stellio.search.entity.model.UpdateResult import com.egm.stellio.search.entity.model.getSucceededOperations import com.egm.stellio.search.entity.model.hasSuccessfulResult -import com.egm.stellio.search.entity.model.updateResultFromDetailedResult import com.egm.stellio.search.entity.util.prepareAttributes import com.egm.stellio.search.entity.util.rowToEntity import com.egm.stellio.search.scope.ScopeService @@ -179,7 +178,7 @@ class EntityService( ) } - updateResultFromDetailedResult(operationResult) + UpdateResult(operationResult) } @Transactional @@ -359,7 +358,7 @@ class EntityService( val operationResult = coreOperationResult.plus(attrsOperationResult) handleSuccessOperationActions(operationResult, entityId, createdAt, sub).bind() - updateResultFromDetailedResult(operationResult) + UpdateResult(operationResult) } @Transactional @@ -387,7 +386,7 @@ class EntityService( val operationResult = coreOperationResult.plus(attrsOperationResult) handleSuccessOperationActions(operationResult, entityId, createdAt, sub).bind() - updateResultFromDetailedResult(operationResult) + UpdateResult(operationResult) } @Transactional @@ -410,7 +409,7 @@ class EntityService( handleSuccessOperationActions(operationResult, entityId, modifiedAt, sub).bind() - updateResultFromDetailedResult(operationResult) + UpdateResult(operationResult) } @Transactional @@ -463,7 +462,7 @@ class EntityService( handleSuccessOperationActions(operationResult, entityId, replacedAt, sub).bind() - updateResultFromDetailedResult(operationResult) + UpdateResult(operationResult) } @Transactional 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 485a44001..f7ceb5fef 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 @@ -4,7 +4,6 @@ import arrow.core.right import com.egm.stellio.search.entity.model.NotUpdatedDetails 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.util.BEEHIVE_TYPE @@ -82,8 +81,8 @@ class ObservationEventListenerTests { coEvery { entityService.partialUpdateAttribute(any(), any(), any()) } returns UpdateResult( - updated = arrayListOf(UpdatedDetails(TEMPERATURE_PROPERTY)), - notUpdated = arrayListOf() + updated = listOf(TEMPERATURE_PROPERTY), + notUpdated = emptyList() ).right() coEvery { @@ -139,7 +138,7 @@ class ObservationEventListenerTests { coEvery { entityService.appendAttributes(any(), any(), any(), any()) } returns UpdateResult( - listOf(UpdatedDetails(TEMPERATURE_PROPERTY)), + listOf(TEMPERATURE_PROPERTY), emptyList() ).right() coEvery { 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 0d36c1d72..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 @@ -24,7 +24,7 @@ class UpdateResultTests { val updateResult = UpdateResult( notUpdated = emptyList(), - updated = listOf(UpdatedDetails("attributeName")) + updated = listOf("attributeName") ) assertTrue(updateResult.isSuccessful()) @@ -35,7 +35,7 @@ class UpdateResultTests { val updateResult = UpdateResult( notUpdated = listOf(NotUpdatedDetails("attributeName", "attribute is malformed")), - updated = listOf(UpdatedDetails("attributeName")) + updated = listOf("attributeName") ) assertFalse(updateResult.isSuccessful()) @@ -48,9 +48,7 @@ class UpdateResultTests { notUpdated = listOf( NotUpdatedDetails("failedAttributeName", "attribute does not exist") ), - updated = listOf( - UpdatedDetails("succeededAttributeName") - ) + updated = listOf("succeededAttributeName") ) assertFalse(updateResult.isSuccessful()) 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 8e9b5cbf5..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 @@ -480,7 +480,7 @@ class EntityServiceTests : WithTimescaleContainer, WithKafkaContainer() { .shouldSucceedWith { it.updated.size == 1 && it.notUpdated.isEmpty() && - it.updated[0].attributeName == INCOMING_PROPERTY + it.updated[0] == INCOMING_PROPERTY } } 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 d61e4c259..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 @@ -11,7 +11,6 @@ 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.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 @@ -1289,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) } } @@ -1406,7 +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)), + listOf(fishNumberAttribute), emptyList() ) @@ -1437,7 +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)), + listOf(fishNumberAttribute), listOf(NotUpdatedDetails(fishSizeAttribute, "overwrite disallowed")) ) @@ -1476,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)), + listOf(JSONLD_TYPE), emptyList() ) @@ -1510,7 +1509,7 @@ class EntityHandlerTests { coEvery { entityService.appendAttributes(any(), any(), any(), any()) } returns UpdateResult( - updated = listOf(UpdatedDetails(fishNumberAttribute)), + updated = listOf(fishNumberAttribute), notUpdated = listOf( NotUpdatedDetails(JSONLD_TYPE, "Append operation has unexpectedly failed"), NotUpdatedDetails(fishSizeAttribute, "overwrite disallowed") @@ -1634,8 +1633,8 @@ class EntityHandlerTests { val entityId = "urn:ngsi-ld:DeadFishes:019BN".toUri() val attrId = "fishNumber" val updateResult = UpdateResult( - updated = arrayListOf(UpdatedDetails(fishNumberAttribute)), - notUpdated = arrayListOf() + updated = listOf(fishNumberAttribute), + notUpdated = emptyList() ) coEvery { @@ -1738,10 +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), - UpdatedDetails(fishSizeAttribute) - ), + updated = listOf(fishNumberAttribute, fishSizeAttribute), notUpdated = emptyList() ) @@ -1767,10 +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), - UpdatedDetails(fishSizeAttribute) - ), + updated = listOf(fishNumberAttribute, fishSizeAttribute), notUpdated = emptyList() ) @@ -1898,7 +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)), + updated = listOf(fishNumberAttribute), notUpdated = emptyList() ) @@ -1930,8 +1923,8 @@ class EntityHandlerTests { coEvery { entityService.updateAttributes(any(), any(), any()) } returns UpdateResult( - updated = arrayListOf(UpdatedDetails(fishNumberAttribute)), - notUpdated = arrayListOf(notUpdatedAttribute) + updated = listOf(fishNumberAttribute), + notUpdated = listOf(notUpdatedAttribute) ).right() webClient.patch() @@ -2313,7 +2306,7 @@ class EntityHandlerTests { coEvery { entityService.replaceAttribute(any(), any(), any()) } returns UpdateResult( - updated = arrayListOf(UpdatedDetails(INCOMING_PROPERTY)), + updated = listOf(INCOMING_PROPERTY), notUpdated = emptyList() ).right() From edc7b7c32e7e73def03b0f5e690a22c8af31190b Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Fri, 3 Jan 2025 18:12:30 +0100 Subject: [PATCH 16/16] try setting a timeout in observation listener test --- .../search/entity/listener/ObservationEventListenerTests.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 f7ceb5fef..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 @@ -32,10 +32,10 @@ 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() @@ -91,7 +91,7 @@ class ObservationEventListenerTests { observationEventListener.dispatchObservationMessage(observationEvent) - coVerify { + coVerify(timeout = 1000L) { entityService.partialUpdateAttribute( expectedEntityId, match { it.first == TEMPERATURE_PROPERTY },