diff --git a/build.gradle.kts b/build.gradle.kts
index 5b1074a5e..4436971f4 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -79,6 +79,7 @@ subprojects {
runtimeOnly("io.micrometer:micrometer-registry-prometheus")
testImplementation("org.springframework.boot:spring-boot-starter-test")
+ testImplementation("org.springframework.boot:spring-boot-testcontainers")
testImplementation("io.projectreactor:reactor-test")
testImplementation("com.ninja-squad:springmockk:4.0.2")
testImplementation("org.springframework.security:spring-security-test")
diff --git a/search-service/config/detekt/baseline.xml b/search-service/config/detekt/baseline.xml
index 7f380d5b4..f467d3c62 100644
--- a/search-service/config/detekt/baseline.xml
+++ b/search-service/config/detekt/baseline.xml
@@ -5,11 +5,11 @@
ClassNaming:V0_29_JsonLd_migrationTests.kt$V0_29_JsonLd_migrationTests
ClassNaming:V0_29__JsonLd_migration.kt$V0_29__JsonLd_migration : BaseJavaMigration
ComplexCondition:EntitiesQueryUtils.kt$geoQuery == null && q.isNullOrEmpty() && typeSelection.isNullOrEmpty() && attrs.isEmpty()
- ComplexCondition:EntityQueryService.kt$EntityQueryService$it && !inverse || !it && inverse
Filename:V0_29__JsonLd_migration.kt$db.migration.V0_29__JsonLd_migration.kt
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:LinkedEntityServiceTests.kt$LinkedEntityServiceTests$@Test fun `it should inline entities up to the asked 2nd level`()
@@ -28,7 +28,6 @@
LongParameterList:EntityAttributeService.kt$EntityAttributeService$( entityId: URI, attributeName: ExpandedTerm, attributeMetadata: AttributeMetadata, createdAt: ZonedDateTime, attributePayload: ExpandedAttributeInstance, sub: Sub? )
LongParameterList:EntityAttributeService.kt$EntityAttributeService$( entityUri: URI, ngsiLdAttributes: List<NgsiLdAttribute>, expandedAttributes: ExpandedAttributes, createdAt: ZonedDateTime, observedAt: ZonedDateTime?, sub: Sub? )
LongParameterList:EntityAttributeService.kt$EntityAttributeService$( entityUri: URI, ngsiLdAttributes: List<NgsiLdAttribute>, expandedAttributes: ExpandedAttributes, disallowOverwrite: Boolean, createdAt: ZonedDateTime, sub: Sub? )
- LongParameterList:EntityEventService.kt$EntityEventService$( updatedDetails: UpdatedDetails, sub: String?, tenantName: String, entityId: URI, entityTypesAndPayload: Pair<List<ExpandedTerm>, String>, serializedAttribute: Pair<ExpandedTerm, String>, overwrite: Boolean )
LongParameterList:TemporalEntityHandler.kt$TemporalEntityHandler$( @RequestHeader httpHeaders: HttpHeaders, @PathVariable entityId: URI, @PathVariable attrId: String, @PathVariable instanceId: URI, @RequestBody requestBody: Mono<String>, @AllowedParameters(notImplemented = [QP.LOCAL, QP.VIA]) @RequestParam queryParams: MultiValueMap<String, String> )
LongParameterList:V0_29__JsonLd_migration.kt$V0_29__JsonLd_migration$( entityId: URI, attributeName: ExpandedTerm, datasetId: URI?, attributePayload: ExpandedAttributeInstance, ngsiLdAttributeInstance: NgsiLdAttributeInstance, defaultCreatedAt: ZonedDateTime )
NestedBlockDepth:V0_29__JsonLd_migration.kt$V0_29__JsonLd_migration$override fun migrate(context: Context)
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/listener/IAMListener.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/listener/IAMListener.kt
index d4fa226e6..a7e32bec2 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/listener/IAMListener.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/listener/IAMListener.kt
@@ -125,7 +125,7 @@ class IAMListener(
// (if it no longer exists, it fails because of access rights checks)
if (searchProperties.onOwnerDeleteCascadeEntities && subjectType == SubjectType.USER) {
entityAccessRightsService.getEntitiesIdsOwnedBySubject(sub).getOrNull()?.forEach { entityId ->
- entityService.deleteEntity(entityId, sub)
+ entityService.permanentlyDeleteEntity(entityId, sub)
}
Unit.right()
} else Unit.right()
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/model/EntityAccessRights.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/model/EntityAccessRights.kt
index 022110962..7528f783c 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/model/EntityAccessRights.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/model/EntityAccessRights.kt
@@ -6,6 +6,7 @@ import com.egm.stellio.shared.model.addNonReifiedProperty
import com.egm.stellio.shared.model.addSubAttribute
import com.egm.stellio.shared.util.AccessRight
import com.egm.stellio.shared.util.AuthContextModel
+import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_IS_DELETED
import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_RIGHT
import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_SAP
import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_SUBJECT_INFO
@@ -27,6 +28,7 @@ import java.net.URI
data class EntityAccessRights(
val id: URI,
val types: List,
+ val isDeleted: Boolean = false,
// right the current user has on the entity
val right: AccessRight,
val specificAccessPolicy: AuthContextModel.SpecificAccessPolicy? = null,
@@ -55,6 +57,8 @@ data class EntityAccessRights(
resultEntity[JSONLD_ID] = id.toString()
resultEntity[JSONLD_TYPE] = types
+ if (isDeleted)
+ resultEntity[AUTH_PROP_IS_DELETED] = buildExpandedPropertyValue(true)
resultEntity[AUTH_PROP_RIGHT] = buildExpandedPropertyValue(right.attributeName)
specificAccessPolicy?.run {
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/AuthorizationService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/AuthorizationService.kt
index c0dd9d0fe..c79064cfa 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/AuthorizationService.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/AuthorizationService.kt
@@ -23,6 +23,7 @@ interface AuthorizationService {
suspend fun getAuthorizedEntities(
entitiesQuery: EntitiesQueryFromGet,
+ includeDeleted: Boolean,
contexts: List,
sub: Option
): Either>>
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/DisabledAuthorizationService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/DisabledAuthorizationService.kt
index f27debb04..5b56b8a42 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/DisabledAuthorizationService.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/DisabledAuthorizationService.kt
@@ -41,6 +41,7 @@ class DisabledAuthorizationService : AuthorizationService {
override suspend fun getAuthorizedEntities(
entitiesQuery: EntitiesQueryFromGet,
+ includeDeleted: Boolean,
contexts: List,
sub: Option
): Either>> = Pair(-1, emptyList()).right()
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/EnabledAuthorizationService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/EnabledAuthorizationService.kt
index 53e3e31cf..65a64a3cf 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/EnabledAuthorizationService.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/EnabledAuthorizationService.kt
@@ -108,6 +108,7 @@ class EnabledAuthorizationService(
override suspend fun getAuthorizedEntities(
entitiesQuery: EntitiesQueryFromGet,
+ includeDeleted: Boolean,
contexts: List,
sub: Option
): Either>> = either {
@@ -115,9 +116,8 @@ class EnabledAuthorizationService(
val entitiesAccessRights = entityAccessRightsService.getSubjectAccessRights(
sub,
accessRights,
- entitiesQuery.typeSelection,
- entitiesQuery.ids,
- entitiesQuery.paginationQuery
+ entitiesQuery,
+ includeDeleted
).bind()
// for each entity user is admin or creator of, retrieve the full details of rights other users have on it
@@ -148,7 +148,8 @@ class EnabledAuthorizationService(
sub,
accessRights,
entitiesQuery.typeSelection,
- entitiesQuery.ids
+ entitiesQuery.ids,
+ includeDeleted
).bind()
Pair(count, entitiesAccessControlWithSubjectRights)
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/EntityAccessRightsService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/EntityAccessRightsService.kt
index e85454db8..fb6b3ce12 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/EntityAccessRightsService.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/EntityAccessRightsService.kt
@@ -18,13 +18,13 @@ import com.egm.stellio.search.common.util.toJsonString
import com.egm.stellio.search.common.util.toList
import com.egm.stellio.search.common.util.toOptionalEnum
import com.egm.stellio.search.common.util.toUri
+import com.egm.stellio.search.entity.model.EntitiesQueryFromGet
import com.egm.stellio.shared.config.ApplicationProperties
import com.egm.stellio.shared.model.APIException
import com.egm.stellio.shared.model.AccessDeniedException
import com.egm.stellio.shared.model.EntityTypeSelection
import com.egm.stellio.shared.model.NgsiLdAttribute
import com.egm.stellio.shared.model.ResourceNotFoundException
-import com.egm.stellio.shared.queryparameter.PaginationQuery
import com.egm.stellio.shared.util.AccessRight
import com.egm.stellio.shared.util.AccessRight.CAN_ADMIN
import com.egm.stellio.shared.util.AccessRight.CAN_READ
@@ -220,30 +220,32 @@ class EntityAccessRightsService(
suspend fun getSubjectAccessRights(
sub: Option,
accessRights: List,
- type: EntityTypeSelection? = null,
- ids: Set? = null,
- paginationQuery: PaginationQuery,
+ entitiesQuery: EntitiesQueryFromGet,
+ includeDeleted: Boolean = false
): Either> = either {
+ val ids = entitiesQuery.ids
+ val typeSelection = entitiesQuery.typeSelection
val subjectUuids = subjectReferentialService.getSubjectAndGroupsUUID(sub).bind()
val isStellioAdmin = subjectReferentialService.hasStellioAdminRole(subjectUuids).bind()
databaseClient
.sql(
"""
- SELECT ep.entity_id, ep.types, ear.access_right, ep.specific_access_policy
+ SELECT ep.entity_id, ep.types, ear.access_right, ep.specific_access_policy, ep.deleted_at
FROM entity_access_rights ear
LEFT JOIN entity_payload ep ON ear.entity_id = ep.entity_id
WHERE ${if (isStellioAdmin) "1 = 1" else "subject_id IN (:subject_uuids)" }
${if (accessRights.isNotEmpty()) " AND access_right IN (:access_rights)" else ""}
- ${if (!type.isNullOrEmpty()) " AND (${buildTypeQuery(type)})" else ""}
- ${if (!ids.isNullOrEmpty()) " AND ear.entity_id IN (:entities_ids)" else ""}
+ ${if (!typeSelection.isNullOrEmpty()) " AND (${buildTypeQuery(typeSelection)})" else ""}
+ ${if (ids.isNotEmpty()) " AND ear.entity_id IN (:entities_ids)" else ""}
+ ${if (!includeDeleted) " AND deleted_at IS NULL" else ""}
ORDER BY entity_id
LIMIT :limit
OFFSET :offset;
""".trimIndent()
)
- .bind("limit", paginationQuery.limit)
- .bind("offset", paginationQuery.offset)
+ .bind("limit", entitiesQuery.paginationQuery.limit)
+ .bind("offset", entitiesQuery.paginationQuery.offset)
.let {
if (!isStellioAdmin)
it.bind("subject_uuids", subjectUuids)
@@ -255,7 +257,7 @@ class EntityAccessRightsService(
else it
}
.let {
- if (!ids.isNullOrEmpty())
+ if (ids.isNotEmpty())
it.bind("entities_ids", ids)
else it
}
@@ -268,6 +270,7 @@ class EntityAccessRightsService(
EntityAccessRights(
ear.id,
ear.types,
+ ear.isDeleted,
entityAccessRights.maxOf { it.right },
ear.specificAccessPolicy
)
@@ -278,7 +281,8 @@ class EntityAccessRightsService(
sub: Option,
accessRights: List,
type: EntityTypeSelection? = null,
- ids: Set? = null
+ ids: Set? = null,
+ includeDeleted: Boolean = false
): Either = either {
val subjectUuids = subjectReferentialService.getSubjectAndGroupsUUID(sub).bind()
val isStellioAdmin = subjectReferentialService.hasStellioAdminRole(subjectUuids).bind()
@@ -293,6 +297,7 @@ class EntityAccessRightsService(
${if (accessRights.isNotEmpty()) " AND access_right IN (:access_rights)" else ""}
${if (!type.isNullOrEmpty()) " AND (${buildTypeQuery(type)})" else ""}
${if (!ids.isNullOrEmpty()) " AND ear.entity_id IN (:entities_ids)" else ""}
+ ${if (!includeDeleted) " AND deleted_at IS NULL" else ""}
""".trimIndent()
)
.let {
@@ -443,6 +448,7 @@ class EntityAccessRightsService(
return EntityAccessRights(
id = toUri(row["entity_id"]),
types = toList(row["types"]),
+ isDeleted = row["deleted_at"] != null,
right = accessRight,
specificAccessPolicy = toOptionalEnum(row["specific_access_policy"])
)
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 996dd3985..cbbdf667c 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/web/EntityAccessControlHandler.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/web/EntityAccessControlHandler.kt
@@ -4,10 +4,11 @@ import arrow.core.left
import arrow.core.raise.either
import com.egm.stellio.search.authorization.service.AuthorizationService
import com.egm.stellio.search.authorization.service.EntityAccessRightsService
+import com.egm.stellio.search.entity.model.FailedAttributeOperationResult
import com.egm.stellio.search.entity.model.NotUpdatedDetails
-import com.egm.stellio.search.entity.model.UpdateAttributeResult
-import com.egm.stellio.search.entity.model.UpdateOperationResult
-import com.egm.stellio.search.entity.model.updateResultFromDetailedResult
+import com.egm.stellio.search.entity.model.OperationStatus
+import com.egm.stellio.search.entity.model.SucceededAttributeOperationResult
+import com.egm.stellio.search.entity.model.UpdateResult
import com.egm.stellio.search.entity.util.composeEntitiesQueryFromGet
import com.egm.stellio.shared.config.ApplicationProperties
import com.egm.stellio.shared.model.AccessDeniedException
@@ -19,6 +20,7 @@ import com.egm.stellio.shared.model.toNgsiLdAttribute
import com.egm.stellio.shared.model.toNgsiLdAttributes
import com.egm.stellio.shared.queryparameter.AllowedParameters
import com.egm.stellio.shared.queryparameter.QP
+import com.egm.stellio.shared.queryparameter.QueryParameter
import com.egm.stellio.shared.util.AccessRight
import com.egm.stellio.shared.util.AuthContextModel.ALL_ASSIGNABLE_IAM_RIGHTS
import com.egm.stellio.shared.util.AuthContextModel.ALL_IAM_RIGHTS
@@ -70,10 +72,11 @@ class EntityAccessControlHandler(
@GetMapping("/entities", produces = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE])
suspend fun getAuthorizedEntities(
@RequestHeader httpHeaders: HttpHeaders,
- @AllowedParameters(implemented = [QP.ID, QP.TYPE, QP.ATTRS, QP.COUNT, QP.OFFSET, QP.LIMIT])
+ @AllowedParameters(implemented = [QP.ID, QP.TYPE, QP.ATTRS, QP.COUNT, QP.OFFSET, QP.LIMIT, QP.INCLUDE_DELETED])
@RequestParam queryParams: MultiValueMap
): ResponseEntity<*> = either {
val sub = getSubFromSecurityContext()
+ val includeDeleted = queryParams.getFirst(QueryParameter.INCLUDE_DELETED.key)?.toBoolean() == true
val contexts = getAuthzContextFromLinkHeaderOrDefault(httpHeaders, applicationProperties.contexts).bind()
val mediaType = getApplicableMediaType(httpHeaders).bind()
@@ -91,6 +94,7 @@ class EntityAccessControlHandler(
val (count, entities) = authorizationService.getAuthorizedEntities(
entitiesQuery,
+ includeDeleted,
contexts,
sub
).bind()
@@ -254,24 +258,24 @@ class EntityAccessControlHandler(
AccessRight.forAttributeName(ngsiLdRel.name).getOrNull()!!
).fold(
ifLeft = { apiException ->
- UpdateAttributeResult(
+ FailedAttributeOperationResult(
ngsiLdRel.name,
ngsiLdRelInstance.datasetId,
- UpdateOperationResult.FAILED,
+ OperationStatus.FAILED,
apiException.message
)
},
ifRight = {
- UpdateAttributeResult(
+ SucceededAttributeOperationResult(
ngsiLdRel.name,
ngsiLdRelInstance.datasetId,
- UpdateOperationResult.APPENDED,
- null
+ OperationStatus.APPENDED,
+ emptyMap()
)
}
)
}
- val appendResult = updateResultFromDetailedResult(results)
+ val appendResult = UpdateResult(results)
if (invalidAttributes.isEmpty() && unauthorizedInstances.isEmpty())
ResponseEntity.status(HttpStatus.NO_CONTENT).build()
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/discovery/service/AttributeService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/service/AttributeService.kt
index eb9aecf56..c2d1720b6 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/discovery/service/AttributeService.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/service/AttributeService.kt
@@ -30,6 +30,7 @@ class AttributeService(
"""
SELECT DISTINCT(attribute_name)
FROM temporal_entity_attribute
+ WHERE deleted_at IS NULL
ORDER BY attribute_name
""".trimIndent()
).allToMappedList { rowToAttributeNames(it) }
@@ -42,7 +43,10 @@ class AttributeService(
"""
SELECT types, attribute_name
FROM entity_payload
- JOIN temporal_entity_attribute ON entity_payload.entity_id = temporal_entity_attribute.entity_id
+ JOIN temporal_entity_attribute
+ ON entity_payload.entity_id = temporal_entity_attribute.entity_id
+ AND temporal_entity_attribute.deleted_at IS NULL
+ WHERE entity_payload.deleted_at IS NULL
ORDER BY attribute_name
""".trimIndent()
).allToMappedList { rowToAttributeDetails(it) }.flatten().groupBy({ it.second }, { it.first }).toList()
@@ -65,11 +69,14 @@ class AttributeService(
WITH entities AS (
SELECT entity_id, attribute_name, attribute_type
FROM temporal_entity_attribute
- WHERE attribute_name = :attribute_name
+ WHERE attribute_name = :attribute_name
+ AND deleted_at IS NULL
)
SELECT attribute_name, attribute_type, types, count(distinct(attribute_name)) as attribute_count
FROM entity_payload
- JOIN entities ON entity_payload.entity_id = entities.entity_id
+ JOIN entities
+ ON entity_payload.entity_id = entities.entity_id
+ AND entity_payload.deleted_at IS NULL
GROUP BY types, attribute_name, attribute_type
""".trimIndent()
)
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/discovery/service/EntityTypeService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/service/EntityTypeService.kt
index 07626ecb7..7a4d796c6 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/discovery/service/EntityTypeService.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/service/EntityTypeService.kt
@@ -29,6 +29,7 @@ class EntityTypeService(
"""
SELECT DISTINCT(unnest(types)) as type
FROM entity_payload
+ WHERE deleted_at IS NULL
ORDER BY type
""".trimIndent()
).allToMappedList { rowToType(it) }
@@ -41,7 +42,10 @@ class EntityTypeService(
"""
SELECT unnest(types) as type, attribute_name
FROM entity_payload
- JOIN temporal_entity_attribute ON entity_payload.entity_id = temporal_entity_attribute.entity_id
+ JOIN temporal_entity_attribute
+ ON entity_payload.entity_id = temporal_entity_attribute.entity_id
+ AND temporal_entity_attribute.deleted_at IS NULL
+ WHERE entity_payload.deleted_at IS NULL
ORDER BY type
""".trimIndent()
).allToMappedList { rowToEntityType(it) }.groupBy({ it.first }, { it.second }).toList()
@@ -65,10 +69,12 @@ class EntityTypeService(
SELECT entity_id
FROM entity_payload
WHERE :type_name = any (types)
+ AND deleted_at IS NULL
)
SELECT attribute_name, attribute_type, (select count(entity_id) from entities) as entity_count
FROM temporal_entity_attribute
WHERE entity_id IN (SELECT entity_id FROM entities)
+ AND deleted_at IS NULL
GROUP BY attribute_name, attribute_type
""".trimIndent()
)
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 bca913e99..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
@@ -1,18 +1,35 @@
package com.egm.stellio.search.entity.model
import com.egm.stellio.shared.model.ExpandedTerm
+import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_LANGUAGE
+import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_LANGUAGEMAP_TERM
+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
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_JSONPROPERTY_TYPE
+import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_JSONPROPERTY_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_JSONPROPERTY_VALUES
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_LANGUAGEPROPERTY_TYPE
+import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_LANGUAGEPROPERTY_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_LANGUAGEPROPERTY_VALUES
+import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_NONE_TERM
+import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_NULL
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_PROPERTY_TYPE
+import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_PROPERTY_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_PROPERTY_VALUES
+import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_RELATIONSHIP_OBJECT
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_RELATIONSHIP_OBJECTS
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
@@ -29,6 +46,7 @@ data class Attribute(
val datasetId: URI? = null,
val createdAt: ZonedDateTime,
val modifiedAt: ZonedDateTime? = null,
+ val deletedAt: ZonedDateTime? = null,
val payload: Json
) {
enum class AttributeValueType {
@@ -63,6 +81,21 @@ data class Attribute(
VocabProperty -> NGSILD_VOCABPROPERTY_TYPE.uri
}
+ /**
+ * Returns the expanded name of the member who holds the value of the attribute.
+ *
+ * For instance, https://uri.etsi.org/ngsi-ld/hasJSON if it is a JsonProperty
+ */
+ fun toExpandedValueMember(): String =
+ when (this) {
+ Property -> NGSILD_PROPERTY_VALUE
+ Relationship -> NGSILD_RELATIONSHIP_OBJECT
+ GeoProperty -> NGSILD_GEOPROPERTY_VALUE
+ JsonProperty -> NGSILD_JSONPROPERTY_VALUE
+ LanguageProperty -> NGSILD_LANGUAGEPROPERTY_VALUE
+ VocabProperty -> NGSILD_VOCABPROPERTY_VALUE
+ }
+
/**
* Returns the key of the member for the simplified representation of the attribute, as defined in 4.5.9
*/
@@ -75,5 +108,37 @@ data class Attribute(
LanguageProperty -> NGSILD_LANGUAGEPROPERTY_VALUES
VocabProperty -> NGSILD_VOCABPROPERTY_VALUES
}
+
+ fun toNullCompactedRepresentation(datasetId: URI? = null): Map =
+ when (this) {
+ Property, GeoProperty, JsonProperty, VocabProperty ->
+ mapOf(
+ JSONLD_TYPE_TERM to this.name,
+ JSONLD_VALUE_TERM to NGSILD_NULL
+ )
+ Relationship ->
+ mapOf(
+ JSONLD_TYPE_TERM to this.name,
+ JSONLD_OBJECT to NGSILD_NULL
+ )
+ LanguageProperty ->
+ mapOf(
+ 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 =
+ when (this) {
+ Property, GeoProperty, JsonProperty, VocabProperty, Relationship -> NGSILD_NULL
+ LanguageProperty ->
+ serializeObject(listOf(mapOf(JSONLD_VALUE to NGSILD_NULL, JSONLD_LANGUAGE to NGSILD_NONE_TERM)))
+ }
}
}
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 e81816479..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
@@ -1,5 +1,6 @@
package com.egm.stellio.search.entity.model
+import com.egm.stellio.shared.model.ExpandedEntity
import com.egm.stellio.shared.model.ExpandedTerm
import com.egm.stellio.shared.util.AuthContextModel
import com.egm.stellio.shared.util.AuthContextModel.SpecificAccessPolicy
@@ -7,6 +8,7 @@ import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_ID
import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_TYPE
import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_VALUE
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_SCOPE_PROPERTY
import com.egm.stellio.shared.util.JsonLdUtils.buildExpandedPropertyValue
@@ -21,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
) {
@@ -44,4 +47,20 @@ data class Entity(
return resultEntity
}
+
+ fun toExpandedDeletedEntity(
+ deletedAt: ZonedDateTime
+ ): ExpandedEntity =
+ ExpandedEntity(
+ members = mapOf(
+ JSONLD_ID to entityId,
+ JSONLD_TYPE to types,
+ NGSILD_CREATED_AT_PROPERTY to buildNonReifiedTemporalValue(createdAt),
+ NGSILD_DELETED_AT_PROPERTY to buildNonReifiedTemporalValue(deletedAt),
+ ).run {
+ if (modifiedAt != null)
+ this.plus(NGSILD_MODIFIED_AT_PROPERTY to buildNonReifiedTemporalValue(modifiedAt))
+ else this
+ }
+ )
}
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/UpdateResult.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/UpdateResult.kt
index 9ccf6a575..a34eef570 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/UpdateResult.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/UpdateResult.kt
@@ -1,78 +1,88 @@
package com.egm.stellio.search.entity.model
+import com.egm.stellio.shared.model.ExpandedAttributeInstance
import com.fasterxml.jackson.annotation.JsonIgnore
-import com.fasterxml.jackson.annotation.JsonValue
import java.net.URI
+/**
+ * UpdateResult datatype as defined in 5.2.18
+ */
data class UpdateResult(
- val updated: List,
+ val updated: List,
val notUpdated: List
) {
@JsonIgnore
fun isSuccessful(): Boolean =
- notUpdated.isEmpty() &&
- updated.all { it.updateOperationResult.isSuccessResult() }
+ notUpdated.isEmpty()
- @JsonIgnore
- fun mergeWith(other: UpdateResult): UpdateResult =
- UpdateResult(
- updated = this.updated.plus(other.updated),
- notUpdated = this.notUpdated.plus(other.notUpdated)
- )
+ companion object {
- @JsonIgnore
- fun hasSuccessfulUpdate(): Boolean =
- this.updated.isNotEmpty()
+ operator fun invoke(operationsResults: List): UpdateResult =
+ operationsResults.map {
+ when (it) {
+ is SucceededAttributeOperationResult -> it.attributeName
+ is FailedAttributeOperationResult -> NotUpdatedDetails(it.attributeName, it.errorMessage)
+ }
+ }.let {
+ UpdateResult(
+ it.filterIsInstance(),
+ it.filterIsInstance()
+ )
+ }
+ }
}
val EMPTY_UPDATE_RESULT: UpdateResult = UpdateResult(emptyList(), emptyList())
+/**
+ * NotUpdatedDetails as defined in 5.2.19
+ */
data class NotUpdatedDetails(
val attributeName: String,
val reason: String
)
-data class UpdatedDetails(
- @JsonValue
- val attributeName: String,
- @JsonIgnore
- val datasetId: URI?,
- @JsonIgnore
- val updateOperationResult: UpdateOperationResult
+/**
+ * Internal structure used to convey the result of an operation (update, delete...)
+ */
+sealed class AttributeOperationResult(
+ open val attributeName: String,
+ open val datasetId: URI? = null,
+ open val operationStatus: OperationStatus
)
-data class UpdateAttributeResult(
- val attributeName: String,
- val datasetId: URI? = null,
- val updateOperationResult: UpdateOperationResult,
- val errorMessage: String? = null
-) {
- fun isSuccessfullyUpdated() =
- this.updateOperationResult in listOf(
- UpdateOperationResult.APPENDED,
- UpdateOperationResult.REPLACED,
- UpdateOperationResult.UPDATED,
- UpdateOperationResult.IGNORED
- )
-}
+data class SucceededAttributeOperationResult(
+ override val attributeName: String,
+ override val datasetId: URI? = null,
+ override val operationStatus: OperationStatus,
+ val newExpandedValue: ExpandedAttributeInstance,
+) : AttributeOperationResult(attributeName, datasetId, operationStatus)
+
+data class FailedAttributeOperationResult(
+ override val attributeName: String,
+ override val datasetId: URI? = null,
+ override val operationStatus: OperationStatus,
+ val errorMessage: String
+) : AttributeOperationResult(attributeName, datasetId, operationStatus)
-enum class UpdateOperationResult {
+enum class OperationStatus {
APPENDED,
REPLACED,
UPDATED,
+ DELETED,
IGNORED,
FAILED;
- fun isSuccessResult(): Boolean = listOf(APPENDED, REPLACED, UPDATED).contains(this)
-}
+ fun isSuccessResult(): Boolean = getSuccessStatuses().contains(this)
-fun updateResultFromDetailedResult(updateStatuses: List): UpdateResult {
- val updated = updateStatuses.filter { it.isSuccessfullyUpdated() }
- .map { UpdatedDetails(it.attributeName, it.datasetId, it.updateOperationResult) }
+ companion object {
+ fun getSuccessStatuses(): List = listOf(APPENDED, REPLACED, UPDATED, DELETED, IGNORED)
+ }
+}
- val notUpdated = updateStatuses.filter { !it.isSuccessfullyUpdated() }
- .map { NotUpdatedDetails(it.attributeName, it.errorMessage!!) }
+fun List.hasSuccessfulResult(): Boolean =
+ this.any { it is SucceededAttributeOperationResult }
- return UpdateResult(updated, notUpdated)
-}
+fun List.getSucceededOperations(): List =
+ this.filterIsInstance()
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityAttributeService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityAttributeService.kt
index adf1ff565..223cec0e4 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityAttributeService.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityAttributeService.kt
@@ -22,12 +22,13 @@ import com.egm.stellio.search.common.util.valueToDoubleOrNull
import com.egm.stellio.search.common.util.valueToStringOrNull
import com.egm.stellio.search.entity.model.Attribute
import com.egm.stellio.search.entity.model.AttributeMetadata
+import com.egm.stellio.search.entity.model.AttributeOperationResult
import com.egm.stellio.search.entity.model.EntitiesQuery
-import com.egm.stellio.search.entity.model.UpdateAttributeResult
-import com.egm.stellio.search.entity.model.UpdateOperationResult
-import com.egm.stellio.search.entity.model.UpdateResult
-import com.egm.stellio.search.entity.model.updateResultFromDetailedResult
+import com.egm.stellio.search.entity.model.FailedAttributeOperationResult
+import com.egm.stellio.search.entity.model.OperationStatus
+import com.egm.stellio.search.entity.model.SucceededAttributeOperationResult
import com.egm.stellio.search.entity.util.guessAttributeValueType
+import com.egm.stellio.search.entity.util.hasNgsiLdNullValue
import com.egm.stellio.search.entity.util.mergePatch
import com.egm.stellio.search.entity.util.partialUpdatePatch
import com.egm.stellio.search.entity.util.prepareAttributes
@@ -35,6 +36,7 @@ import com.egm.stellio.search.entity.util.toAttributeMetadata
import com.egm.stellio.search.entity.util.toExpandedAttributeInstance
import com.egm.stellio.search.temporal.model.AttributeInstance
import com.egm.stellio.search.temporal.service.AttributeInstanceService
+import com.egm.stellio.shared.config.ApplicationProperties
import com.egm.stellio.shared.model.APIException
import com.egm.stellio.shared.model.BadRequestDataException
import com.egm.stellio.shared.model.ExpandedAttribute
@@ -46,6 +48,7 @@ import com.egm.stellio.shared.model.NgsiLdAttribute
import com.egm.stellio.shared.model.NgsiLdEntity
import com.egm.stellio.shared.model.ResourceNotFoundException
import com.egm.stellio.shared.model.WKTCoordinates
+import com.egm.stellio.shared.model.addSysAttrs
import com.egm.stellio.shared.model.flatOnInstances
import com.egm.stellio.shared.model.getAttributeFromExpandedAttributes
import com.egm.stellio.shared.model.getDatasetId
@@ -56,6 +59,7 @@ import com.egm.stellio.shared.model.isAttributeOfType
import com.egm.stellio.shared.model.toNgsiLdEntity
import com.egm.stellio.shared.util.AttributeType
import com.egm.stellio.shared.util.AuthContextModel
+import com.egm.stellio.shared.util.JsonLdUtils
import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_TYPE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_JSONPROPERTY_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_LANGUAGEPROPERTY_VALUE
@@ -70,6 +74,7 @@ import com.egm.stellio.shared.util.JsonUtils.serializeObject
import com.egm.stellio.shared.util.Sub
import com.egm.stellio.shared.util.attributeNotFoundMessage
import com.egm.stellio.shared.util.entityNotFoundMessage
+import com.egm.stellio.shared.util.ngsiLdDateTime
import io.r2dbc.postgresql.codec.Json
import org.slf4j.LoggerFactory
import org.springframework.r2dbc.core.DatabaseClient
@@ -77,14 +82,14 @@ import org.springframework.r2dbc.core.bind
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.net.URI
-import java.time.ZoneOffset
import java.time.ZonedDateTime
-import java.util.UUID
+import java.util.*
@Service
class EntityAttributeService(
private val databaseClient: DatabaseClient,
- private val attributeInstanceService: AttributeInstanceService
+ private val attributeInstanceService: AttributeInstanceService,
+ private val applicationProperties: ApplicationProperties
) {
private val logger = LoggerFactory.getLogger(javaClass)
@@ -99,6 +104,12 @@ class EntityAttributeService(
VALUES
(:id, :entity_id, :attribute_name, :attribute_type, :attribute_value_type, :created_at, :dataset_id,
:payload)
+ ON CONFLICT (entity_id, attribute_name, dataset_id)
+ DO UPDATE SET deleted_at = null,
+ attribute_type = :attribute_type,
+ attribute_value_type = :attribute_value_type,
+ modified_at = :created_at,
+ payload = :payload
""".trimIndent()
)
.bind("id", attribute.id)
@@ -111,31 +122,6 @@ class EntityAttributeService(
.bind("payload", attribute.payload)
.execute()
- @Transactional
- suspend fun updateOnReplace(
- attributeUUID: UUID,
- attributeMetadata: AttributeMetadata,
- modifiedAt: ZonedDateTime,
- payload: String
- ): Either =
- databaseClient.sql(
- """
- UPDATE temporal_entity_attribute
- SET
- attribute_type = :attribute_type,
- attribute_value_type = :attribute_value_type,
- modified_at = :modified_at,
- payload = :payload
- WHERE id = :id
- """.trimIndent()
- )
- .bind("id", attributeUUID)
- .bind("attribute_type", attributeMetadata.type.toString())
- .bind("attribute_value_type", attributeMetadata.valueType.toString())
- .bind("modified_at", modifiedAt)
- .bind("payload", Json.of(payload))
- .execute()
-
@Transactional
suspend fun updateOnUpdate(
attributeUUID: UUID,
@@ -169,7 +155,7 @@ class EntityAttributeService(
contexts: List,
sub: String? = null
): Either = either {
- val createdAt = ZonedDateTime.now(ZoneOffset.UTC)
+ val createdAt = ngsiLdDateTime()
val expandedEntity = expandJsonLdEntity(payload, contexts)
val ngsiLdEntity = expandedEntity.toNgsiLdEntity().bind()
ngsiLdEntity.prepareAttributes()
@@ -218,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(
@@ -268,32 +253,15 @@ class EntityAttributeService(
attributeMetadata.datasetId,
attribute.entityId
)
- updateOnReplace(
- attribute.id,
+ deleteAttribute(attribute.entityId, attribute.attributeName, attribute.datasetId, false, createdAt).bind()
+ addAttribute(
+ attribute.entityId,
+ attribute.attributeName,
attributeMetadata,
createdAt,
- serializeObject(attributePayload)
+ attributePayload,
+ sub
).bind()
-
- val attributeInstance = AttributeInstance(
- attributeUuid = attribute.id,
- timeProperty = AttributeInstance.TemporalProperty.MODIFIED_AT,
- time = createdAt,
- attributeMetadata = attributeMetadata,
- payload = attributePayload,
- sub = sub
- )
- 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()
- }
}
@Transactional
@@ -329,22 +297,9 @@ class EntityAttributeService(
}
@Transactional
- suspend fun deleteAttributes(entityId: URI): Either {
- val uuids = databaseClient.sql(
- """
- DELETE FROM temporal_entity_attribute
- WHERE entity_id = :entity_id
- RETURNING id
- """.trimIndent()
- )
- .bind("entity_id", entityId)
- .allToMappedList {
- toUuid(it["id"])
- }
-
- return if (uuids.isNotEmpty())
- attributeInstanceService.deleteInstancesOfEntity(uuids)
- else Unit.right()
+ suspend fun deleteAttributes(entityId: URI, deletedAt: ZonedDateTime): Either = either {
+ val attributesToDelete = getForEntity(entityId, emptySet(), emptySet())
+ deleteSelectedAttributes(attributesToDelete, deletedAt).bind()
}
@Transactional
@@ -352,56 +307,129 @@ class EntityAttributeService(
entityId: URI,
attributeName: String,
datasetId: URI?,
- deleteAll: Boolean = false
- ): Either =
- either {
- logger.debug("Deleting attribute {} from entity {} (all: {})", attributeName, entityId, deleteAll)
- if (deleteAll) {
- attributeInstanceService.deleteAllInstancesOfAttribute(entityId, attributeName).bind()
- deleteAllInstances(entityId, attributeName).bind()
- } else {
- attributeInstanceService.deleteInstancesOfAttribute(entityId, attributeName, datasetId).bind()
- deleteSpecificInstance(entityId, attributeName, datasetId).bind()
+ deleteAll: Boolean = false,
+ deletedAt: ZonedDateTime
+ ): 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 emptyList().right()
+ val attributesToDeleteWithPayload = attributesToDelete.map {
+ Pair(
+ it,
+ JsonLdUtils.expandAttribute(
+ it.attributeName,
+ it.attributeType.toNullCompactedRepresentation(it.datasetId),
+ listOf(applicationProperties.contexts.core)
+ ).second[0]
+ )
}
+ 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 { row ->
+ mapOf(
+ toUuid(row["id"]) to Triple(
+ toZonedDateTime(row["created_at"]),
+ toOptionalZonedDateTime(row["modified_at"]),
+ toZonedDateTime(row["deleted_at"])
+ )
+ )
+ }
+
+ attributesToDeleteWithPayload.forEach { (attribute, expandedAttributePayload) ->
+ attributeInstanceService.addDeletedAttributeInstance(
+ attributeUuid = attribute.id,
+ value = attribute.attributeType.toNullValue(),
+ deletedAt = deletedAt,
+ 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
- suspend fun deleteSpecificInstance(
+ suspend fun permanentlyDeleteAttribute(
entityId: URI,
attributeName: String,
- datasetId: URI?
- ): Either =
+ datasetId: URI?,
+ deleteAll: Boolean = false
+ ): Either = either {
+ logger.debug("Permanently deleting attribute {} from entity {} (all: {})", attributeName, entityId, deleteAll)
+ val attributesToDelete =
+ if (deleteAll)
+ getForEntity(entityId, setOf(attributeName), emptySet(), false)
+ else
+ listOf(getForEntityAndAttribute(entityId, attributeName, datasetId).bind())
+
databaseClient.sql(
"""
DELETE FROM temporal_entity_attribute
- WHERE entity_id = :entity_id
- ${datasetId.toDatasetIdFilter()}
- AND attribute_name = :attribute_name
+ WHERE id IN(:uuids)
""".trimIndent()
)
- .bind("entity_id", entityId)
- .bind("attribute_name", attributeName)
- .let {
- if (datasetId != null) it.bind("dataset_id", datasetId)
- else it
- }
+ .bind("uuids", attributesToDelete.map { it.id })
.execute()
+ if (deleteAll)
+ attributeInstanceService.deleteAllInstancesOfAttribute(entityId, attributeName).bind()
+ else
+ attributeInstanceService.deleteInstancesOfAttribute(entityId, attributeName, datasetId).bind()
+ }
+
@Transactional
- suspend fun deleteAllInstances(
+ suspend fun permanentlyDeleteAttributes(
entityId: URI,
- attributeName: String
- ): Either =
- databaseClient.sql(
+ ): Either = either {
+ logger.debug("Permanently deleting all attributes from entity {}", entityId)
+
+ val deletedTeas = databaseClient.sql(
"""
DELETE FROM temporal_entity_attribute
WHERE entity_id = :entity_id
- AND attribute_name = :attribute_name
+ RETURNING id
""".trimIndent()
)
.bind("entity_id", entityId)
- .bind("attribute_name", attributeName)
- .execute()
+ .allToMappedList { toUuid(it["id"]) }
+
+ if (deletedTeas.isNotEmpty())
+ attributeInstanceService.deleteInstancesOfEntity(deletedTeas).bind()
+ else Unit.right()
+ }
suspend fun getForEntities(
entitiesIds: List,
@@ -426,7 +454,7 @@ class EntityAttributeService(
val selectQuery =
"""
SELECT id, entity_id, attribute_name, attribute_type, attribute_value_type, created_at, modified_at,
- dataset_id, payload
+ deleted_at, dataset_id, payload
FROM temporal_entity_attribute
WHERE entity_id IN (:entities_ids)
$filterOnAttributes
@@ -440,7 +468,12 @@ class EntityAttributeService(
.allToMappedList { rowToAttribute(it) }
}
- suspend fun getForEntity(id: URI, attrs: Set, datasetIds: Set): List {
+ suspend fun getForEntity(
+ id: URI,
+ attrs: Set,
+ datasetIds: Set,
+ excludedDeleted: Boolean = true
+ ): List {
val filterOnAttributes =
if (attrs.isNotEmpty())
" AND " + attrs.joinToString(
@@ -463,6 +496,7 @@ class EntityAttributeService(
dataset_id, payload
FROM temporal_entity_attribute
WHERE entity_id = :entity_id
+ ${if (excludedDeleted) " and deleted_at is null " else ""}
$filterOnAttributes
$filterOnDatasetId
""".trimIndent()
@@ -500,33 +534,6 @@ class EntityAttributeService(
}
}
- suspend fun hasAttribute(
- id: URI,
- attributeName: String,
- datasetId: URI? = null
- ): Either {
- val selectQuery =
- """
- SELECT count(entity_id) as count
- FROM temporal_entity_attribute
- WHERE entity_id = :entity_id
- ${datasetId.toDatasetIdFilter()}
- AND attribute_name = :attribute_name
- """.trimIndent()
-
- return databaseClient
- .sql(selectQuery)
- .bind("entity_id", id)
- .bind("attribute_name", attributeName)
- .let {
- if (datasetId != null) it.bind("dataset_id", datasetId)
- else it
- }
- .oneToResult {
- it["count"] as Long == 1L
- }
- }
-
private fun rowToAttribute(row: Map) =
Attribute(
id = toUuid(row["id"]),
@@ -539,6 +546,7 @@ class EntityAttributeService(
datasetId = toOptionalUri(row["dataset_id"]),
createdAt = toZonedDateTime(row["created_at"]),
modifiedAt = toOptionalZonedDateTime(row["modified_at"]),
+ deletedAt = toOptionalZonedDateTime(row["deleted_at"]),
payload = toJson(row["payload"])
)
@@ -564,6 +572,7 @@ class EntityAttributeService(
from temporal_entity_attribute
where entity_id = :entity_id
and attribute_name = :attribute_name
+ and deleted_at is null
$datasetIdFilter
) as attributeNameExists;
""".trimIndent()
@@ -594,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)
@@ -615,21 +624,20 @@ class EntityAttributeService(
attributePayload,
sub
).map {
- UpdateAttributeResult(
+ SucceededAttributeOperationResult(
ngsiLdAttribute.name,
ngsiLdAttributeInstance.datasetId,
- UpdateOperationResult.APPENDED,
- null
+ 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(
@@ -640,16 +648,16 @@ class EntityAttributeService(
attributePayload,
sub
).map {
- UpdateAttributeResult(
+ SucceededAttributeOperationResult(
ngsiLdAttribute.name,
ngsiLdAttributeInstance.datasetId,
- UpdateOperationResult.REPLACED,
- null
+ OperationStatus.REPLACED,
+ attributePayload
)
}.bind()
}
}
- }.fold({ it.left() }, { updateResultFromDetailedResult(it).right() })
+ }.fold({ it.left() }, { it.right() })
@Transactional
suspend fun updateAttributes(
@@ -658,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)
@@ -670,41 +678,50 @@ class EntityAttributeService(
ngsiLdAttribute.name,
ngsiLdAttributeInstance.datasetId
)!!
- if (currentAttribute != null) {
- replaceAttribute(
- currentAttribute,
- ngsiLdAttribute,
+
+ if (currentAttribute == null) {
+ addAttribute(
+ entityUri,
+ ngsiLdAttribute.name,
attributeMetadata,
createdAt,
attributePayload,
sub
).map {
- UpdateAttributeResult(
+ SucceededAttributeOperationResult(
ngsiLdAttribute.name,
ngsiLdAttributeInstance.datasetId,
- UpdateOperationResult.REPLACED,
- null
+ OperationStatus.APPENDED,
+ attributePayload
)
}.bind()
- } else {
- addAttribute(
+ } else if (hasNgsiLdNullValue(attributePayload, currentAttribute.attributeType)) {
+ deleteAttribute(
entityUri,
ngsiLdAttribute.name,
+ ngsiLdAttributeInstance.datasetId,
+ false,
+ createdAt
+ ).bind().first()
+ } else {
+ replaceAttribute(
+ currentAttribute,
+ ngsiLdAttribute,
attributeMetadata,
createdAt,
attributePayload,
sub
).map {
- UpdateAttributeResult(
+ SucceededAttributeOperationResult(
ngsiLdAttribute.name,
ngsiLdAttributeInstance.datasetId,
- UpdateOperationResult.APPENDED,
- null
+ OperationStatus.REPLACED,
+ attributePayload
)
}.bind()
}
}
- }.fold({ it.left() }, { updateResultFromDetailedResult(it).right() })
+ }.fold({ it.left() }, { it.right() })
@Transactional
suspend fun partialUpdateAttribute(
@@ -712,20 +729,30 @@ class EntityAttributeService(
expandedAttribute: ExpandedAttribute,
modifiedAt: ZonedDateTime,
sub: Sub?
- ): Either = either {
+ ): Either = either {
val attributeName = expandedAttribute.first
val attributeValues = expandedAttribute.second[0]
- logger.debug(
- "Updating attribute {} of entity {} with values: {}",
- attributeName,
- entityId,
- attributeValues
- )
+ logger.debug("Partial updating attribute {} in entity {}", attributeName, entityId)
val datasetId = attributeValues.getDatasetId()
- val exists = hasAttribute(entityId, attributeName, datasetId).bind()
- val updateAttributeResult =
- if (exists) {
+ val currentAttribute = getForEntityAndAttribute(entityId, attributeName, datasetId).fold({ null }, { it })
+ val attributeOperationResult =
+ if (currentAttribute == null) {
+ FailedAttributeOperationResult(
+ attributeName,
+ datasetId,
+ OperationStatus.FAILED,
+ "Unknown attribute $attributeName with datasetId $datasetId in entity $entityId"
+ )
+ } else if (hasNgsiLdNullValue(attributeValues, currentAttribute.attributeType)) {
+ deleteAttribute(
+ entityId,
+ attributeName,
+ datasetId,
+ false,
+ modifiedAt
+ ).bind().first()
+ } else {
// first update payload in temporal entity attribute
val attribute = getForEntityAndAttribute(entityId, attributeName, datasetId).bind()
attributeValues[JSONLD_TYPE]?.let {
@@ -749,22 +776,15 @@ class EntityAttributeService(
)
attributeInstanceService.create(attributeInstance).bind()
- UpdateAttributeResult(
+ SucceededAttributeOperationResult(
attributeName,
datasetId,
- UpdateOperationResult.UPDATED,
- null
- )
- } else {
- UpdateAttributeResult(
- attributeName,
- datasetId,
- UpdateOperationResult.FAILED,
- "Unknown attribute $attributeName with datasetId $datasetId in entity $entityId"
+ OperationStatus.UPDATED,
+ updatedAttributeInstance
)
}
- updateResultFromDetailedResult(listOf(updateAttributeResult))
+ attributeOperationResult
}
@Transactional
@@ -802,7 +822,7 @@ class EntityAttributeService(
).bind()
} else {
logger.debug("Adding instance to attribute {} to entity {}", currentAttribute.attributeName, entityUri)
- attributeInstanceService.addAttributeInstance(
+ attributeInstanceService.addObservedAttributeInstance(
currentAttribute.id,
attributeMetadata,
expandedAttributes[currentAttribute.attributeName]!!.first()
@@ -818,7 +838,7 @@ class EntityAttributeService(
createdAt: ZonedDateTime,
observedAt: ZonedDateTime?,
sub: Sub?
- ): Either = either {
+ ): Either> = either {
val attributeInstances = ngsiLdAttributes.flatOnInstances()
attributeInstances.parMap { (ngsiLdAttribute, ngsiLdAttributeInstance) ->
logger.debug("Merging attribute {} in entity {}", ngsiLdAttribute.name, entityUri)
@@ -831,7 +851,7 @@ class EntityAttributeService(
ngsiLdAttributeInstance.datasetId
)!!
- if (currentAttribute == null) {
+ if (currentAttribute == null)
addAttribute(
entityUri,
ngsiLdAttribute.name,
@@ -840,14 +860,22 @@ class EntityAttributeService(
attributePayload,
sub
).map {
- UpdateAttributeResult(
+ SucceededAttributeOperationResult(
ngsiLdAttribute.name,
ngsiLdAttributeInstance.datasetId,
- UpdateOperationResult.APPENDED,
- null
+ OperationStatus.APPENDED,
+ attributePayload
)
}.bind()
- } else {
+ else if (hasNgsiLdNullValue(attributePayload, currentAttribute.attributeType))
+ deleteAttribute(
+ entityUri,
+ ngsiLdAttribute.name,
+ ngsiLdAttributeInstance.datasetId,
+ false,
+ createdAt
+ ).bind().first()
+ else
mergeAttribute(
currentAttribute,
ngsiLdAttribute.name,
@@ -857,16 +885,15 @@ class EntityAttributeService(
attributePayload,
sub
).map {
- UpdateAttributeResult(
+ SucceededAttributeOperationResult(
ngsiLdAttribute.name,
ngsiLdAttributeInstance.datasetId,
- UpdateOperationResult.UPDATED,
- null
+ OperationStatus.UPDATED,
+ attributePayload
)
}.bind()
- }
}
- }.fold({ it.left() }, { updateResultFromDetailedResult(it).right() })
+ }.fold({ it.left() }, { it.right() })
@Transactional
suspend fun replaceAttribute(
@@ -875,19 +902,19 @@ class EntityAttributeService(
expandedAttribute: ExpandedAttribute,
replacedAt: ZonedDateTime,
sub: Sub?
- ): Either = either {
+ ): Either = either {
val ngsiLdAttributeInstance = ngsiLdAttribute.getAttributeInstances()[0]
val attributeName = ngsiLdAttribute.name
val datasetId = ngsiLdAttributeInstance.datasetId
val currentTea =
getForEntityAndAttribute(entityId, attributeName, datasetId).fold({ null }, { it })
val attributeMetadata = ngsiLdAttributeInstance.toAttributeMetadata().bind()
- val updateAttributeResult =
+ val attributeOperationResult =
if (currentTea == null) {
- UpdateAttributeResult(
+ FailedAttributeOperationResult(
attributeName,
datasetId,
- UpdateOperationResult.FAILED,
+ OperationStatus.FAILED,
"Unknown attribute $attributeName with datasetId $datasetId in entity $entityId"
)
} else {
@@ -900,15 +927,15 @@ class EntityAttributeService(
sub
).bind()
- UpdateAttributeResult(
+ SucceededAttributeOperationResult(
ngsiLdAttribute.name,
ngsiLdAttributeInstance.datasetId,
- UpdateOperationResult.REPLACED,
- null
+ OperationStatus.REPLACED,
+ expandedAttribute.second.first()
)
}
- updateResultFromDetailedResult(listOf(updateAttributeResult))
+ attributeOperationResult
}
suspend fun getValueFromPartialAttributePayload(
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityEventService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityEventService.kt
index 8586746ef..830a91a7b 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityEventService.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityEventService.kt
@@ -2,12 +2,10 @@ package com.egm.stellio.search.entity.service
import arrow.core.Either
import com.egm.stellio.search.entity.model.Entity
-import com.egm.stellio.search.entity.model.UpdateOperationResult
-import com.egm.stellio.search.entity.model.UpdateResult
-import com.egm.stellio.search.entity.model.UpdatedDetails
+import com.egm.stellio.search.entity.model.OperationStatus
+import com.egm.stellio.search.entity.model.SucceededAttributeOperationResult
import com.egm.stellio.shared.model.APIException
import com.egm.stellio.shared.model.AttributeAppendEvent
-import com.egm.stellio.shared.model.AttributeDeleteAllInstancesEvent
import com.egm.stellio.shared.model.AttributeDeleteEvent
import com.egm.stellio.shared.model.AttributeReplaceEvent
import com.egm.stellio.shared.model.AttributeUpdateEvent
@@ -16,10 +14,10 @@ import com.egm.stellio.shared.model.EntityDeleteEvent
import com.egm.stellio.shared.model.EntityEvent
import com.egm.stellio.shared.model.EntityReplaceEvent
import com.egm.stellio.shared.model.EventsType
-import com.egm.stellio.shared.model.ExpandedAttributes
+import com.egm.stellio.shared.model.ExpandedAttributeInstance
+import com.egm.stellio.shared.model.ExpandedEntity
import com.egm.stellio.shared.model.ExpandedTerm
-import com.egm.stellio.shared.model.getAttributeFromExpandedAttributes
-import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_TYPE
+import com.egm.stellio.shared.util.JsonUtils.deserializeAsMap
import com.egm.stellio.shared.util.JsonUtils.serializeObject
import com.egm.stellio.shared.util.getTenantFromContext
import kotlinx.coroutines.CoroutineScope
@@ -84,18 +82,20 @@ class EntityEventService(
suspend fun publishEntityDeleteEvent(
sub: String?,
- entity: Entity
+ previousEntity: Entity,
+ deletedEntityPayload: ExpandedEntity
): Job {
val tenantName = getTenantFromContext()
return coroutineScope.launch {
- logger.debug("Sending delete event for entity {} in tenant {}", entity.entityId, tenantName)
+ logger.debug("Sending delete event for entity {} in tenant {}", previousEntity.entityId, tenantName)
publishEntityEvent(
EntityDeleteEvent(
sub,
tenantName,
- entity.entityId,
- entity.types,
- entity.payload.asString(),
+ previousEntity.entityId,
+ previousEntity.types,
+ previousEntity.payload.asString(),
+ serializeObject(deletedEntityPayload.members),
emptyList()
)
)
@@ -105,27 +105,19 @@ class EntityEventService(
suspend fun publishAttributeChangeEvents(
sub: String?,
entityId: URI,
- jsonLdAttributes: Map,
- updateResult: UpdateResult,
- overwrite: Boolean
+ attributesOperationsResults: List
): Job {
val tenantName = getTenantFromContext()
val entity = getSerializedEntity(entityId)
return coroutineScope.launch {
- logger.debug("Sending attributes change events for entity {} in tenant {}", entityId, tenantName)
entity.onRight {
- updateResult.updated.forEach { updatedDetails ->
- val attributeName = updatedDetails.attributeName
- val serializedAttribute =
- getSerializedAttribute(jsonLdAttributes, attributeName, updatedDetails.datasetId)
+ attributesOperationsResults.forEach { attributeOperationResult ->
publishAttributeChangeEvent(
- updatedDetails,
sub,
tenantName,
entityId,
it,
- serializedAttribute,
- overwrite
+ attributeOperationResult
)
}
}.logAttributeEvent("Attribute Change", entityId, tenantName)
@@ -133,65 +125,85 @@ class EntityEventService(
}
private fun publishAttributeChangeEvent(
- updatedDetails: UpdatedDetails,
sub: String?,
tenantName: String,
entityId: URI,
entityTypesAndPayload: Pair, String>,
- serializedAttribute: Pair,
- overwrite: Boolean
+ attributeOperationResult: SucceededAttributeOperationResult
) {
- when (updatedDetails.updateOperationResult) {
- UpdateOperationResult.APPENDED ->
+ val attributeName = attributeOperationResult.attributeName
+ val (types, payload) = entityTypesAndPayload
+ logger.debug(
+ "Sending {} event for attribute {} of entity {} in tenant {}",
+ attributeOperationResult.operationStatus,
+ attributeName,
+ entityId,
+ tenantName
+ )
+ when (attributeOperationResult.operationStatus) {
+ OperationStatus.APPENDED ->
publishEntityEvent(
AttributeAppendEvent(
sub,
tenantName,
entityId,
- entityTypesAndPayload.first,
- serializedAttribute.first,
- updatedDetails.datasetId,
- overwrite,
- serializedAttribute.second,
- entityTypesAndPayload.second,
+ types,
+ attributeOperationResult.attributeName,
+ attributeOperationResult.datasetId,
+ serializeObject(attributeOperationResult.newExpandedValue),
+ payload,
emptyList()
)
)
- UpdateOperationResult.REPLACED ->
+ OperationStatus.REPLACED ->
publishEntityEvent(
AttributeReplaceEvent(
sub,
tenantName,
entityId,
- entityTypesAndPayload.first,
- serializedAttribute.first,
- updatedDetails.datasetId,
- serializedAttribute.second,
- entityTypesAndPayload.second,
+ types,
+ attributeOperationResult.attributeName,
+ attributeOperationResult.datasetId,
+ serializeObject(attributeOperationResult.newExpandedValue),
+ payload,
emptyList()
)
)
- UpdateOperationResult.UPDATED ->
+ OperationStatus.UPDATED ->
publishEntityEvent(
AttributeUpdateEvent(
sub,
tenantName,
entityId,
- entityTypesAndPayload.first,
- serializedAttribute.first,
- updatedDetails.datasetId,
- serializedAttribute.second,
- entityTypesAndPayload.second,
+ types,
+ attributeOperationResult.attributeName,
+ attributeOperationResult.datasetId,
+ serializeObject(attributeOperationResult.newExpandedValue),
+ payload,
+ emptyList()
+ )
+ )
+
+ OperationStatus.DELETED ->
+ publishEntityEvent(
+ AttributeDeleteEvent(
+ sub,
+ tenantName,
+ entityId,
+ types,
+ attributeOperationResult.attributeName,
+ attributeOperationResult.datasetId,
+ injectDeletedAttribute(payload, attributeName, attributeOperationResult.newExpandedValue),
emptyList()
)
)
else ->
logger.warn(
- "Received an unexpected result (${updatedDetails.updateOperationResult} " +
- "for entity $entityId and attribute ${updatedDetails.attributeName}"
+ "Received an unexpected result (${attributeOperationResult.operationStatus} " +
+ "for entity $entityId and attribute ${attributeOperationResult.attributeName}"
)
}
}
@@ -199,11 +211,10 @@ class EntityEventService(
suspend fun publishAttributeDeleteEvent(
sub: String?,
entityId: URI,
- attributeName: ExpandedTerm,
- datasetId: URI? = null,
- deleteAll: Boolean
+ attributeOperationResult: SucceededAttributeOperationResult
): Job {
val tenantName = getTenantFromContext()
+ val attributeName = attributeOperationResult.attributeName
val entity = getSerializedEntity(entityId)
return coroutineScope.launch {
logger.debug(
@@ -213,31 +224,18 @@ class EntityEventService(
tenantName
)
entity.onRight {
- if (deleteAll)
- publishEntityEvent(
- AttributeDeleteAllInstancesEvent(
- sub,
- tenantName,
- entityId,
- it.first,
- attributeName,
- it.second,
- emptyList()
- )
- )
- else
- publishEntityEvent(
- AttributeDeleteEvent(
- sub,
- tenantName,
- entityId,
- it.first,
- attributeName,
- datasetId,
- it.second,
- emptyList()
- )
+ publishEntityEvent(
+ AttributeDeleteEvent(
+ sub,
+ tenantName,
+ entityId,
+ it.first,
+ attributeName,
+ attributeOperationResult.datasetId,
+ injectDeletedAttribute(it.second, attributeName, attributeOperationResult.newExpandedValue),
+ emptyList()
)
+ )
}.logAttributeEvent("Attribute Delete", entityId, tenantName)
}
}
@@ -250,21 +248,19 @@ class EntityEventService(
Pair(it.types, it.payload.asString())
}
- private fun getSerializedAttribute(
- jsonLdAttributes: Map,
+ internal fun injectDeletedAttribute(
+ entityPayload: String,
attributeName: ExpandedTerm,
- datasetId: URI?
- ): Pair =
- if (attributeName == JSONLD_TYPE) {
- Pair(JSONLD_TYPE, serializeObject(jsonLdAttributes[JSONLD_TYPE]!!))
- } else {
- val extractedPayload = (jsonLdAttributes as ExpandedAttributes).getAttributeFromExpandedAttributes(
- attributeName,
- datasetId
- )!!
- Pair(attributeName, serializeObject(extractedPayload))
+ attributeInstance: ExpandedAttributeInstance
+ ): String {
+ val entityPayload = entityPayload.deserializeAsMap().toMutableMap()
+ entityPayload.merge(attributeName, listOf(attributeInstance)) { currentValue, newValue ->
+ (currentValue as List).plus(newValue as List)
}
+ return serializeObject(entityPayload)
+ }
+
private fun Either.logEntityEvent(eventsType: EventsType, entityId: URI, tenantName: String) =
this.fold({
logger.error("Error sending {} event for entity {} in tenant {}: {}", eventsType, entityId, tenantName, it)
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityQueryService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityQueryService.kt
index 7c756b569..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
@@ -18,14 +18,12 @@ import com.egm.stellio.search.entity.model.EntitiesQueryFromPost
import com.egm.stellio.search.entity.model.Entity
import com.egm.stellio.search.entity.util.rowToEntity
import com.egm.stellio.shared.model.APIException
-import com.egm.stellio.shared.model.AlreadyExistsException
import com.egm.stellio.shared.model.ExpandedEntity
import com.egm.stellio.shared.model.ResourceNotFoundException
import com.egm.stellio.shared.util.Sub
import com.egm.stellio.shared.util.buildQQuery
import com.egm.stellio.shared.util.buildScopeQQuery
import com.egm.stellio.shared.util.buildTypeQuery
-import com.egm.stellio.shared.util.entityAlreadyExistsMessage
import com.egm.stellio.shared.util.entityNotFoundMessage
import org.springframework.r2dbc.core.DatabaseClient
import org.springframework.stereotype.Service
@@ -40,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(
@@ -73,6 +70,13 @@ class EntityQueryService(
suspend fun queryEntities(
entitiesQuery: EntitiesQuery,
accessRightFilter: () -> String?
+ ): List =
+ queryEntities(entitiesQuery, true, accessRightFilter)
+
+ suspend fun queryEntities(
+ entitiesQuery: EntitiesQuery,
+ excludedDeleted: Boolean = true,
+ accessRightFilter: () -> String?
): List {
val filterQuery = buildFullEntitiesFilter(entitiesQuery, accessRightFilter)
@@ -81,8 +85,10 @@ class EntityQueryService(
SELECT DISTINCT(entity_payload.entity_id)
FROM entity_payload
LEFT JOIN temporal_entity_attribute tea
- ON tea.entity_id = entity_payload.entity_id
+ ON tea.entity_id = entity_payload.entity_id
+ ${if (excludedDeleted) " AND tea.deleted_at is null " else ""}
WHERE $filterQuery
+ ${if (excludedDeleted) " AND entity_payload.deleted_at is null " else ""}
ORDER BY entity_id
LIMIT :limit
OFFSET :offset
@@ -98,6 +104,13 @@ class EntityQueryService(
suspend fun queryEntitiesCount(
entitiesQuery: EntitiesQuery,
accessRightFilter: () -> String?
+ ): Either =
+ queryEntitiesCount(entitiesQuery, true, accessRightFilter)
+
+ suspend fun queryEntitiesCount(
+ entitiesQuery: EntitiesQuery,
+ excludedDeleted: Boolean = true,
+ accessRightFilter: () -> String?
): Either {
val filterQuery = buildFullEntitiesFilter(entitiesQuery, accessRightFilter)
@@ -107,7 +120,9 @@ class EntityQueryService(
FROM entity_payload
LEFT JOIN temporal_entity_attribute tea
ON tea.entity_id = entity_payload.entity_id
+ ${if (excludedDeleted) " AND tea.deleted_at is null " else ""}
WHERE $filterQuery
+ ${if (excludedDeleted) " AND entity_payload.deleted_at is null " else ""}
""".trimIndent()
return databaseClient
@@ -212,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(
@@ -232,10 +248,7 @@ class EntityQueryService(
.bind("entities_ids", entitiesIds)
.allToMappedList { it.rowToEntity() }
- suspend fun checkEntityExistence(
- entityId: URI,
- inverse: Boolean = false
- ): Either {
+ suspend fun checkEntityExistence(entityId: URI, allowDeleted: Boolean = false): Either {
val selectQuery =
"""
select
@@ -243,6 +256,7 @@ class EntityQueryService(
select 1
from entity_payload
where entity_id = :entity_id
+ ${if (!allowDeleted) " and deleted_at is null " else ""}
) as entityExists;
""".trimIndent()
@@ -251,15 +265,32 @@ class EntityQueryService(
.bind("entity_id", entityId)
.oneToResult { it["entityExists"] as Boolean }
.flatMap {
- if (it && !inverse || !it && inverse)
+ if (it)
Unit.right()
- else if (it)
- AlreadyExistsException(entityAlreadyExistsMessage(entityId.toString())).left()
else
ResourceNotFoundException(entityNotFoundMessage(entityId.toString())).left()
}
}
+ /**
+ * Used for checks before creating a (temporal) entity. Allows to know if the entity does not exist,
+ * or, if it exists, whether it is currently deleted (in which case, it may be possible to create it again
+ * if authorized)
+ */
+ suspend fun isMarkedAsDeleted(entityId: URI): Either {
+ val selectQuery =
+ """
+ select entity_id, deleted_at
+ from entity_payload
+ where entity_id = :entity_id
+ """.trimIndent()
+
+ return databaseClient
+ .sql(selectQuery)
+ .bind("entity_id", entityId)
+ .oneToResult { it["deleted_at"] != null }
+ }
+
suspend fun filterExistingEntitiesAsIds(entitiesIds: List): List {
if (entitiesIds.isEmpty()) {
return emptyList()
@@ -270,6 +301,7 @@ class EntityQueryService(
select entity_id
from entity_payload
where entity_id in (:entities_ids)
+ and deleted_at is null
""".trimIndent()
return databaseClient
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 800d741a5..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
@@ -1,6 +1,9 @@
package com.egm.stellio.search.entity.service
import arrow.core.Either
+import arrow.core.Either.Left
+import arrow.core.Either.Right
+import arrow.core.left
import arrow.core.raise.either
import arrow.core.right
import arrow.core.toOption
@@ -10,21 +13,25 @@ import com.egm.stellio.search.common.util.execute
import com.egm.stellio.search.common.util.oneToResult
import com.egm.stellio.search.common.util.toZonedDateTime
import com.egm.stellio.search.entity.model.Attribute
-import com.egm.stellio.search.entity.model.EMPTY_UPDATE_RESULT
+import com.egm.stellio.search.entity.model.AttributeOperationResult
import com.egm.stellio.search.entity.model.Entity
+import com.egm.stellio.search.entity.model.FailedAttributeOperationResult
+import com.egm.stellio.search.entity.model.OperationStatus
import com.egm.stellio.search.entity.model.OperationType
import com.egm.stellio.search.entity.model.OperationType.APPEND_ATTRIBUTES
import com.egm.stellio.search.entity.model.OperationType.APPEND_ATTRIBUTES_OVERWRITE_ALLOWED
import com.egm.stellio.search.entity.model.OperationType.MERGE_ENTITY
import com.egm.stellio.search.entity.model.OperationType.UPDATE_ATTRIBUTES
-import com.egm.stellio.search.entity.model.UpdateAttributeResult
-import com.egm.stellio.search.entity.model.UpdateOperationResult
+import com.egm.stellio.search.entity.model.SucceededAttributeOperationResult
import com.egm.stellio.search.entity.model.UpdateResult
-import com.egm.stellio.search.entity.model.updateResultFromDetailedResult
+import com.egm.stellio.search.entity.model.getSucceededOperations
+import com.egm.stellio.search.entity.model.hasSuccessfulResult
import com.egm.stellio.search.entity.util.prepareAttributes
import com.egm.stellio.search.entity.util.rowToEntity
import com.egm.stellio.search.scope.ScopeService
+import com.egm.stellio.search.temporal.model.AttributeInstance.TemporalProperty
import com.egm.stellio.shared.model.APIException
+import com.egm.stellio.shared.model.AlreadyExistsException
import com.egm.stellio.shared.model.ExpandedAttribute
import com.egm.stellio.shared.model.ExpandedAttributeInstances
import com.egm.stellio.shared.model.ExpandedAttributes
@@ -32,13 +39,14 @@ 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
import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_TYPE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_SCOPE_PROPERTY
import com.egm.stellio.shared.util.JsonUtils.serializeObject
import com.egm.stellio.shared.util.Sub
+import com.egm.stellio.shared.util.entityAlreadyExistsMessage
import com.egm.stellio.shared.util.getSpecificAccessPolicy
import com.egm.stellio.shared.util.ngsiLdDateTime
import io.r2dbc.postgresql.codec.Json
@@ -48,7 +56,6 @@ import org.springframework.r2dbc.core.bind
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.net.URI
-import java.time.ZoneOffset
import java.time.ZonedDateTime
@Service
@@ -68,14 +75,23 @@ class EntityService(
expandedEntity: ExpandedEntity,
sub: Sub? = null
): Either = either {
- authorizationService.userCanCreateEntities(sub.toOption()).bind()
- entityQueryService.checkEntityExistence(ngsiLdEntity.id, true).bind()
+ entityQueryService.isMarkedAsDeleted(ngsiLdEntity.id).let {
+ when (it) {
+ is Left -> authorizationService.userCanCreateEntities(sub.toOption()).bind()
+ is Right ->
+ if (!it.value)
+ AlreadyExistsException(entityAlreadyExistsMessage(ngsiLdEntity.id.toString())).left().bind()
+ else
+ authorizationService.userCanAdminEntity(ngsiLdEntity.id, sub.toOption()).bind()
+ }
+ }
val createdAt = ngsiLdDateTime()
val attributesMetadata = ngsiLdEntity.prepareAttributes().bind()
logger.debug("Creating entity {}", ngsiLdEntity.id)
- createEntityPayload(ngsiLdEntity, expandedEntity, createdAt, sub).bind()
+ createEntityPayload(ngsiLdEntity, expandedEntity, createdAt).bind()
+ scopeService.createHistory(ngsiLdEntity, createdAt, sub).bind()
entityAttributeService.createAttributes(
ngsiLdEntity,
expandedEntity,
@@ -96,14 +112,20 @@ class EntityService(
suspend fun createEntityPayload(
ngsiLdEntity: NgsiLdEntity,
expandedEntity: ExpandedEntity,
- createdAt: ZonedDateTime,
- sub: Sub? = null
+ createdAt: ZonedDateTime
): Either = either {
val specificAccessPolicy = ngsiLdEntity.getSpecificAccessPolicy()?.bind()
databaseClient.sql(
"""
INSERT INTO entity_payload (entity_id, types, scopes, created_at, payload, specific_access_policy)
VALUES (:entity_id, :types, :scopes, :created_at, :payload, :specific_access_policy)
+ ON CONFLICT (entity_id)
+ DO UPDATE SET types = :types,
+ scopes = :scopes,
+ modified_at = :created_at,
+ deleted_at = null,
+ payload = :payload,
+ specific_access_policy = :specific_access_policy
""".trimIndent()
)
.bind("entity_id", ngsiLdEntity.id)
@@ -113,9 +135,6 @@ class EntityService(
.bind("payload", Json.of(serializeObject(expandedEntity.populateCreationTimeDate(createdAt).members)))
.bind("specific_access_policy", specificAccessPolicy?.toString())
.execute()
- .map {
- scopeService.createHistory(ngsiLdEntity, createdAt, sub)
- }
}
@Transactional
@@ -129,12 +148,15 @@ class EntityService(
authorizationService.userCanUpdateEntity(entityId, sub.toOption()).bind()
val (coreAttrs, otherAttrs) =
- expandedAttributes.toList().partition { JSONLD_EXPANDED_ENTITY_SPECIFIC_MEMBERS.contains(it.first) }
+ expandedAttributes.toList()
+ // remove @id if it is present (optional as per 5.4)
+ .filter { it.first != JSONLD_ID }
+ .partition { JSONLD_EXPANDED_ENTITY_SPECIFIC_MEMBERS.contains(it.first) }
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,
@@ -143,24 +165,20 @@ class EntityService(
sub
).bind()
- val updateResult = coreUpdateResult.mergeWith(attrsUpdateResult)
+ val operationResult = coreOperationResult.plus(attrsOperationResult)
// update modifiedAt in entity if at least one attribute has been merged
- if (updateResult.hasSuccessfulUpdate()) {
+ if (operationResult.hasSuccessfulResult()) {
val attributes = entityAttributeService.getForEntity(entityId, emptySet(), emptySet())
updateState(entityId, mergedAt, attributes).bind()
- }
- if (updateResult.updated.isNotEmpty()) {
entityEventService.publishAttributeChangeEvents(
sub,
entityId,
- expandedAttributes,
- updateResult,
- true
+ operationResult.getSucceededOperations()
)
}
- updateResult
+ UpdateResult(operationResult)
}
@Transactional
@@ -173,13 +191,14 @@ class EntityService(
entityQueryService.checkEntityExistence(entityId).bind()
authorizationService.userCanUpdateEntity(entityId, sub.toOption()).bind()
- val replacedAt = ngsiLdDateTime()
val attributesMetadata = ngsiLdEntity.prepareAttributes().bind()
logger.debug("Replacing entity {}", ngsiLdEntity.id)
- entityAttributeService.deleteAttributes(entityId)
+ entityAttributeService.deleteAttributes(entityId, ngsiLdDateTime()).bind()
- replaceEntityPayload(ngsiLdEntity, expandedEntity, replacedAt, sub).bind()
+ val replacedAt = ngsiLdDateTime()
+ replaceEntityPayload(ngsiLdEntity, expandedEntity, replacedAt).bind()
+ scopeService.replace(ngsiLdEntity, replacedAt, sub).bind()
entityAttributeService.createAttributes(
ngsiLdEntity,
expandedEntity,
@@ -199,8 +218,7 @@ class EntityService(
suspend fun replaceEntityPayload(
ngsiLdEntity: NgsiLdEntity,
expandedEntity: ExpandedEntity,
- replacedAt: ZonedDateTime,
- sub: Sub? = null
+ replacedAt: ZonedDateTime
): Either = either {
val specificAccessPolicy = ngsiLdEntity.getSpecificAccessPolicy()?.bind()
val createdAt = retrieveCreatedAt(ngsiLdEntity.id).bind()
@@ -225,9 +243,6 @@ class EntityService(
.bind("payload", Json.of(serializedPayload))
.bind("specific_access_policy", specificAccessPolicy?.toString())
.execute()
- .map {
- scopeService.replaceHistoryEntry(ngsiLdEntity, createdAt, sub)
- }
}
private suspend fun retrieveCreatedAt(entityId: URI): Either =
@@ -246,7 +261,7 @@ class EntityService(
coreAttrs: List>,
modifiedAt: ZonedDateTime,
operationType: OperationType
- ): Either = either {
+ ): Either> = either {
coreAttrs.map { (expandedTerm, expandedAttributeInstances) ->
when (expandedTerm) {
JSONLD_TYPE ->
@@ -255,11 +270,14 @@ class EntityService(
scopeService.update(entityId, expandedAttributeInstances, modifiedAt, operationType).bind()
else -> {
logger.warn("Ignoring unhandled core property: {}", expandedTerm)
- EMPTY_UPDATE_RESULT.right().bind()
+ FailedAttributeOperationResult(
+ attributeName = expandedTerm,
+ operationStatus = OperationStatus.IGNORED,
+ errorMessage = "Ignoring unhandled core property: $expandedTerm"
+ ).right().bind()
}
}
- }.ifEmpty { listOf(EMPTY_UPDATE_RESULT) }
- .reduce { acc, cur -> acc.mergeWith(cur) }
+ }
}
@Transactional
@@ -268,12 +286,16 @@ class EntityService(
newTypes: List,
modifiedAt: ZonedDateTime,
allowEmptyListOfTypes: Boolean = true
- ): Either = either {
+ ): Either = either {
val entityPayload = entityQueryService.retrieve(entityId).bind()
val currentTypes = entityPayload.types
// when dealing with an entity update, list of types can be empty if no change of type is requested
if (currentTypes.sorted() == newTypes.sorted() || newTypes.isEmpty() && allowEmptyListOfTypes)
- return@either UpdateResult(emptyList(), emptyList())
+ return@either SucceededAttributeOperationResult(
+ attributeName = JSONLD_TYPE,
+ operationStatus = OperationStatus.APPENDED,
+ newExpandedValue = mapOf(JSONLD_TYPE to currentTypes.toList())
+ )
val updatedTypes = currentTypes.union(newTypes)
val updatedPayload = entityPayload.payload.deserializeExpandedPayload()
@@ -298,13 +320,10 @@ class EntityService(
.bind("payload", Json.of(serializeObject(updatedPayload)))
.execute()
.map {
- updateResultFromDetailedResult(
- listOf(
- UpdateAttributeResult(
- attributeName = JSONLD_TYPE,
- updateOperationResult = UpdateOperationResult.APPENDED
- )
- )
+ SucceededAttributeOperationResult(
+ attributeName = JSONLD_TYPE,
+ operationStatus = OperationStatus.APPENDED,
+ newExpandedValue = mapOf(JSONLD_TYPE to updatedTypes.toList())
)
}.bind()
}
@@ -326,8 +345,8 @@ class EntityService(
val operationType =
if (disallowOverwrite) APPEND_ATTRIBUTES
else APPEND_ATTRIBUTES_OVERWRITE_ALLOWED
- val coreUpdateResult = updateCoreAttributes(entityId, coreAttrs, createdAt, operationType).bind()
- val attrsUpdateResult = entityAttributeService.appendAttributes(
+ val coreOperationResult = updateCoreAttributes(entityId, coreAttrs, createdAt, operationType).bind()
+ val attrsOperationResult = entityAttributeService.appendAttributes(
entityId,
otherAttrs.toMap().toNgsiLdAttributes().bind(),
expandedAttributes,
@@ -336,24 +355,10 @@ class EntityService(
sub
).bind()
- val updateResult = coreUpdateResult.mergeWith(attrsUpdateResult)
- // update modifiedAt in entity if at least one attribute has been added
- if (updateResult.hasSuccessfulUpdate()) {
- val attributes = entityAttributeService.getForEntity(entityId, emptySet(), emptySet())
- updateState(entityId, createdAt, attributes).bind()
- }
-
- if (updateResult.hasSuccessfulUpdate()) {
- entityEventService.publishAttributeChangeEvents(
- sub,
- entityId,
- expandedAttributes,
- updateResult,
- true
- )
- }
+ val operationResult = coreOperationResult.plus(attrsOperationResult)
+ handleSuccessOperationActions(operationResult, entityId, createdAt, sub).bind()
- updateResult
+ UpdateResult(operationResult)
}
@Transactional
@@ -369,8 +374,8 @@ class EntityService(
expandedAttributes.toList().partition { JSONLD_EXPANDED_ENTITY_SPECIFIC_MEMBERS.contains(it.first) }
val createdAt = ngsiLdDateTime()
- val coreUpdateResult = updateCoreAttributes(entityId, coreAttrs, createdAt, UPDATE_ATTRIBUTES).bind()
- val attrsUpdateResult = entityAttributeService.updateAttributes(
+ val coreOperationResult = updateCoreAttributes(entityId, coreAttrs, createdAt, UPDATE_ATTRIBUTES).bind()
+ val attrsOperationResult = entityAttributeService.updateAttributes(
entityId,
otherAttrs.toMap().toNgsiLdAttributes().bind(),
expandedAttributes,
@@ -378,24 +383,10 @@ class EntityService(
sub
).bind()
- val updateResult = coreUpdateResult.mergeWith(attrsUpdateResult)
- // update modifiedAt in entity if at least one attribute has been added
- if (updateResult.hasSuccessfulUpdate()) {
- val attributes = entityAttributeService.getForEntity(entityId, emptySet(), emptySet())
- updateState(entityId, createdAt, attributes).bind()
- }
-
- if (updateResult.updated.isNotEmpty()) {
- entityEventService.publishAttributeChangeEvents(
- sub,
- entityId,
- expandedAttributes,
- updateResult,
- true
- )
- }
+ val operationResult = coreOperationResult.plus(attrsOperationResult)
+ handleSuccessOperationActions(operationResult, entityId, createdAt, sub).bind()
- updateResult
+ UpdateResult(operationResult)
}
@Transactional
@@ -409,28 +400,16 @@ class EntityService(
val modifiedAt = ngsiLdDateTime()
- val updateResult = entityAttributeService.partialUpdateAttribute(
+ val operationResult = entityAttributeService.partialUpdateAttribute(
entityId,
expandedAttribute,
modifiedAt,
sub
- ).bind()
+ ).bind().let { listOf(it) }
- if (updateResult.isSuccessful()) {
- val attributes = entityAttributeService.getForEntity(entityId, emptySet(), emptySet())
- updateState(entityId, modifiedAt, attributes).bind()
- }
-
- if (updateResult.updated.isNotEmpty())
- entityEventService.publishAttributeChangeEvents(
- sub,
- entityId,
- expandedAttribute.toExpandedAttributes(),
- updateResult,
- false
- )
+ handleSuccessOperationActions(operationResult, entityId, modifiedAt, sub).bind()
- updateResult
+ UpdateResult(operationResult)
}
@Transactional
@@ -439,7 +418,7 @@ class EntityService(
expandedAttributes: ExpandedAttributes,
sub: Sub? = null
): Either = either {
- val createdAt = ZonedDateTime.now(ZoneOffset.UTC)
+ val createdAt = ngsiLdDateTime()
expandedAttributes.forEach { (attributeName, expandedAttributeInstances) ->
expandedAttributeInstances.forEach { expandedAttributeInstance ->
val jsonLdAttribute = mapOf(attributeName to listOf(expandedAttributeInstance))
@@ -473,30 +452,37 @@ class EntityService(
val ngsiLdAttribute = listOf(expandedAttribute).toMap().toNgsiLdAttributes().bind()[0]
val replacedAt = ngsiLdDateTime()
- val updateResult = entityAttributeService.replaceAttribute(
+ val operationResult = entityAttributeService.replaceAttribute(
entityId,
ngsiLdAttribute,
expandedAttribute,
replacedAt,
sub
- ).bind()
+ ).bind().let { listOf(it) }
+
+ handleSuccessOperationActions(operationResult, entityId, replacedAt, sub).bind()
+
+ UpdateResult(operationResult)
+ }
+ @Transactional
+ internal suspend fun handleSuccessOperationActions(
+ operationResult: List,
+ entityId: URI,
+ createdAt: ZonedDateTime,
+ sub: Sub? = null
+ ): Either = either {
// update modifiedAt in entity if at least one attribute has been added
- if (updateResult.hasSuccessfulUpdate()) {
+ if (operationResult.hasSuccessfulResult()) {
val attributes = entityAttributeService.getForEntity(entityId, emptySet(), emptySet())
- updateState(entityId, replacedAt, attributes).bind()
- }
+ updateState(entityId, createdAt, attributes).bind()
- if (updateResult.updated.isNotEmpty())
entityEventService.publishAttributeChangeEvents(
sub,
entityId,
- expandedAttribute.toExpandedAttributes(),
- updateResult,
- false
+ operationResult.getSucceededOperations()
)
-
- updateResult
+ }
}
@Transactional
@@ -545,36 +531,72 @@ class EntityService(
}
@Transactional
- suspend fun upsertEntityPayload(entityId: URI, payload: String): Either =
+ suspend fun deleteEntity(entityId: URI, sub: Sub? = null): Either = either {
+ val currentEntity = entityQueryService.retrieve(entityId).bind()
+ authorizationService.userCanAdminEntity(entityId, sub.toOption()).bind()
+
+ val deletedAt = ngsiLdDateTime()
+ val deletedEntityPayload = currentEntity.toExpandedDeletedEntity(deletedAt)
+ val previousEntity = deleteEntityPayload(entityId, deletedAt, deletedEntityPayload).bind()
+ entityAttributeService.deleteAttributes(entityId, deletedAt).bind()
+ scopeService.addHistoryEntry(entityId, emptyList(), TemporalProperty.DELETED_AT, deletedAt, sub).bind()
+
+ entityEventService.publishEntityDeleteEvent(sub, previousEntity, deletedEntityPayload)
+ }
+
+ @Transactional
+ suspend fun deleteEntityPayload(
+ entityId: URI,
+ deletedAt: ZonedDateTime,
+ deletedEntityPayload: ExpandedEntity
+ ): Either = either {
databaseClient.sql(
"""
- INSERT INTO entity_payload (entity_id, payload)
- VALUES (:entity_id, :payload)
- ON CONFLICT (entity_id)
- DO UPDATE SET payload = :payload
+ 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("payload", Json.of(payload))
.bind("entity_id", entityId)
- .execute()
+ .bind("deleted_at", deletedAt)
+ .bind("payload", Json.of(serializeObject(deletedEntityPayload.members)))
+ .oneToResult {
+ it.rowToEntity()
+ }
+ .bind()
+ }
@Transactional
- suspend fun deleteEntity(entityId: URI, sub: Sub? = null): Either = either {
- entityQueryService.checkEntityExistence(entityId).bind()
+ suspend fun permanentlyDeleteEntity(entityId: URI, sub: Sub? = null): Either = either {
+ val currentEntity = entityQueryService.retrieve(entityId, true).bind()
authorizationService.userCanAdminEntity(entityId, sub.toOption()).bind()
- val entity = deleteEntityPayload(entityId).bind()
-
- entityAttributeService.deleteAttributes(entityId).bind()
- scopeService.deleteHistory(entityId).bind()
+ val previousEntity = permanentyDeleteEntityPayload(entityId).bind()
+ entityAttributeService.permanentlyDeleteAttributes(entityId).bind()
authorizationService.removeRightsOnEntity(entityId).bind()
- entityEventService.publishEntityDeleteEvent(sub, entity)
+ if (currentEntity.deletedAt == null) {
+ // only send a notification if entity was not already previously deleted
+ val deletedEntityPayload = currentEntity.toExpandedDeletedEntity(ngsiLdDateTime())
+ entityEventService.publishEntityDeleteEvent(sub, previousEntity, deletedEntityPayload)
+ }
}
@Transactional
- suspend fun deleteEntityPayload(entityId: URI): Either = either {
- val entity = databaseClient.sql(
+ suspend fun permanentyDeleteEntityPayload(entityId: URI): Either = either {
+ databaseClient.sql(
"""
DELETE FROM entity_payload
WHERE entity_id = :entity_id
@@ -586,7 +608,6 @@ class EntityService(
it.rowToEntity()
}
.bind()
- entity
}
@Transactional
@@ -599,7 +620,7 @@ class EntityService(
): Either = either {
authorizationService.userCanUpdateEntity(entityId, sub.toOption()).bind()
- if (attributeName == NGSILD_SCOPE_PROPERTY) {
+ val deleteAttributeResults = if (attributeName == NGSILD_SCOPE_PROPERTY) {
scopeService.delete(entityId).bind()
} else {
entityAttributeService.checkEntityAndAttributeExistence(
@@ -612,7 +633,8 @@ class EntityService(
entityId,
attributeName,
datasetId,
- deleteAll
+ deleteAll,
+ ngsiLdDateTime()
).bind()
}
updateState(
@@ -621,12 +643,41 @@ class EntityService(
entityAttributeService.getForEntity(entityId, emptySet(), emptySet())
).bind()
- entityEventService.publishAttributeDeleteEvent(
- sub,
+ deleteAttributeResults.filterIsInstance()
+ .forEach {
+ entityEventService.publishAttributeDeleteEvent(sub, entityId, it)
+ }
+ }
+
+ @Transactional
+ suspend fun permanentlyDeleteAttribute(
+ entityId: URI,
+ attributeName: ExpandedTerm,
+ datasetId: URI?,
+ deleteAll: Boolean = false,
+ sub: Sub? = null
+ ): Either = either {
+ authorizationService.userCanUpdateEntity(entityId, sub.toOption()).bind()
+
+ if (attributeName == NGSILD_SCOPE_PROPERTY) {
+ scopeService.permanentlyDelete(entityId).bind()
+ } else {
+ entityAttributeService.checkEntityAndAttributeExistence(
+ entityId,
+ attributeName,
+ datasetId
+ ).bind()
+ entityAttributeService.permanentlyDeleteAttribute(
+ entityId,
+ attributeName,
+ datasetId,
+ deleteAll
+ ).bind()
+ }
+ updateState(
entityId,
- attributeName,
- datasetId,
- deleteAll
- )
+ ngsiLdDateTime(),
+ entityAttributeService.getForEntity(entityId, emptySet(), emptySet())
+ ).bind()
}
}
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/util/AttributeUtils.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/util/AttributeUtils.kt
index 32d2c2c35..af8e883d6 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/util/AttributeUtils.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/util/AttributeUtils.kt
@@ -7,6 +7,7 @@ import arrow.core.right
import com.egm.stellio.search.common.util.deserializeAsMap
import com.egm.stellio.search.common.util.valueToDoubleOrNull
import com.egm.stellio.search.entity.model.Attribute
+import com.egm.stellio.search.entity.model.Attribute.AttributeType
import com.egm.stellio.search.entity.model.AttributeMetadata
import com.egm.stellio.shared.model.APIException
import com.egm.stellio.shared.model.BadRequestDataException
@@ -20,12 +21,17 @@ import com.egm.stellio.shared.model.NgsiLdPropertyInstance
import com.egm.stellio.shared.model.NgsiLdRelationshipInstance
import com.egm.stellio.shared.model.NgsiLdVocabPropertyInstance
import com.egm.stellio.shared.model.WKTCoordinates
+import com.egm.stellio.shared.model.getMemberValue
import com.egm.stellio.shared.model.getPropertyValue
+import com.egm.stellio.shared.model.getRelationshipId
import com.egm.stellio.shared.util.JsonLdUtils
+import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_LANGUAGEPROPERTY_VALUE
+import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_NULL
import com.egm.stellio.shared.util.JsonUtils
import com.egm.stellio.shared.util.JsonUtils.deserializeAsMap
import com.savvasdalkitsis.jsonmerger.JsonMerger
import io.r2dbc.postgresql.codec.Json
+import java.net.URI
import java.time.LocalDate
import java.time.LocalTime
import java.time.ZonedDateTime
@@ -47,35 +53,35 @@ fun NgsiLdAttributeInstance.toAttributeMetadata(): Either
guessPropertyValueType(this).let {
- Triple(Attribute.AttributeType.Property, it.first, it.second)
+ Triple(AttributeType.Property, it.first, it.second)
}
is NgsiLdRelationshipInstance ->
Triple(
- Attribute.AttributeType.Relationship,
+ AttributeType.Relationship,
Attribute.AttributeValueType.URI,
Triple(this.objectId.toString(), null, null)
)
is NgsiLdGeoPropertyInstance ->
Triple(
- Attribute.AttributeType.GeoProperty,
+ AttributeType.GeoProperty,
Attribute.AttributeValueType.GEOMETRY,
Triple(null, null, this.coordinates)
)
is NgsiLdJsonPropertyInstance ->
Triple(
- Attribute.AttributeType.JsonProperty,
+ AttributeType.JsonProperty,
Attribute.AttributeValueType.JSON,
Triple(JsonUtils.serializeObject(this.json), null, null)
)
is NgsiLdLanguagePropertyInstance ->
Triple(
- Attribute.AttributeType.LanguageProperty,
+ AttributeType.LanguageProperty,
Attribute.AttributeValueType.ARRAY,
Triple(JsonUtils.serializeObject(this.languageMap), null, null)
)
is NgsiLdVocabPropertyInstance ->
Triple(
- Attribute.AttributeType.VocabProperty,
+ AttributeType.VocabProperty,
Attribute.AttributeValueType.ARRAY,
Triple(JsonUtils.serializeObject(this.vocab), null, null)
)
@@ -97,17 +103,17 @@ fun NgsiLdAttributeInstance.toAttributeMetadata(): Either
+ AttributeType.Property ->
guessPropertyValueType(expandedAttributeInstance.getPropertyValue()!!).first
- Attribute.AttributeType.Relationship -> Attribute.AttributeValueType.URI
- Attribute.AttributeType.GeoProperty -> Attribute.AttributeValueType.GEOMETRY
- Attribute.AttributeType.JsonProperty -> Attribute.AttributeValueType.JSON
- Attribute.AttributeType.LanguageProperty -> Attribute.AttributeValueType.ARRAY
- Attribute.AttributeType.VocabProperty -> Attribute.AttributeValueType.ARRAY
+ AttributeType.Relationship -> Attribute.AttributeValueType.URI
+ AttributeType.GeoProperty -> Attribute.AttributeValueType.GEOMETRY
+ AttributeType.JsonProperty -> Attribute.AttributeValueType.JSON
+ AttributeType.LanguageProperty -> Attribute.AttributeValueType.ARRAY
+ AttributeType.VocabProperty -> Attribute.AttributeValueType.ARRAY
}
fun guessPropertyValueType(
@@ -131,6 +137,21 @@ fun guessPropertyValueType(
else -> Pair(Attribute.AttributeValueType.STRING, Triple(value.toString(), null, null))
}
+/**
+ * Returns whether the expanded attribute instance holds a NGSI-LD Null value
+ */
+fun hasNgsiLdNullValue(
+ expandedAttributeInstance: ExpandedAttributeInstance,
+ attributeType: AttributeType
+): Boolean =
+ if (attributeType == AttributeType.Relationship) {
+ val value = expandedAttributeInstance.getRelationshipId()
+ value is URI && value.toString() == NGSILD_NULL
+ } else {
+ val value = expandedAttributeInstance.getMemberValue(attributeType.toExpandedValueMember())
+ value is String && value == NGSILD_NULL
+ }
+
fun Json.toExpandedAttributeInstance(): ExpandedAttributeInstance =
this.deserializeAsMap() as ExpandedAttributeInstance
@@ -169,7 +190,7 @@ fun mergePatch(
).deserializeAsMap()
)
}
- } else if (listOf(JsonLdUtils.NGSILD_LANGUAGEPROPERTY_VALUE).contains(attrName)) {
+ } else if (listOf(NGSILD_LANGUAGEPROPERTY_VALUE).contains(attrName)) {
val sourceLangEntries = source[attrName] as List