From 7d9c62f358e99cfcacfa1b22c5de8dc47df9a202 Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Fri, 30 Aug 2024 14:33:30 +0100 Subject: [PATCH] refactor: entity and temporal handlers before adding support for distOps (#1219) * refactor: introduce business oriented packaging (temporal/entities/discovery/common) * refactor: rename EntityPayloadService -> EntityService * refactor: rename EntityPayload to Entity and TemporalEntityAttribute to Attribute * refactor: entity and temporal handlers * fix(authz): delete user entities before deleting the user (if the user no longer exists, it fails because of access rights checks) * refactor: remove service logic from EntityOperationHandler (except upsert separation between create and update) * refactor: upsert logic in service --------- Co-authored-by: Thomas BOUSSELIN --- search-service/config/detekt/baseline.xml | 23 +- .../search/SearchServiceApplication.kt | 2 +- .../listener/IAMListener.kt | 38 +- .../{ => model}/EntityAccessRights.kt | 2 +- .../search/authorization/{ => model}/Group.kt | 2 +- .../{ => model}/SubjectAccessRight.kt | 2 +- .../{ => model}/SubjectReferential.kt | 4 +- .../search/authorization/{ => model}/User.kt | 2 +- .../{ => service}/AuthorizationService.kt | 4 +- .../DisabledAuthorizationService.kt | 4 +- .../EnabledAuthorizationService.kt | 4 +- .../EntityAccessRightsService.kt | 62 +- .../SubjectReferentialService.kt | 7 +- .../web/EntityAccessControlHandler.kt | 18 +- .../search/{ => common}/config/KafkaConfig.kt | 2 +- .../{ => common}/config/SearchProperties.kt | 2 +- .../search/{ => common}/config/WebConfig.kt | 2 +- .../search/{ => common}/model/Query.kt | 2 +- .../support/ApiTestsBootstrapper.kt | 6 +- .../{ => common}/tenant/DatabaseMigration.kt | 2 +- .../tenant/DatabaseTenantConfig.kt | 2 +- .../tenant/DatabaseTenantConnectionFactory.kt | 2 +- .../{ => common}/util/DBConversionUtils.kt | 16 +- .../search/{ => common}/util/DBQueryUtils.kt | 2 +- .../{ => discovery}/model/AttributeDetails.kt | 2 +- .../{ => discovery}/model/AttributeList.kt | 2 +- .../model/AttributeTypeInfo.kt | 2 +- .../{ => discovery}/model/EntityType.kt | 2 +- .../{ => discovery}/model/EntityTypeInfo.kt | 2 +- .../{ => discovery}/model/EntityTypeList.kt | 2 +- .../service/AttributeService.kt | 18 +- .../service/EntityTypeService.kt | 10 +- .../{ => discovery}/web/AttributeHandler.kt | 4 +- .../{ => discovery}/web/EntityTypeHandler.kt | 4 +- .../listener/ObservationEventListener.kt | 14 +- .../model/Attribute.kt} | 6 +- .../{ => entity}/model/AttributeMetadata.kt | 6 +- .../{ => entity}/model/EntitiesQuery.kt | 2 +- .../model/Entity.kt} | 4 +- .../{ => entity}/model/OperationType.kt | 2 +- .../search/{ => entity}/model/UpdateResult.kt | 2 +- .../service/EntityAttributeService.kt} | 209 ++-- .../service/EntityEventService.kt | 24 +- .../service/EntityOperationService.kt | 123 +- .../entity/service/EntityQueryService.kt | 225 ++++ .../service/EntityService.kt} | 538 +++----- .../search/entity/util/AttributeUtils.kt | 190 +++ .../search/entity/util/EntitiesQueryUtils.kt | 105 ++ .../stellio/search/entity/util/EntityUtils.kt | 16 + .../{ => entity}/web/BatchAPIResponses.kt | 4 +- .../search/{ => entity}/web/EntityHandler.kt | 159 +-- .../web/EntityOperationHandler.kt | 150 +-- .../search/model/EntityTemporalResult.kt | 10 - .../search/scope/ScopeInstanceResult.kt | 2 +- .../egm/stellio/search/scope/ScopeService.kt | 12 +- .../search/scope/TemporalScopeBuilder.kt | 10 +- .../{ => temporal}/model/AttributeInstance.kt | 15 +- .../model/AttributeInstanceResult.kt | 16 +- .../temporal/model/EntityTemporalResult.kt | 11 + .../model/TemporalEntitiesQuery.kt | 3 +- .../{ => temporal}/model/TemporalQuery.kt | 2 +- .../service/AttributeInstanceService.kt | 77 +- .../service/TemporalPaginationService.kt | 37 +- .../service/TemporalQueryService.kt} | 159 ++- .../temporal/service/TemporalService.kt | 148 +++ .../{ => temporal}/util/DBAggregationUtils.kt | 6 +- .../util/TemporalEntityBuilder.kt | 47 +- .../util/TemporalQueryParamsUtils.kt | 2 +- .../util/TemporalQueryUtils.kt} | 115 +- .../web/TemporalApiResponses.kt | 6 +- .../web/TemporalEntityHandler.kt | 142 +-- .../web/TemporalEntityOperationsHandler.kt | 20 +- .../search/util/AttributeInstanceUtils.kt | 192 --- .../db/migration/V0_29__JsonLd_migration.kt | 20 +- .../listener/IAMListenerTests.kt | 32 +- .../AuthorizationServiceTests.kt | 4 +- .../EnabledAuthorizationServiceTests.kt | 9 +- .../EntityAccessRightsServiceTests.kt | 205 +++- .../SubjectReferentialServiceTests.kt | 6 +- .../web/AnonymousUserHandlerTests.kt | 27 +- .../web/EntityAccessControlHandlerTests.kt | 34 +- .../service/AttributeServiceTests.kt | 92 +- .../service/EntityTypeServiceTests.kt | 117 +- .../web/AttributeHandlerTests.kt | 14 +- .../web/EntityTypeHandlerTests.kt | 10 +- .../listener/ObservationEventListenerTests.kt | 36 +- .../{ => entity}/model/EntityModelTests.kt | 8 +- .../{ => entity}/model/UpdateResultTests.kt | 2 +- .../service/EntityAttributeServiceTests.kt} | 174 +-- .../service/EntityEventServiceTests.kt | 64 +- .../service/EntityOperationServiceTests.kt | 317 +++-- .../entity/service/EntityQueryServiceTests.kt | 219 ++++ .../service/EntityServiceQueriesTests.kt} | 86 +- .../entity/service/EntityServiceTests.kt | 518 ++++++++ .../util/AttributeUtilsTests.kt} | 50 +- .../entity/util/EntitiesQueryUtilsTests.kt | 257 ++++ .../{ => entity}/util/PatchAttributeTests.kt | 6 +- .../{ => entity}/web/EntityHandlerTests.kt | 535 ++------ .../entity/web/EntityOperationHandlerTests.kt | 608 +++++++++ .../stellio/search/scope/ScopeServiceTests.kt | 35 +- .../search/scope/TemporalScopeBuilderTests.kt | 6 +- .../service/EntityPayloadServiceTests.kt | 747 ----------- .../search/support/BusinessObjectsFactory.kt | 46 +- .../egm/stellio/search/support/TestUtils.kt | 12 +- .../AggregatedTemporalQueryServiceTests.kt} | 105 +- .../service/AttributeInstanceServiceTests.kt | 222 ++-- .../service/TemporalPaginationServiceTests.kt | 83 +- .../service/TemporalQueryServiceTests.kt} | 179 ++- .../TemporalEntitiesParameterizedSource.kt | 72 +- .../util/TemporalEntityBuilderTests.kt | 42 +- .../util/TemporalEntityParameterizedSource.kt | 151 +-- .../util/TemporalQueryUtilsTests.kt} | 252 +--- .../TemporalEntityHandlerPaginationTests.kt | 22 +- .../web/TemporalEntityHandlerTestCommon.kt | 21 +- .../web/TemporalEntityHandlerTests.kt | 461 ++----- .../TemporalEntityOperationsHandlerTests.kt | 30 +- .../search/web/EntityOperationHandlerTests.kt | 1088 ----------------- 117 files changed, 4648 insertions(+), 5447 deletions(-) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => authorization}/listener/IAMListener.kt (85%) rename search-service/src/main/kotlin/com/egm/stellio/search/authorization/{ => model}/EntityAccessRights.kt (98%) rename search-service/src/main/kotlin/com/egm/stellio/search/authorization/{ => model}/Group.kt (95%) rename search-service/src/main/kotlin/com/egm/stellio/search/authorization/{ => model}/SubjectAccessRight.kt (80%) rename search-service/src/main/kotlin/com/egm/stellio/search/authorization/{ => model}/SubjectReferential.kt (92%) rename search-service/src/main/kotlin/com/egm/stellio/search/authorization/{ => model}/User.kt (97%) rename search-service/src/main/kotlin/com/egm/stellio/search/authorization/{ => service}/AuthorizationService.kt (93%) rename search-service/src/main/kotlin/com/egm/stellio/search/authorization/{ => service}/DisabledAuthorizationService.kt (95%) rename search-service/src/main/kotlin/com/egm/stellio/search/authorization/{ => service}/EnabledAuthorizationService.kt (98%) rename search-service/src/main/kotlin/com/egm/stellio/search/authorization/{ => service}/EntityAccessRightsService.kt (88%) rename search-service/src/main/kotlin/com/egm/stellio/search/authorization/{ => service}/SubjectReferentialService.kt (97%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => authorization}/web/EntityAccessControlHandler.kt (94%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => common}/config/KafkaConfig.kt (95%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => common}/config/SearchProperties.kt (83%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => common}/config/WebConfig.kt (93%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => common}/model/Query.kt (97%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => common}/support/ApiTestsBootstrapper.kt (95%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => common}/tenant/DatabaseMigration.kt (97%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => common}/tenant/DatabaseTenantConfig.kt (98%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => common}/tenant/DatabaseTenantConnectionFactory.kt (96%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => common}/util/DBConversionUtils.kt (82%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => common}/util/DBQueryUtils.kt (98%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => discovery}/model/AttributeDetails.kt (78%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => discovery}/model/AttributeList.kt (84%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => discovery}/model/AttributeTypeInfo.kt (83%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => discovery}/model/EntityType.kt (77%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => discovery}/model/EntityTypeInfo.kt (94%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => discovery}/model/EntityTypeList.kt (84%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => discovery}/service/AttributeService.kt (88%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => discovery}/service/EntityTypeService.kt (93%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => discovery}/web/AttributeHandler.kt (95%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => discovery}/web/EntityTypeHandler.kt (95%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => entity}/listener/ObservationEventListener.kt (94%) rename search-service/src/main/kotlin/com/egm/stellio/search/{model/TemporalEntityAttribute.kt => entity/model/Attribute.kt} (95%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => entity}/model/AttributeMetadata.kt (65%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => entity}/model/EntitiesQuery.kt (93%) rename search-service/src/main/kotlin/com/egm/stellio/search/{model/EntityPayload.kt => entity/model/Entity.kt} (96%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => entity}/model/OperationType.kt (89%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => entity}/model/UpdateResult.kt (98%) rename search-service/src/main/kotlin/com/egm/stellio/search/{service/TemporalEntityAttributeService.kt => entity/service/EntityAttributeService.kt} (84%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => entity}/service/EntityEventService.kt (94%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => entity}/service/EntityOperationService.kt (73%) create mode 100644 search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityQueryService.kt rename search-service/src/main/kotlin/com/egm/stellio/search/{service/EntityPayloadService.kt => entity/service/EntityService.kt} (51%) create mode 100644 search-service/src/main/kotlin/com/egm/stellio/search/entity/util/AttributeUtils.kt create mode 100644 search-service/src/main/kotlin/com/egm/stellio/search/entity/util/EntitiesQueryUtils.kt create mode 100644 search-service/src/main/kotlin/com/egm/stellio/search/entity/util/EntityUtils.kt rename search-service/src/main/kotlin/com/egm/stellio/search/{ => entity}/web/BatchAPIResponses.kt (95%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => entity}/web/EntityHandler.kt (73%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => entity}/web/EntityOperationHandler.kt (63%) delete mode 100644 search-service/src/main/kotlin/com/egm/stellio/search/model/EntityTemporalResult.kt rename search-service/src/main/kotlin/com/egm/stellio/search/{ => temporal}/model/AttributeInstance.kt (91%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => temporal}/model/AttributeInstanceResult.kt (67%) create mode 100644 search-service/src/main/kotlin/com/egm/stellio/search/temporal/model/EntityTemporalResult.kt rename search-service/src/main/kotlin/com/egm/stellio/search/{ => temporal}/model/TemporalEntitiesQuery.kt (90%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => temporal}/model/TemporalQuery.kt (96%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => temporal}/service/AttributeInstanceService.kt (86%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => temporal}/service/TemporalPaginationService.kt (70%) rename search-service/src/main/kotlin/com/egm/stellio/search/{service/QueryService.kt => temporal/service/TemporalQueryService.kt} (54%) create mode 100644 search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/TemporalService.kt rename search-service/src/main/kotlin/com/egm/stellio/search/{ => temporal}/util/DBAggregationUtils.kt (96%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => temporal}/util/TemporalEntityBuilder.kt (88%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => temporal}/util/TemporalQueryParamsUtils.kt (88%) rename search-service/src/main/kotlin/com/egm/stellio/search/{util/EntitiesQueryUtils.kt => temporal/util/TemporalQueryUtils.kt} (65%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => temporal}/web/TemporalApiResponses.kt (94%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => temporal}/web/TemporalEntityHandler.kt (69%) rename search-service/src/main/kotlin/com/egm/stellio/search/{ => temporal}/web/TemporalEntityOperationsHandler.kt (76%) delete mode 100644 search-service/src/main/kotlin/com/egm/stellio/search/util/AttributeInstanceUtils.kt rename search-service/src/test/kotlin/com/egm/stellio/search/{ => authorization}/listener/IAMListenerTests.kt (92%) rename search-service/src/test/kotlin/com/egm/stellio/search/authorization/{ => service}/AuthorizationServiceTests.kt (95%) rename search-service/src/test/kotlin/com/egm/stellio/search/authorization/{ => service}/EnabledAuthorizationServiceTests.kt (98%) rename search-service/src/test/kotlin/com/egm/stellio/search/authorization/{ => service}/EntityAccessRightsServiceTests.kt (83%) rename search-service/src/test/kotlin/com/egm/stellio/search/authorization/{ => service}/SubjectReferentialServiceTests.kt (98%) rename search-service/src/test/kotlin/com/egm/stellio/search/{ => authorization}/web/AnonymousUserHandlerTests.kt (70%) rename search-service/src/test/kotlin/com/egm/stellio/search/{ => authorization}/web/EntityAccessControlHandlerTests.kt (96%) rename search-service/src/test/kotlin/com/egm/stellio/search/{ => discovery}/service/AttributeServiceTests.kt (72%) rename search-service/src/test/kotlin/com/egm/stellio/search/{ => discovery}/service/EntityTypeServiceTests.kt (71%) rename search-service/src/test/kotlin/com/egm/stellio/search/{ => discovery}/web/AttributeHandlerTests.kt (95%) rename search-service/src/test/kotlin/com/egm/stellio/search/{ => discovery}/web/EntityTypeHandlerTests.kt (97%) rename search-service/src/test/kotlin/com/egm/stellio/search/{ => entity}/listener/ObservationEventListenerTests.kt (86%) rename search-service/src/test/kotlin/com/egm/stellio/search/{ => entity}/model/EntityModelTests.kt (83%) rename search-service/src/test/kotlin/com/egm/stellio/search/{ => entity}/model/UpdateResultTests.kt (98%) rename search-service/src/test/kotlin/com/egm/stellio/search/{service/TemporalEntityAttributeServiceTests.kt => entity/service/EntityAttributeServiceTests.kt} (76%) rename search-service/src/test/kotlin/com/egm/stellio/search/{ => entity}/service/EntityEventServiceTests.kt (88%) rename search-service/src/test/kotlin/com/egm/stellio/search/{ => entity}/service/EntityOperationServiceTests.kt (55%) create mode 100644 search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityQueryServiceTests.kt rename search-service/src/test/kotlin/com/egm/stellio/search/{service/EntityQueryServiceTests.kt => entity/service/EntityServiceQueriesTests.kt} (87%) create mode 100644 search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityServiceTests.kt rename search-service/src/test/kotlin/com/egm/stellio/search/{util/AttributeInstanceUtilsTests.kt => entity/util/AttributeUtilsTests.kt} (67%) create mode 100644 search-service/src/test/kotlin/com/egm/stellio/search/entity/util/EntitiesQueryUtilsTests.kt rename search-service/src/test/kotlin/com/egm/stellio/search/{ => entity}/util/PatchAttributeTests.kt (98%) rename search-service/src/test/kotlin/com/egm/stellio/search/{ => entity}/web/EntityHandlerTests.kt (78%) create mode 100644 search-service/src/test/kotlin/com/egm/stellio/search/entity/web/EntityOperationHandlerTests.kt delete mode 100644 search-service/src/test/kotlin/com/egm/stellio/search/service/EntityPayloadServiceTests.kt rename search-service/src/test/kotlin/com/egm/stellio/search/{service/AggregatedQueryServiceTests.kt => temporal/service/AggregatedTemporalQueryServiceTests.kt} (81%) rename search-service/src/test/kotlin/com/egm/stellio/search/{ => temporal}/service/AttributeInstanceServiceTests.kt (84%) rename search-service/src/test/kotlin/com/egm/stellio/search/{ => temporal}/service/TemporalPaginationServiceTests.kt (75%) rename search-service/src/test/kotlin/com/egm/stellio/search/{service/QueryServiceTests.kt => temporal/service/TemporalQueryServiceTests.kt} (65%) rename search-service/src/test/kotlin/com/egm/stellio/search/{ => temporal}/util/TemporalEntitiesParameterizedSource.kt (80%) rename search-service/src/test/kotlin/com/egm/stellio/search/{ => temporal}/util/TemporalEntityBuilderTests.kt (82%) rename search-service/src/test/kotlin/com/egm/stellio/search/{ => temporal}/util/TemporalEntityParameterizedSource.kt (84%) rename search-service/src/test/kotlin/com/egm/stellio/search/{util/EntitiesQueryUtilsTests.kt => temporal/util/TemporalQueryUtilsTests.kt} (58%) rename search-service/src/test/kotlin/com/egm/stellio/search/{ => temporal}/web/TemporalEntityHandlerPaginationTests.kt (85%) rename search-service/src/test/kotlin/com/egm/stellio/search/{ => temporal}/web/TemporalEntityHandlerTestCommon.kt (71%) rename search-service/src/test/kotlin/com/egm/stellio/search/{ => temporal}/web/TemporalEntityHandlerTests.kt (72%) rename search-service/src/test/kotlin/com/egm/stellio/search/{ => temporal}/web/TemporalEntityOperationsHandlerTests.kt (87%) delete mode 100644 search-service/src/test/kotlin/com/egm/stellio/search/web/EntityOperationHandlerTests.kt diff --git a/search-service/config/detekt/baseline.xml b/search-service/config/detekt/baseline.xml index 0ee11ab05..c392b1a73 100644 --- a/search-service/config/detekt/baseline.xml +++ b/search-service/config/detekt/baseline.xml @@ -5,28 +5,27 @@ 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:EntityPayloadService.kt$EntityPayloadService$it && !inverse || !it && inverse + 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> ): ResponseEntity<*> LongMethod:PatchAttributeTests.kt$PatchAttributeTests.Companion$@JvmStatic fun mergePatchProvider(): Stream<Arguments> LongMethod:PatchAttributeTests.kt$PatchAttributeTests.Companion$@JvmStatic fun partialUpdatePatchProvider(): Stream<Arguments> - LongMethod:QueryServiceTests.kt$QueryServiceTests$@Test fun `it should query temporal entities as requested by query params`() - LongMethod:QueryServiceTests.kt$QueryServiceTests$@Test fun `it should return an empty list for an attribute if it has no temporal values`() + LongMethod:TemporalQueryServiceTests.kt$TemporalQueryServiceTests$@Test fun `it should query temporal entities as requested by query params`() + LongMethod:TemporalQueryServiceTests.kt$TemporalQueryServiceTests$@Test fun `it should return an empty list for an attribute if it has no temporal values`() LongMethod:TemporalScopeBuilderTests.kt$TemporalScopeBuilderTests$@Test fun `it should build an aggregated temporal representation of scopes`() LongMethod:V0_29__JsonLd_migration.kt$V0_29__JsonLd_migration$override fun migrate(context: Context) - LongParameterList:AttributeInstance.kt$AttributeInstance.Companion$( temporalEntityAttribute: UUID, instanceId: URI = generateRandomInstanceId(), timeAndProperty: Pair<ZonedDateTime, TemporalProperty>, value: Triple<String?, Double?, WKTCoordinates?>, payload: ExpandedAttributeInstance, sub: String? ) - LongParameterList:AttributeInstance.kt$AttributeInstance.Companion$( temporalEntityAttribute: UUID, instanceId: URI = generateRandomInstanceId(), timeProperty: TemporalProperty? = TemporalProperty.OBSERVED_AT, modifiedAt: ZonedDateTime? = null, attributeMetadata: AttributeMetadata, payload: ExpandedAttributeInstance, time: ZonedDateTime, sub: String? = null ) + LongParameterList:AttributeInstance.kt$AttributeInstance.Companion$( attributeUuid: UUID, instanceId: URI = generateRandomInstanceId(), timeAndProperty: Pair<ZonedDateTime, TemporalProperty>, value: Triple<String?, Double?, WKTCoordinates?>, payload: ExpandedAttributeInstance, sub: String? ) + LongParameterList:AttributeInstance.kt$AttributeInstance.Companion$( attributeUuid: UUID, instanceId: URI = generateRandomInstanceId(), timeProperty: TemporalProperty? = TemporalProperty.OBSERVED_AT, modifiedAt: ZonedDateTime? = null, attributeMetadata: AttributeMetadata, payload: ExpandedAttributeInstance, time: ZonedDateTime, sub: String? = null ) + LongParameterList:EntityAttributeService.kt$EntityAttributeService$( attribute: Attribute, attributeName: ExpandedTerm, attributeMetadata: AttributeMetadata, mergedAt: ZonedDateTime, observedAt: ZonedDateTime?, attributePayload: ExpandedAttributeInstance, sub: Sub? ) + LongParameterList:EntityAttributeService.kt$EntityAttributeService$( attribute: Attribute, ngsiLdAttribute: NgsiLdAttribute, attributeMetadata: AttributeMetadata, createdAt: ZonedDateTime, attributePayload: ExpandedAttributeInstance, sub: Sub? ) + 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:TemporalEntityAttributeService.kt$TemporalEntityAttributeService$( entityId: URI, attributeName: ExpandedTerm, attributeMetadata: AttributeMetadata, createdAt: ZonedDateTime, attributePayload: ExpandedAttributeInstance, sub: Sub? ) - LongParameterList:TemporalEntityAttributeService.kt$TemporalEntityAttributeService$( entityUri: URI, ngsiLdAttributes: List<NgsiLdAttribute>, expandedAttributes: ExpandedAttributes, createdAt: ZonedDateTime, observedAt: ZonedDateTime?, sub: Sub? ) - LongParameterList:TemporalEntityAttributeService.kt$TemporalEntityAttributeService$( entityUri: URI, ngsiLdAttributes: List<NgsiLdAttribute>, expandedAttributes: ExpandedAttributes, disallowOverwrite: Boolean, createdAt: ZonedDateTime, sub: Sub? ) - LongParameterList:TemporalEntityAttributeService.kt$TemporalEntityAttributeService$( tea: TemporalEntityAttribute, attributeName: ExpandedTerm, attributeMetadata: AttributeMetadata, mergedAt: ZonedDateTime, observedAt: ZonedDateTime?, attributePayload: ExpandedAttributeInstance, sub: Sub? ) - LongParameterList:TemporalEntityAttributeService.kt$TemporalEntityAttributeService$( temporalEntityAttribute: TemporalEntityAttribute, ngsiLdAttribute: NgsiLdAttribute, attributeMetadata: AttributeMetadata, createdAt: ZonedDateTime, attributePayload: ExpandedAttributeInstance, sub: Sub? ) 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) - SwallowedException:EntitiesQueryUtils.kt$e: IllegalArgumentException - TooManyFunctions:EntityPayloadService.kt$EntityPayloadService + SwallowedException:TemporalQueryUtils.kt$e: IllegalArgumentException diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/SearchServiceApplication.kt b/search-service/src/main/kotlin/com/egm/stellio/search/SearchServiceApplication.kt index 3067df6df..f0b10e6a5 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/SearchServiceApplication.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/SearchServiceApplication.kt @@ -5,7 +5,7 @@ import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.boot.runApplication @SpringBootApplication(scanBasePackages = ["com.egm.stellio.search", "com.egm.stellio.shared"]) -@ConfigurationPropertiesScan("com.egm.stellio.search.config", "com.egm.stellio.shared.config") +@ConfigurationPropertiesScan("com.egm.stellio.search.common.config", "com.egm.stellio.shared.config") class SearchServiceApplication @Suppress("SpreadOperator") diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/listener/IAMListener.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/listener/IAMListener.kt similarity index 85% rename from search-service/src/main/kotlin/com/egm/stellio/search/listener/IAMListener.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/authorization/listener/IAMListener.kt index ad68c4960..a124254ad 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/listener/IAMListener.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/listener/IAMListener.kt @@ -1,17 +1,16 @@ -package com.egm.stellio.search.listener +package com.egm.stellio.search.authorization.listener import arrow.core.Either import arrow.core.flattenOption import arrow.core.left import arrow.core.raise.either import arrow.core.right -import com.egm.stellio.search.authorization.EntityAccessRightsService -import com.egm.stellio.search.authorization.SubjectReferential -import com.egm.stellio.search.authorization.SubjectReferentialService -import com.egm.stellio.search.authorization.toSubjectInfo -import com.egm.stellio.search.config.SearchProperties -import com.egm.stellio.search.service.EntityEventService -import com.egm.stellio.search.service.EntityPayloadService +import com.egm.stellio.search.authorization.model.SubjectReferential +import com.egm.stellio.search.authorization.model.toSubjectInfo +import com.egm.stellio.search.authorization.service.EntityAccessRightsService +import com.egm.stellio.search.authorization.service.SubjectReferentialService +import com.egm.stellio.search.common.config.SearchProperties +import com.egm.stellio.search.entity.service.EntityService import com.egm.stellio.shared.model.* import com.egm.stellio.shared.util.AuthContextModel.AUTH_TERM_IS_MEMBER_OF import com.egm.stellio.shared.util.AuthContextModel.AUTH_TERM_ROLES @@ -39,8 +38,7 @@ class IAMListener( private val subjectReferentialService: SubjectReferentialService, private val searchProperties: SearchProperties, private val entityAccessRightsService: EntityAccessRightsService, - private val entityPayloadService: EntityPayloadService, - private val entityEventService: EntityEventService + private val entityService: EntityService ) { private val logger = LoggerFactory.getLogger(javaClass) @@ -114,19 +112,15 @@ class IAMListener( val subjectType = SubjectType.valueOf(entityDeleteEvent.entityTypes.first().uppercase()) val sub = entityDeleteEvent.entityId.extractSub() mono { + // delete the entities owned by the user while the user still exists + // (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) + } + Unit.right() + } else Unit.right() subjectReferentialService.delete(entityDeleteEvent.entityId.extractSub()) - val result: Either = - if (searchProperties.onOwnerDeleteCascadeEntities && subjectType == SubjectType.USER) { - val entitiesIds = entityAccessRightsService.getEntitiesIdsOwnedBySubject(sub).getOrNull() - entitiesIds?.let { entityAccessRightsService.deleteAllAccessRightsOnEntities(it) } - entitiesIds?.forEach { entityId -> - entityPayloadService.deleteEntity(entityId).getOrNull()?.also { - entityEventService.publishEntityDeleteEvent(null, it) - } - } - Unit.right() - } else Unit.right() - result }.writeContextAndSubscribe(tenantName, entityDeleteEvent) } diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/EntityAccessRights.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/model/EntityAccessRights.kt similarity index 98% rename from search-service/src/main/kotlin/com/egm/stellio/search/authorization/EntityAccessRights.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/authorization/model/EntityAccessRights.kt index f222d051c..022110962 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/EntityAccessRights.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/model/EntityAccessRights.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.authorization +package com.egm.stellio.search.authorization.model import com.egm.stellio.shared.model.ExpandedAttributeInstances import com.egm.stellio.shared.model.ExpandedTerm diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/Group.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/model/Group.kt similarity index 95% rename from search-service/src/main/kotlin/com/egm/stellio/search/authorization/Group.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/authorization/model/Group.kt index a664c6bf1..c9599753c 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/Group.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/model/Group.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.authorization +package com.egm.stellio.search.authorization.model import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_NAME import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_IS_MEMBER_OF diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/SubjectAccessRight.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/model/SubjectAccessRight.kt similarity index 80% rename from search-service/src/main/kotlin/com/egm/stellio/search/authorization/SubjectAccessRight.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/authorization/model/SubjectAccessRight.kt index c9e163335..363e757a8 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/SubjectAccessRight.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/model/SubjectAccessRight.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.authorization +package com.egm.stellio.search.authorization.model import com.egm.stellio.shared.util.AccessRight import java.net.URI diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/SubjectReferential.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/model/SubjectReferential.kt similarity index 92% rename from search-service/src/main/kotlin/com/egm/stellio/search/authorization/SubjectReferential.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/authorization/model/SubjectReferential.kt index 98b15672f..e781e628c 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/SubjectReferential.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/model/SubjectReferential.kt @@ -1,6 +1,6 @@ -package com.egm.stellio.search.authorization +package com.egm.stellio.search.authorization.model -import com.egm.stellio.search.util.deserializeAsMap +import com.egm.stellio.search.common.util.deserializeAsMap import com.egm.stellio.shared.util.GlobalRole import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_VALUE_TERM import com.egm.stellio.shared.util.JsonUtils diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/User.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/model/User.kt similarity index 97% rename from search-service/src/main/kotlin/com/egm/stellio/search/authorization/User.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/authorization/model/User.kt index 8999a36fd..46cebf398 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/User.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/model/User.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.authorization +package com.egm.stellio.search.authorization.model import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_FAMILY_NAME import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_GIVEN_NAME diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/AuthorizationService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/AuthorizationService.kt similarity index 93% rename from search-service/src/main/kotlin/com/egm/stellio/search/authorization/AuthorizationService.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/AuthorizationService.kt index ec2bc3c5c..1aead2e42 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/AuthorizationService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/AuthorizationService.kt @@ -1,8 +1,8 @@ -package com.egm.stellio.search.authorization +package com.egm.stellio.search.authorization.service import arrow.core.Either import arrow.core.Option -import com.egm.stellio.search.model.EntitiesQuery +import com.egm.stellio.search.entity.model.EntitiesQuery import com.egm.stellio.shared.model.APIException import com.egm.stellio.shared.model.ExpandedEntity import com.egm.stellio.shared.util.Sub diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/DisabledAuthorizationService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/DisabledAuthorizationService.kt similarity index 95% rename from search-service/src/main/kotlin/com/egm/stellio/search/authorization/DisabledAuthorizationService.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/DisabledAuthorizationService.kt index dff262042..21da63b1e 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/DisabledAuthorizationService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/DisabledAuthorizationService.kt @@ -1,9 +1,9 @@ -package com.egm.stellio.search.authorization +package com.egm.stellio.search.authorization.service import arrow.core.Either import arrow.core.Option import arrow.core.right -import com.egm.stellio.search.model.EntitiesQuery +import com.egm.stellio.search.entity.model.EntitiesQuery import com.egm.stellio.shared.model.APIException import com.egm.stellio.shared.model.ExpandedEntity import com.egm.stellio.shared.util.Sub diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/EnabledAuthorizationService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/EnabledAuthorizationService.kt similarity index 98% rename from search-service/src/main/kotlin/com/egm/stellio/search/authorization/EnabledAuthorizationService.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/EnabledAuthorizationService.kt index dc21a84eb..4c927d91a 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/EnabledAuthorizationService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/EnabledAuthorizationService.kt @@ -1,9 +1,9 @@ -package com.egm.stellio.search.authorization +package com.egm.stellio.search.authorization.service import arrow.core.* import arrow.core.raise.either import arrow.fx.coroutines.parMap -import com.egm.stellio.search.model.EntitiesQuery +import com.egm.stellio.search.entity.model.EntitiesQuery import com.egm.stellio.shared.model.APIException import com.egm.stellio.shared.model.AccessDeniedException import com.egm.stellio.shared.model.ExpandedEntity diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/EntityAccessRightsService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/EntityAccessRightsService.kt similarity index 88% rename from search-service/src/main/kotlin/com/egm/stellio/search/authorization/EntityAccessRightsService.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/EntityAccessRightsService.kt index f7921a402..6a375dc56 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/EntityAccessRightsService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/EntityAccessRightsService.kt @@ -1,10 +1,10 @@ -package com.egm.stellio.search.authorization +package com.egm.stellio.search.authorization.service import arrow.core.* import arrow.core.raise.either -import com.egm.stellio.search.authorization.EntityAccessRights.SubjectRightInfo -import com.egm.stellio.search.service.EntityPayloadService -import com.egm.stellio.search.util.* +import com.egm.stellio.search.authorization.model.EntityAccessRights +import com.egm.stellio.search.authorization.model.EntityAccessRights.SubjectRightInfo +import com.egm.stellio.search.common.util.* import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.model.* import com.egm.stellio.shared.util.* @@ -30,8 +30,7 @@ import java.net.URI class EntityAccessRightsService( private val applicationProperties: ApplicationProperties, private val databaseClient: DatabaseClient, - private val subjectReferentialService: SubjectReferentialService, - private val entityPayloadService: EntityPayloadService + private val subjectReferentialService: SubjectReferentialService ) { @Transactional suspend fun setReadRoleOnEntity(sub: Sub, entityId: URI): Either = @@ -143,7 +142,7 @@ class EntityAccessRightsService( subjectReferentialService.hasStellioAdminRole(subjectUuids) .flatMap { if (!it) - entityPayloadService.hasSpecificAccessPolicies(entityId, specificAccessPolicies) + hasSpecificAccessPolicies(entityId, specificAccessPolicies) else true.right() }.flatMap { if (!it) @@ -152,6 +151,26 @@ class EntityAccessRightsService( }.bind() } + suspend fun hasSpecificAccessPolicies( + entityId: URI, + specificAccessPolicies: List + ): Either { + if (specificAccessPolicies.isEmpty()) + return either { false } + + return databaseClient.sql( + """ + SELECT count(entity_id) as count + FROM entity_payload + WHERE entity_id = :entity_id + AND specific_access_policy IN (:specific_access_policies) + """.trimIndent() + ) + .bind("entity_id", entityId) + .bind("specific_access_policies", specificAccessPolicies.map { it.toString() }) + .oneToResult { it["count"] as Long > 0 } + } + private suspend fun hasDirectAccessRightOnEntity( uuids: List, entityId: URI, @@ -337,6 +356,35 @@ class EntityAccessRightsService( .allToMappedList { toUri(it["entity_id"]) } } + suspend fun updateSpecificAccessPolicy( + entityId: URI, + ngsiLdAttribute: NgsiLdAttribute + ): Either = either { + val specificAccessPolicy = ngsiLdAttribute.getSpecificAccessPolicy().bind() + databaseClient.sql( + """ + UPDATE entity_payload + SET specific_access_policy = :specific_access_policy + WHERE entity_id = :entity_id + """.trimIndent() + ) + .bind("entity_id", entityId) + .bind("specific_access_policy", specificAccessPolicy.toString()) + .execute() + .bind() + } + + suspend fun removeSpecificAccessPolicy(entityId: URI): Either = + databaseClient.sql( + """ + UPDATE entity_payload + SET specific_access_policy = null + WHERE entity_id = :entity_id + """.trimIndent() + ) + .bind("entity_id", entityId) + .execute() + @Transactional suspend fun delete(sub: Sub): Either = databaseClient diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/SubjectReferentialService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/SubjectReferentialService.kt similarity index 97% rename from search-service/src/main/kotlin/com/egm/stellio/search/authorization/SubjectReferentialService.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/SubjectReferentialService.kt index ba9bbc8dc..5cd0c844f 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/SubjectReferentialService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/SubjectReferentialService.kt @@ -1,10 +1,13 @@ -package com.egm.stellio.search.authorization +package com.egm.stellio.search.authorization.service import arrow.core.Either import arrow.core.Option import arrow.core.Some import arrow.core.getOrElse -import com.egm.stellio.search.util.* +import com.egm.stellio.search.authorization.model.Group +import com.egm.stellio.search.authorization.model.SubjectReferential +import com.egm.stellio.search.authorization.model.User +import com.egm.stellio.search.common.util.* import com.egm.stellio.shared.model.APIException import com.egm.stellio.shared.model.AccessDeniedException import com.egm.stellio.shared.util.* diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityAccessControlHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/web/EntityAccessControlHandler.kt similarity index 94% rename from search-service/src/main/kotlin/com/egm/stellio/search/web/EntityAccessControlHandler.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/authorization/web/EntityAccessControlHandler.kt index 1e4532ebf..bb1784eac 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityAccessControlHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/web/EntityAccessControlHandler.kt @@ -1,11 +1,14 @@ -package com.egm.stellio.search.web +package com.egm.stellio.search.authorization.web import arrow.core.* import arrow.core.raise.either -import com.egm.stellio.search.authorization.* -import com.egm.stellio.search.model.* -import com.egm.stellio.search.service.EntityPayloadService -import com.egm.stellio.search.util.composeEntitiesQuery +import com.egm.stellio.search.authorization.service.AuthorizationService +import com.egm.stellio.search.authorization.service.EntityAccessRightsService +import com.egm.stellio.search.entity.model.NotUpdatedDetails +import com.egm.stellio.search.entity.model.UpdateAttributeResult +import com.egm.stellio.search.entity.model.UpdateOperationResult +import com.egm.stellio.search.entity.model.updateResultFromDetailedResult +import com.egm.stellio.search.entity.util.composeEntitiesQuery import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.model.* import com.egm.stellio.shared.util.* @@ -36,7 +39,6 @@ import kotlin.collections.flatten class EntityAccessControlHandler( private val applicationProperties: ApplicationProperties, private val entityAccessRightsService: EntityAccessRightsService, - private val entityPayloadService: EntityPayloadService, private val authorizationService: AuthorizationService ) : BaseHandler() { @@ -298,7 +300,7 @@ class EntityAccessControlHandler( val ngsiLdAttribute = expandedAttribute.toNgsiLdAttribute().bind() - entityPayloadService.updateSpecificAccessPolicy(entityId, ngsiLdAttribute).bind() + entityAccessRightsService.updateSpecificAccessPolicy(entityId, ngsiLdAttribute).bind() ResponseEntity.status(HttpStatus.NO_CONTENT).build() }.fold( @@ -314,7 +316,7 @@ class EntityAccessControlHandler( authorizationService.userCanAdminEntity(entityId, sub).bind() - entityPayloadService.removeSpecificAccessPolicy(entityId).bind() + entityAccessRightsService.removeSpecificAccessPolicy(entityId).bind() ResponseEntity.status(HttpStatus.NO_CONTENT).build() }.fold( diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/config/KafkaConfig.kt b/search-service/src/main/kotlin/com/egm/stellio/search/common/config/KafkaConfig.kt similarity index 95% rename from search-service/src/main/kotlin/com/egm/stellio/search/config/KafkaConfig.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/common/config/KafkaConfig.kt index dc315993f..1f82df6b5 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/config/KafkaConfig.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/common/config/KafkaConfig.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.config +package com.egm.stellio.search.common.config import org.springframework.boot.autoconfigure.kafka.ConcurrentKafkaListenerContainerFactoryConfigurer import org.springframework.context.annotation.Bean diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/config/SearchProperties.kt b/search-service/src/main/kotlin/com/egm/stellio/search/common/config/SearchProperties.kt similarity index 83% rename from search-service/src/main/kotlin/com/egm/stellio/search/config/SearchProperties.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/common/config/SearchProperties.kt index 8de9ac5f1..9c7528233 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/config/SearchProperties.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/common/config/SearchProperties.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.config +package com.egm.stellio.search.common.config import org.springframework.boot.context.properties.ConfigurationProperties diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/config/WebConfig.kt b/search-service/src/main/kotlin/com/egm/stellio/search/common/config/WebConfig.kt similarity index 93% rename from search-service/src/main/kotlin/com/egm/stellio/search/config/WebConfig.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/common/config/WebConfig.kt index b55387b80..8e98dbc65 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/config/WebConfig.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/common/config/WebConfig.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.config +package com.egm.stellio.search.common.config import org.springframework.context.annotation.Configuration import org.springframework.http.codec.ServerCodecConfigurer diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/model/Query.kt b/search-service/src/main/kotlin/com/egm/stellio/search/common/model/Query.kt similarity index 97% rename from search-service/src/main/kotlin/com/egm/stellio/search/model/Query.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/common/model/Query.kt index 4c7167977..32ab3d5e5 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/model/Query.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/common/model/Query.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.model +package com.egm.stellio.search.common.model import arrow.core.Either import arrow.core.left diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/support/ApiTestsBootstrapper.kt b/search-service/src/main/kotlin/com/egm/stellio/search/common/support/ApiTestsBootstrapper.kt similarity index 95% rename from search-service/src/main/kotlin/com/egm/stellio/search/support/ApiTestsBootstrapper.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/common/support/ApiTestsBootstrapper.kt index 9ef861aa3..cfd605c61 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/support/ApiTestsBootstrapper.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/common/support/ApiTestsBootstrapper.kt @@ -1,7 +1,7 @@ -package com.egm.stellio.search.support +package com.egm.stellio.search.common.support -import com.egm.stellio.search.authorization.SubjectReferential -import com.egm.stellio.search.authorization.SubjectReferentialService +import com.egm.stellio.search.authorization.model.SubjectReferential +import com.egm.stellio.search.authorization.service.SubjectReferentialService import com.egm.stellio.shared.util.GlobalRole import com.egm.stellio.shared.util.SubjectType import io.r2dbc.postgresql.codec.Json diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/tenant/DatabaseMigration.kt b/search-service/src/main/kotlin/com/egm/stellio/search/common/tenant/DatabaseMigration.kt similarity index 97% rename from search-service/src/main/kotlin/com/egm/stellio/search/tenant/DatabaseMigration.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/common/tenant/DatabaseMigration.kt index 355724f2c..1d9647a4b 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/tenant/DatabaseMigration.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/common/tenant/DatabaseMigration.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.tenant +package com.egm.stellio.search.common.tenant import com.egm.stellio.shared.config.ApplicationProperties import jakarta.annotation.PostConstruct diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/tenant/DatabaseTenantConfig.kt b/search-service/src/main/kotlin/com/egm/stellio/search/common/tenant/DatabaseTenantConfig.kt similarity index 98% rename from search-service/src/main/kotlin/com/egm/stellio/search/tenant/DatabaseTenantConfig.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/common/tenant/DatabaseTenantConfig.kt index e4186f54f..755e56a03 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/tenant/DatabaseTenantConfig.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/common/tenant/DatabaseTenantConfig.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.tenant +package com.egm.stellio.search.common.tenant import com.egm.stellio.shared.config.ApplicationProperties import io.r2dbc.spi.ConnectionFactories diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/tenant/DatabaseTenantConnectionFactory.kt b/search-service/src/main/kotlin/com/egm/stellio/search/common/tenant/DatabaseTenantConnectionFactory.kt similarity index 96% rename from search-service/src/main/kotlin/com/egm/stellio/search/tenant/DatabaseTenantConnectionFactory.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/common/tenant/DatabaseTenantConnectionFactory.kt index 2f2198326..c90cf5865 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/tenant/DatabaseTenantConnectionFactory.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/common/tenant/DatabaseTenantConnectionFactory.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.tenant +package com.egm.stellio.search.common.tenant import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.model.NonexistentTenantException diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/util/DBConversionUtils.kt b/search-service/src/main/kotlin/com/egm/stellio/search/common/util/DBConversionUtils.kt similarity index 82% rename from search-service/src/main/kotlin/com/egm/stellio/search/util/DBConversionUtils.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/common/util/DBConversionUtils.kt index d81b48770..b90fa6abc 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/util/DBConversionUtils.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/common/util/DBConversionUtils.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.util +package com.egm.stellio.search.common.util import com.egm.stellio.shared.model.ExpandedAttributeInstance import com.egm.stellio.shared.util.JsonUtils.deserializeAsMap @@ -33,3 +33,17 @@ fun Json.deserializeExpandedPayload(): Map> = this.asString(). fun Json.deserializeAsMap(): Map = this.asString().deserializeAsMap() fun ExpandedAttributeInstance.toJson(): Json = Json.of(serializeObject(this)) + +fun valueToDoubleOrNull(value: Any): Double? = + when (value) { + is Double -> value + is Int -> value.toDouble() + else -> null + } + +fun valueToStringOrNull(value: Any): String? = + when (value) { + is String -> value + is Boolean -> value.toString() + else -> null + } diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/util/DBQueryUtils.kt b/search-service/src/main/kotlin/com/egm/stellio/search/common/util/DBQueryUtils.kt similarity index 98% rename from search-service/src/main/kotlin/com/egm/stellio/search/util/DBQueryUtils.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/common/util/DBQueryUtils.kt index 2114dab2b..cab99490c 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/util/DBQueryUtils.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/common/util/DBQueryUtils.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.util +package com.egm.stellio.search.common.util import arrow.core.Either import arrow.core.left diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/model/AttributeDetails.kt b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/model/AttributeDetails.kt similarity index 78% rename from search-service/src/main/kotlin/com/egm/stellio/search/model/AttributeDetails.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/discovery/model/AttributeDetails.kt index ac7f6f5ee..c7705ac82 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/model/AttributeDetails.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/model/AttributeDetails.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.model +package com.egm.stellio.search.discovery.model import java.net.URI diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/model/AttributeList.kt b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/model/AttributeList.kt similarity index 84% rename from search-service/src/main/kotlin/com/egm/stellio/search/model/AttributeList.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/discovery/model/AttributeList.kt index 42814cbd9..7cd511b88 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/model/AttributeList.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/model/AttributeList.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.model +package com.egm.stellio.search.discovery.model import com.egm.stellio.shared.util.toUri import java.net.URI diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/model/AttributeTypeInfo.kt b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/model/AttributeTypeInfo.kt similarity index 83% rename from search-service/src/main/kotlin/com/egm/stellio/search/model/AttributeTypeInfo.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/discovery/model/AttributeTypeInfo.kt index 24048d0ce..388387bda 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/model/AttributeTypeInfo.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/model/AttributeTypeInfo.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.model +package com.egm.stellio.search.discovery.model import java.net.URI diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/model/EntityType.kt b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/model/EntityType.kt similarity index 77% rename from search-service/src/main/kotlin/com/egm/stellio/search/model/EntityType.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/discovery/model/EntityType.kt index cade8ab18..14f4cf3b5 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/model/EntityType.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/model/EntityType.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.model +package com.egm.stellio.search.discovery.model import java.net.URI diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/model/EntityTypeInfo.kt b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/model/EntityTypeInfo.kt similarity index 94% rename from search-service/src/main/kotlin/com/egm/stellio/search/model/EntityTypeInfo.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/discovery/model/EntityTypeInfo.kt index 0f940695c..9b5c3eb1f 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/model/EntityTypeInfo.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/model/EntityTypeInfo.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.model +package com.egm.stellio.search.discovery.model import java.net.URI diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/model/EntityTypeList.kt b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/model/EntityTypeList.kt similarity index 84% rename from search-service/src/main/kotlin/com/egm/stellio/search/model/EntityTypeList.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/discovery/model/EntityTypeList.kt index 8a6f092a3..c6c2041e5 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/model/EntityTypeList.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/model/EntityTypeList.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.model +package com.egm.stellio.search.discovery.model import com.egm.stellio.shared.util.toUri import java.net.URI diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/AttributeService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/service/AttributeService.kt similarity index 88% rename from search-service/src/main/kotlin/com/egm/stellio/search/service/AttributeService.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/discovery/service/AttributeService.kt index 363cdf661..eb9aecf56 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/AttributeService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/service/AttributeService.kt @@ -1,17 +1,17 @@ -package com.egm.stellio.search.service +package com.egm.stellio.search.discovery.service import arrow.core.Either import arrow.core.flatten import arrow.core.left import arrow.core.right -import com.egm.stellio.search.model.AttributeDetails -import com.egm.stellio.search.model.AttributeList -import com.egm.stellio.search.model.AttributeType -import com.egm.stellio.search.model.AttributeTypeInfo -import com.egm.stellio.search.util.allToMappedList -import com.egm.stellio.search.util.toInt -import com.egm.stellio.search.util.toList -import com.egm.stellio.search.util.toUri +import com.egm.stellio.search.common.util.allToMappedList +import com.egm.stellio.search.common.util.toInt +import com.egm.stellio.search.common.util.toList +import com.egm.stellio.search.common.util.toUri +import com.egm.stellio.search.discovery.model.AttributeDetails +import com.egm.stellio.search.discovery.model.AttributeList +import com.egm.stellio.search.discovery.model.AttributeType +import com.egm.stellio.search.discovery.model.AttributeTypeInfo import com.egm.stellio.shared.model.APIException import com.egm.stellio.shared.model.ExpandedTerm import com.egm.stellio.shared.model.ResourceNotFoundException diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityTypeService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/service/EntityTypeService.kt similarity index 93% rename from search-service/src/main/kotlin/com/egm/stellio/search/service/EntityTypeService.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/discovery/service/EntityTypeService.kt index 40b6c1092..b1a515eac 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityTypeService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/service/EntityTypeService.kt @@ -1,12 +1,12 @@ -package com.egm.stellio.search.service +package com.egm.stellio.search.discovery.service import arrow.core.Either import arrow.core.left import arrow.core.right -import com.egm.stellio.search.model.* -import com.egm.stellio.search.util.allToMappedList -import com.egm.stellio.search.util.toInt -import com.egm.stellio.search.util.toUri +import com.egm.stellio.search.common.util.allToMappedList +import com.egm.stellio.search.common.util.toInt +import com.egm.stellio.search.common.util.toUri +import com.egm.stellio.search.discovery.model.* import com.egm.stellio.shared.model.APIException import com.egm.stellio.shared.model.ExpandedTerm import com.egm.stellio.shared.model.ResourceNotFoundException diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/web/AttributeHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/web/AttributeHandler.kt similarity index 95% rename from search-service/src/main/kotlin/com/egm/stellio/search/web/AttributeHandler.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/discovery/web/AttributeHandler.kt index 29a05e0b1..06783a2b2 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/web/AttributeHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/web/AttributeHandler.kt @@ -1,7 +1,7 @@ -package com.egm.stellio.search.web +package com.egm.stellio.search.discovery.web import arrow.core.raise.either -import com.egm.stellio.search.service.AttributeService +import com.egm.stellio.search.discovery.service.AttributeService import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.util.* import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdTerm diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityTypeHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/web/EntityTypeHandler.kt similarity index 95% rename from search-service/src/main/kotlin/com/egm/stellio/search/web/EntityTypeHandler.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/discovery/web/EntityTypeHandler.kt index 01a15cc0f..7b079329d 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityTypeHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/web/EntityTypeHandler.kt @@ -1,7 +1,7 @@ -package com.egm.stellio.search.web +package com.egm.stellio.search.discovery.web import arrow.core.raise.either -import com.egm.stellio.search.service.EntityTypeService +import com.egm.stellio.search.discovery.service.EntityTypeService import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.util.* import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdTerm diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/listener/ObservationEventListener.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/listener/ObservationEventListener.kt similarity index 94% rename from search-service/src/main/kotlin/com/egm/stellio/search/listener/ObservationEventListener.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/entity/listener/ObservationEventListener.kt index f04afeeaa..7ea31de4f 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/listener/ObservationEventListener.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/listener/ObservationEventListener.kt @@ -1,10 +1,10 @@ -package com.egm.stellio.search.listener +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.service.EntityEventService -import com.egm.stellio.search.service.EntityPayloadService +import com.egm.stellio.search.entity.service.EntityEventService +import com.egm.stellio.search.entity.service.EntityService import com.egm.stellio.shared.model.* import com.egm.stellio.shared.util.JsonLdUtils.expandAttribute import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdEntity @@ -23,7 +23,7 @@ import reactor.core.publisher.Mono @Component class ObservationEventListener( - private val entityPayloadService: EntityPayloadService, + private val entityService: EntityService, private val entityEventService: EntityEventService ) { private val logger = LoggerFactory.getLogger(javaClass) @@ -70,7 +70,7 @@ class ObservationEventListener( val ngsiLdEntity = expandedEntity.toNgsiLdEntity().bind() mono { - entityPayloadService.createEntity( + entityService.createEntity( ngsiLdEntity, expandedEntity, observationEvent.sub @@ -95,7 +95,7 @@ class ObservationEventListener( ) mono { - entityPayloadService.partialUpdateAttribute( + entityService.partialUpdateAttribute( observationEvent.entityId, expandedAttribute, observationEvent.sub @@ -132,7 +132,7 @@ class ObservationEventListener( ) mono { - entityPayloadService.appendAttributes( + entityService.appendAttributes( observationEvent.entityId, expandedAttribute.toExpandedAttributes(), !observationEvent.overwrite, diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/model/TemporalEntityAttribute.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/Attribute.kt similarity index 95% rename from search-service/src/main/kotlin/com/egm/stellio/search/model/TemporalEntityAttribute.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/entity/model/Attribute.kt index a04f1da97..bca913e99 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/model/TemporalEntityAttribute.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/Attribute.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.model +package com.egm.stellio.search.entity.model import com.egm.stellio.shared.model.ExpandedTerm import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_GEOPROPERTY_TYPE @@ -19,7 +19,7 @@ import java.net.URI import java.time.ZonedDateTime import java.util.UUID -data class TemporalEntityAttribute( +data class Attribute( @Id val id: UUID = UUID.randomUUID(), val entityId: URI, @@ -66,7 +66,7 @@ data class TemporalEntityAttribute( /** * Returns the key of the member for the simplified representation of the attribute, as defined in 4.5.9 */ - fun toSimpliedRepresentationKey(): String = + fun toSimplifiedRepresentationKey(): String = when (this) { Property -> NGSILD_PROPERTY_VALUES Relationship -> NGSILD_RELATIONSHIP_OBJECTS diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/model/AttributeMetadata.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/AttributeMetadata.kt similarity index 65% rename from search-service/src/main/kotlin/com/egm/stellio/search/model/AttributeMetadata.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/entity/model/AttributeMetadata.kt index e7c60d060..8797978c3 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/model/AttributeMetadata.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/AttributeMetadata.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.model +package com.egm.stellio.search.entity.model import com.egm.stellio.shared.model.WKTCoordinates import java.net.URI @@ -8,8 +8,8 @@ data class AttributeMetadata( val measuredValue: Double?, val value: String?, val geoValue: WKTCoordinates?, - val valueType: TemporalEntityAttribute.AttributeValueType, + val valueType: Attribute.AttributeValueType, val datasetId: URI?, - val type: TemporalEntityAttribute.AttributeType, + val type: Attribute.AttributeType, val observedAt: ZonedDateTime? ) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/model/EntitiesQuery.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/EntitiesQuery.kt similarity index 93% rename from search-service/src/main/kotlin/com/egm/stellio/search/model/EntitiesQuery.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/entity/model/EntitiesQuery.kt index d7d3706a5..9a1e80113 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/model/EntitiesQuery.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/EntitiesQuery.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.model +package com.egm.stellio.search.entity.model import com.egm.stellio.shared.model.EntityTypeSelection import com.egm.stellio.shared.model.ExpandedTerm diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/model/EntityPayload.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/Entity.kt similarity index 96% rename from search-service/src/main/kotlin/com/egm/stellio/search/model/EntityPayload.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/entity/model/Entity.kt index 5f8d986d7..e81816479 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/model/EntityPayload.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/Entity.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.model +package com.egm.stellio.search.entity.model import com.egm.stellio.shared.model.ExpandedTerm import com.egm.stellio.shared.util.AuthContextModel @@ -15,7 +15,7 @@ import io.r2dbc.postgresql.codec.Json import java.net.URI import java.time.ZonedDateTime -data class EntityPayload( +data class Entity( val entityId: URI, val types: List, val scopes: List? = null, diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/model/OperationType.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/OperationType.kt similarity index 89% rename from search-service/src/main/kotlin/com/egm/stellio/search/model/OperationType.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/entity/model/OperationType.kt index b1a0045c0..f7c824f11 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/model/OperationType.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/OperationType.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.model +package com.egm.stellio.search.entity.model /** * Used to manage the different types of updates on existing attributes depending on the current operation. diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/model/UpdateResult.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/UpdateResult.kt similarity index 98% rename from search-service/src/main/kotlin/com/egm/stellio/search/model/UpdateResult.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/entity/model/UpdateResult.kt index ddb61f626..9ccf6a575 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/model/UpdateResult.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/UpdateResult.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.model +package com.egm.stellio.search.entity.model import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonValue diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/TemporalEntityAttributeService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityAttributeService.kt similarity index 84% rename from search-service/src/main/kotlin/com/egm/stellio/search/service/TemporalEntityAttributeService.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityAttributeService.kt index bbcc52997..e4e7f02e7 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/TemporalEntityAttributeService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityAttributeService.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.service +package com.egm.stellio.search.entity.service import arrow.core.Either import arrow.core.flatMap @@ -7,8 +7,17 @@ import arrow.core.raise.either import arrow.core.raise.ensure import arrow.core.right import arrow.fx.coroutines.parMap -import com.egm.stellio.search.model.* -import com.egm.stellio.search.util.* +import com.egm.stellio.search.common.util.* +import com.egm.stellio.search.entity.model.* +import com.egm.stellio.search.entity.model.Attribute +import com.egm.stellio.search.entity.util.guessAttributeValueType +import com.egm.stellio.search.entity.util.mergePatch +import com.egm.stellio.search.entity.util.partialUpdatePatch +import com.egm.stellio.search.entity.util.prepareAttributes +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.model.* import com.egm.stellio.shared.util.* import com.egm.stellio.shared.util.AttributeType @@ -35,7 +44,7 @@ import java.time.ZonedDateTime import java.util.UUID @Service -class TemporalEntityAttributeService( +class EntityAttributeService( private val databaseClient: DatabaseClient, private val attributeInstanceService: AttributeInstanceService ) { @@ -43,7 +52,7 @@ class TemporalEntityAttributeService( private val logger = LoggerFactory.getLogger(javaClass) @Transactional - suspend fun create(temporalEntityAttribute: TemporalEntityAttribute): Either = + suspend fun create(attribute: Attribute): Either = databaseClient.sql( """ INSERT INTO temporal_entity_attribute @@ -54,19 +63,19 @@ class TemporalEntityAttributeService( :payload) """.trimIndent() ) - .bind("id", temporalEntityAttribute.id) - .bind("entity_id", temporalEntityAttribute.entityId) - .bind("attribute_name", temporalEntityAttribute.attributeName) - .bind("attribute_type", temporalEntityAttribute.attributeType.toString()) - .bind("attribute_value_type", temporalEntityAttribute.attributeValueType.toString()) - .bind("created_at", temporalEntityAttribute.createdAt) - .bind("dataset_id", temporalEntityAttribute.datasetId) - .bind("payload", temporalEntityAttribute.payload) + .bind("id", attribute.id) + .bind("entity_id", attribute.entityId) + .bind("attribute_name", attribute.attributeName) + .bind("attribute_type", attribute.attributeType.toString()) + .bind("attribute_value_type", attribute.attributeValueType.toString()) + .bind("created_at", attribute.createdAt) + .bind("dataset_id", attribute.datasetId) + .bind("payload", attribute.payload) .execute() @Transactional suspend fun updateOnReplace( - teaUUID: UUID, + attributeUUID: UUID, attributeMetadata: AttributeMetadata, modifiedAt: ZonedDateTime, payload: String @@ -82,7 +91,7 @@ class TemporalEntityAttributeService( WHERE id = :id """.trimIndent() ) - .bind("id", teaUUID) + .bind("id", attributeUUID) .bind("attribute_type", attributeMetadata.type.toString()) .bind("attribute_value_type", attributeMetadata.valueType.toString()) .bind("modified_at", modifiedAt) @@ -91,8 +100,8 @@ class TemporalEntityAttributeService( @Transactional suspend fun updateOnUpdate( - teaUUID: UUID, - valueType: TemporalEntityAttribute.AttributeValueType, + attributeUUID: UUID, + valueType: Attribute.AttributeValueType, modifiedAt: ZonedDateTime, payload: String ): Either = @@ -102,10 +111,10 @@ class TemporalEntityAttributeService( SET payload = :payload, attribute_value_type = :attribute_value_type, modified_at = :modified_at - WHERE id = :tea_uuid + WHERE id = :attribute_uuid """.trimIndent() ) - .bind("tea_uuid", teaUUID) + .bind("attribute_uuid", attributeUUID) .bind("payload", Json.of(payload)) .bind("attribute_value_type", valueType.toString()) .bind("modified_at", modifiedAt) @@ -117,7 +126,7 @@ class TemporalEntityAttributeService( * To be removed at some point later. */ @Transactional - suspend fun createEntityTemporalReferences( + suspend fun createAttributes( payload: String, contexts: List, sub: String? = null @@ -125,14 +134,14 @@ class TemporalEntityAttributeService( val createdAt = ZonedDateTime.now(ZoneOffset.UTC) val expandedEntity = expandJsonLdEntity(payload, contexts) val ngsiLdEntity = expandedEntity.toNgsiLdEntity().bind() - ngsiLdEntity.prepareTemporalAttributes() + ngsiLdEntity.prepareAttributes() .map { - createEntityTemporalReferences(ngsiLdEntity, expandedEntity, it, createdAt, sub).bind() + createAttributes(ngsiLdEntity, expandedEntity, it, createdAt, sub).bind() }.bind() } @Transactional - suspend fun createEntityTemporalReferences( + suspend fun createAttributes( ngsiLdEntity: NgsiLdEntity, expandedEntity: ExpandedEntity, attributesMetadata: List>, @@ -174,7 +183,7 @@ class TemporalEntityAttributeService( ): Either = either { logger.debug("Adding attribute {} to entity {}", attributeName, entityId) - val temporalEntityAttribute = TemporalEntityAttribute( + val attribute = Attribute( entityId = entityId, attributeName = attributeName, attributeType = attributeMetadata.type, @@ -183,10 +192,10 @@ class TemporalEntityAttributeService( createdAt = createdAt, payload = Json.of(serializeObject(attributePayload)) ) - create(temporalEntityAttribute).bind() + create(attribute).bind() val attributeInstance = AttributeInstance( - temporalEntityAttribute = temporalEntityAttribute.id, + attributeUuid = attribute.id, timeProperty = AttributeInstance.TemporalProperty.CREATED_AT, time = createdAt, attributeMetadata = attributeMetadata, @@ -197,7 +206,7 @@ class TemporalEntityAttributeService( if (attributeMetadata.observedAt != null) { val attributeObservedAtInstance = AttributeInstance( - temporalEntityAttribute = temporalEntityAttribute.id, + attributeUuid = attribute.id, time = attributeMetadata.observedAt, attributeMetadata = attributeMetadata, payload = attributePayload @@ -208,7 +217,7 @@ class TemporalEntityAttributeService( @Transactional suspend fun replaceAttribute( - temporalEntityAttribute: TemporalEntityAttribute, + attribute: Attribute, ngsiLdAttribute: NgsiLdAttribute, attributeMetadata: AttributeMetadata, createdAt: ZonedDateTime, @@ -219,17 +228,17 @@ class TemporalEntityAttributeService( "Replacing attribute {} ({}) in entity {}", ngsiLdAttribute.name, attributeMetadata.datasetId, - temporalEntityAttribute.entityId + attribute.entityId ) updateOnReplace( - temporalEntityAttribute.id, + attribute.id, attributeMetadata, createdAt, serializeObject(attributePayload) ).bind() val attributeInstance = AttributeInstance( - temporalEntityAttribute = temporalEntityAttribute.id, + attributeUuid = attribute.id, timeProperty = AttributeInstance.TemporalProperty.MODIFIED_AT, time = createdAt, attributeMetadata = attributeMetadata, @@ -240,7 +249,7 @@ class TemporalEntityAttributeService( if (attributeMetadata.observedAt != null) { val attributeObservedAtInstance = AttributeInstance( - temporalEntityAttribute = temporalEntityAttribute.id, + attributeUuid = attribute.id, time = attributeMetadata.observedAt, attributeMetadata = attributeMetadata, payload = attributePayload @@ -251,7 +260,7 @@ class TemporalEntityAttributeService( @Transactional suspend fun mergeAttribute( - tea: TemporalEntityAttribute, + attribute: Attribute, attributeName: ExpandedTerm, attributeMetadata: AttributeMetadata, mergedAt: ZonedDateTime, @@ -263,26 +272,26 @@ class TemporalEntityAttributeService( "Merging attribute {} ({}) in entity {}", attributeName, attributeMetadata.datasetId, - tea.entityId + attribute.entityId ) val (processedAttributePayload, processedAttributeMetadata) = processObservedAtInMergeOperation( - tea, + attribute, attributePayload, attributeMetadata, observedAt ) val (jsonTargetObject, updatedAttributeInstance) = - mergePatch(tea.payload.toExpandedAttributeInstance(), processedAttributePayload) - val value = getValueFromPartialAttributePayload(tea, updatedAttributeInstance) - updateOnUpdate(tea.id, processedAttributeMetadata.valueType, mergedAt, jsonTargetObject).bind() + mergePatch(attribute.payload.toExpandedAttributeInstance(), processedAttributePayload) + val value = getValueFromPartialAttributePayload(attribute, updatedAttributeInstance) + updateOnUpdate(attribute.id, processedAttributeMetadata.valueType, mergedAt, jsonTargetObject).bind() val attributeInstance = - createContextualAttributeInstance(tea, updatedAttributeInstance, value, mergedAt, sub) + createContextualAttributeInstance(attribute, updatedAttributeInstance, value, mergedAt, sub) attributeInstanceService.create(attributeInstance).bind() } @Transactional - suspend fun deleteTemporalAttributesOfEntity(entityId: URI): Either { + suspend fun deleteAttributes(entityId: URI): Either { val uuids = databaseClient.sql( """ DELETE FROM temporal_entity_attribute @@ -301,7 +310,7 @@ class TemporalEntityAttributeService( } @Transactional - suspend fun deleteTemporalAttribute( + suspend fun deleteAttribute( entityId: URI, attributeName: String, datasetId: URI?, @@ -311,15 +320,15 @@ class TemporalEntityAttributeService( logger.debug("Deleting attribute {} from entity {} (all: {})", attributeName, entityId, deleteAll) if (deleteAll) { attributeInstanceService.deleteAllInstancesOfAttribute(entityId, attributeName).bind() - deleteTemporalAttributeAllInstancesReferences(entityId, attributeName).bind() + deleteAllInstances(entityId, attributeName).bind() } else { attributeInstanceService.deleteInstancesOfAttribute(entityId, attributeName, datasetId).bind() - deleteTemporalAttributeReferences(entityId, attributeName, datasetId).bind() + deleteSpecificInstance(entityId, attributeName, datasetId).bind() } } @Transactional - suspend fun deleteTemporalAttributeReferences( + suspend fun deleteSpecificInstance( entityId: URI, attributeName: String, datasetId: URI? @@ -341,7 +350,7 @@ class TemporalEntityAttributeService( .execute() @Transactional - suspend fun deleteTemporalAttributeAllInstancesReferences( + suspend fun deleteAllInstances( entityId: URI, attributeName: String ): Either = @@ -356,10 +365,10 @@ class TemporalEntityAttributeService( .bind("attribute_name", attributeName) .execute() - suspend fun getForTemporalEntities( + suspend fun getForEntities( entitiesIds: List, entitiesQuery: EntitiesQuery - ): List { + ): List { val filterOnAttributes = if (entitiesQuery.attrs.isNotEmpty()) " AND " + entitiesQuery.attrs.joinToString( @@ -390,10 +399,10 @@ class TemporalEntityAttributeService( return databaseClient .sql(selectQuery) .bind("entities_ids", entitiesIds) - .allToMappedList { rowToTemporalEntityAttribute(it) } + .allToMappedList { rowToAttribute(it) } } - suspend fun getForEntity(id: URI, attrs: Set, datasetIds: Set): List { + suspend fun getForEntity(id: URI, attrs: Set, datasetIds: Set): List { val filterOnAttributes = if (attrs.isNotEmpty()) " AND " + attrs.joinToString( @@ -423,14 +432,14 @@ class TemporalEntityAttributeService( return databaseClient .sql(selectQuery) .bind("entity_id", id) - .allToMappedList { rowToTemporalEntityAttribute(it) } + .allToMappedList { rowToAttribute(it) } } suspend fun getForEntityAndAttribute( id: URI, attributeName: String, datasetId: URI? = null - ): Either { + ): Either { val selectQuery = """ SELECT * @@ -449,7 +458,7 @@ class TemporalEntityAttributeService( else it } .oneToResult { - rowToTemporalEntityAttribute(it) + rowToAttribute(it) } } @@ -480,13 +489,13 @@ class TemporalEntityAttributeService( } } - private fun rowToTemporalEntityAttribute(row: Map) = - TemporalEntityAttribute( + private fun rowToAttribute(row: Map) = + Attribute( id = toUuid(row["id"]), entityId = toUri(row["entity_id"]), attributeName = row["attribute_name"] as ExpandedTerm, - attributeType = TemporalEntityAttribute.AttributeType.valueOf(row["attribute_type"] as String), - attributeValueType = TemporalEntityAttribute.AttributeValueType.valueOf( + attributeType = Attribute.AttributeType.valueOf(row["attribute_type"] as String), + attributeValueType = Attribute.AttributeValueType.valueOf( row["attribute_value_type"] as String ), datasetId = toOptionalUri(row["dataset_id"]), @@ -536,7 +545,7 @@ class TemporalEntityAttributeService( } @Transactional - suspend fun appendEntityAttributes( + suspend fun appendAttributes( entityUri: URI, ngsiLdAttributes: List, expandedAttributes: ExpandedAttributes, @@ -547,15 +556,15 @@ class TemporalEntityAttributeService( val attributeInstances = ngsiLdAttributes.flatOnInstances() attributeInstances.parMap { (ngsiLdAttribute, ngsiLdAttributeInstance) -> logger.debug("Appending attribute {} in entity {}", ngsiLdAttribute.name, entityUri) - val currentTea = + val currentAttribute = getForEntityAndAttribute(entityUri, ngsiLdAttribute.name, ngsiLdAttributeInstance.datasetId) .fold({ null }, { it }) - val attributeMetadata = ngsiLdAttributeInstance.toTemporalAttributeMetadata().bind() + val attributeMetadata = ngsiLdAttributeInstance.toAttributeMetadata().bind() val attributePayload = expandedAttributes.getAttributeFromExpandedAttributes( ngsiLdAttribute.name, ngsiLdAttributeInstance.datasetId )!! - if (currentTea == null) { + if (currentAttribute == null) { addAttribute( entityUri, ngsiLdAttribute.name, @@ -582,7 +591,7 @@ class TemporalEntityAttributeService( ).right().bind() } else { replaceAttribute( - currentTea, + currentAttribute, ngsiLdAttribute, attributeMetadata, createdAt, @@ -601,7 +610,7 @@ class TemporalEntityAttributeService( }.fold({ it.left() }, { updateResultFromDetailedResult(it).right() }) @Transactional - suspend fun updateEntityAttributes( + suspend fun updateAttributes( entityUri: URI, ngsiLdAttributes: List, expandedAttributes: ExpandedAttributes, @@ -611,17 +620,17 @@ class TemporalEntityAttributeService( val attributeInstances = ngsiLdAttributes.flatOnInstances() attributeInstances.parMap { (ngsiLdAttribute, ngsiLdAttributeInstance) -> logger.debug("Updating attribute {} in entity {}", ngsiLdAttribute.name, entityUri) - val currentTea = + val currentAttribute = getForEntityAndAttribute(entityUri, ngsiLdAttribute.name, ngsiLdAttributeInstance.datasetId) .fold({ null }, { it }) - val attributeMetadata = ngsiLdAttributeInstance.toTemporalAttributeMetadata().bind() + val attributeMetadata = ngsiLdAttributeInstance.toAttributeMetadata().bind() val attributePayload = expandedAttributes.getAttributeFromExpandedAttributes( ngsiLdAttribute.name, ngsiLdAttributeInstance.datasetId )!! - if (currentTea != null) { + if (currentAttribute != null) { replaceAttribute( - currentTea, + currentAttribute, ngsiLdAttribute, attributeMetadata, createdAt, @@ -656,7 +665,7 @@ class TemporalEntityAttributeService( }.fold({ it.left() }, { updateResultFromDetailedResult(it).right() }) @Transactional - suspend fun partialUpdateEntityAttribute( + suspend fun partialUpdateAttribute( entityId: URI, expandedAttribute: ExpandedAttribute, modifiedAt: ZonedDateTime, @@ -676,21 +685,21 @@ class TemporalEntityAttributeService( val updateAttributeResult = if (exists) { // first update payload in temporal entity attribute - val tea = getForEntityAndAttribute(entityId, attributeName, datasetId).bind() + val attribute = getForEntityAndAttribute(entityId, attributeName, datasetId).bind() attributeValues[JSONLD_TYPE]?.let { - ensure(isAttributeOfType(attributeValues, AttributeType(NGSILD_PREFIX + tea.attributeType))) { + ensure(isAttributeOfType(attributeValues, AttributeType(NGSILD_PREFIX + attribute.attributeType))) { BadRequestDataException("The type of the attribute has to be the same as the existing one") } } val (jsonTargetObject, updatedAttributeInstance) = - partialUpdatePatch(tea.payload.toExpandedAttributeInstance(), attributeValues) - val value = getValueFromPartialAttributePayload(tea, updatedAttributeInstance) - val attributeValueType = guessAttributeValueType(tea.attributeType, attributeValues) - updateOnUpdate(tea.id, attributeValueType, modifiedAt, jsonTargetObject).bind() + partialUpdatePatch(attribute.payload.toExpandedAttributeInstance(), attributeValues) + val value = getValueFromPartialAttributePayload(attribute, updatedAttributeInstance) + val attributeValueType = guessAttributeValueType(attribute.attributeType, attributeValues) + updateOnUpdate(attribute.id, attributeValueType, modifiedAt, jsonTargetObject).bind() // then update attribute instance val attributeInstance = createContextualAttributeInstance( - tea, + attribute, updatedAttributeInstance, value, modifiedAt, @@ -717,7 +726,7 @@ class TemporalEntityAttributeService( } @Transactional - suspend fun upsertEntityAttributes( + suspend fun upsertAttributes( entityUri: URI, ngsiLdAttribute: NgsiLdAttribute, expandedAttributes: ExpandedAttributes, @@ -726,16 +735,16 @@ class TemporalEntityAttributeService( ): Either = either { val ngsiLdAttributeInstance = ngsiLdAttribute.getAttributeInstances()[0] logger.debug("Upserting temporal attribute {} in entity {}", ngsiLdAttribute.name, entityUri) - val currentTea = + val currentAttribute = getForEntityAndAttribute(entityUri, ngsiLdAttribute.name, ngsiLdAttributeInstance.datasetId) .fold({ null }, { it }) - val attributeMetadata = ngsiLdAttributeInstance.toTemporalAttributeMetadata().bind() + val attributeMetadata = ngsiLdAttributeInstance.toAttributeMetadata().bind() val attributePayload = expandedAttributes.getAttributeFromExpandedAttributes( ngsiLdAttribute.name, ngsiLdAttributeInstance.datasetId )!! - if (currentTea == null) { + if (currentAttribute == null) { logger.debug( "Creating attribute and instance for attribute {} in entity {}", ngsiLdAttribute.name, @@ -750,17 +759,17 @@ class TemporalEntityAttributeService( sub ).bind() } else { - logger.debug("Adding instance to attribute {} to entity {}", currentTea.attributeName, entityUri) + logger.debug("Adding instance to attribute {} to entity {}", currentAttribute.attributeName, entityUri) attributeInstanceService.addAttributeInstance( - currentTea.id, + currentAttribute.id, attributeMetadata, - expandedAttributes[currentTea.attributeName]!!.first() + expandedAttributes[currentAttribute.attributeName]!!.first() ).bind() } } @Transactional - suspend fun mergeEntityAttributes( + suspend fun mergeAttributes( entityUri: URI, ngsiLdAttributes: List, expandedAttributes: ExpandedAttributes, @@ -771,16 +780,16 @@ class TemporalEntityAttributeService( val attributeInstances = ngsiLdAttributes.flatOnInstances() attributeInstances.parMap { (ngsiLdAttribute, ngsiLdAttributeInstance) -> logger.debug("Merging attribute {} in entity {}", ngsiLdAttribute.name, entityUri) - val currentTea = + val currentAttribute = getForEntityAndAttribute(entityUri, ngsiLdAttribute.name, ngsiLdAttributeInstance.datasetId) .fold({ null }, { it }) - val attributeMetadata = ngsiLdAttributeInstance.toTemporalAttributeMetadata().bind() + val attributeMetadata = ngsiLdAttributeInstance.toAttributeMetadata().bind() val attributePayload = expandedAttributes.getAttributeFromExpandedAttributes( ngsiLdAttribute.name, ngsiLdAttributeInstance.datasetId )!! - if (currentTea == null) { + if (currentAttribute == null) { addAttribute( entityUri, ngsiLdAttribute.name, @@ -798,7 +807,7 @@ class TemporalEntityAttributeService( }.bind() } else { mergeAttribute( - currentTea, + currentAttribute, ngsiLdAttribute.name, attributeMetadata, createdAt, @@ -818,7 +827,7 @@ class TemporalEntityAttributeService( }.fold({ it.left() }, { updateResultFromDetailedResult(it).right() }) @Transactional - suspend fun replaceEntityAttribute( + suspend fun replaceAttribute( entityId: URI, ngsiLdAttribute: NgsiLdAttribute, expandedAttribute: ExpandedAttribute, @@ -830,7 +839,7 @@ class TemporalEntityAttributeService( val datasetId = ngsiLdAttributeInstance.datasetId val currentTea = getForEntityAndAttribute(entityId, attributeName, datasetId).fold({ null }, { it }) - val attributeMetadata = ngsiLdAttributeInstance.toTemporalAttributeMetadata().bind() + val attributeMetadata = ngsiLdAttributeInstance.toAttributeMetadata().bind() val updateAttributeResult = if (currentTea == null) { UpdateAttributeResult( @@ -861,41 +870,41 @@ class TemporalEntityAttributeService( } suspend fun getValueFromPartialAttributePayload( - tea: TemporalEntityAttribute, + attribute: Attribute, attributePayload: ExpandedAttributeInstance ): Triple = - when (tea.attributeType) { - TemporalEntityAttribute.AttributeType.Property -> + when (attribute.attributeType) { + Attribute.AttributeType.Property -> Triple( valueToStringOrNull(attributePayload.getPropertyValue()!!), valueToDoubleOrNull(attributePayload.getPropertyValue()!!), null ) - TemporalEntityAttribute.AttributeType.Relationship -> + Attribute.AttributeType.Relationship -> Triple( attributePayload.getMemberValue(NGSILD_RELATIONSHIP_OBJECT)!! as String, null, null ) - TemporalEntityAttribute.AttributeType.GeoProperty -> + Attribute.AttributeType.GeoProperty -> Triple( null, null, WKTCoordinates(attributePayload.getPropertyValue()!! as String) ) - TemporalEntityAttribute.AttributeType.JsonProperty -> + Attribute.AttributeType.JsonProperty -> Triple( serializeObject(attributePayload.getMemberValue(NGSILD_JSONPROPERTY_VALUE)!!), null, null ) - TemporalEntityAttribute.AttributeType.LanguageProperty -> + Attribute.AttributeType.LanguageProperty -> Triple( serializeObject(attributePayload.getMemberValue(NGSILD_LANGUAGEPROPERTY_VALUE)!!), null, null ) - TemporalEntityAttribute.AttributeType.VocabProperty -> + Attribute.AttributeType.VocabProperty -> Triple( serializeObject(attributePayload.getMemberValue(NGSILD_VOCABPROPERTY_VALUE)!!), null, @@ -904,7 +913,7 @@ class TemporalEntityAttributeService( } private fun createContextualAttributeInstance( - tea: TemporalEntityAttribute, + attribute: Attribute, expandedAttributeInstance: ExpandedAttributeInstance, value: Triple, modifiedAt: ZonedDateTime, @@ -920,7 +929,7 @@ class TemporalEntityAttributeService( Pair(modifiedAt, AttributeInstance.TemporalProperty.MODIFIED_AT) return AttributeInstance( - temporalEntityAttribute = tea.id, + attributeUuid = attribute.id, timeAndProperty = timeAndProperty, value = value, payload = expandedAttributeInstance, @@ -935,14 +944,14 @@ class TemporalEntityAttributeService( * "observedAt" sub-Attribute. */ internal fun processObservedAtInMergeOperation( - tea: TemporalEntityAttribute, + attribute: Attribute, attributePayload: ExpandedAttributeInstance, attributeMetadata: AttributeMetadata, observedAt: ZonedDateTime? ): Pair = if ( observedAt != null && - tea.payload.deserializeAsMap().containsKey(NGSILD_OBSERVED_AT_PROPERTY) && + attribute.payload.deserializeAsMap().containsKey(NGSILD_OBSERVED_AT_PROPERTY) && !attributePayload.containsKey(NGSILD_OBSERVED_AT_PROPERTY) ) Pair( diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityEventService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityEventService.kt similarity index 94% rename from search-service/src/main/kotlin/com/egm/stellio/search/service/EntityEventService.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityEventService.kt index e84f688a5..0627084f6 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityEventService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityEventService.kt @@ -1,10 +1,10 @@ -package com.egm.stellio.search.service +package com.egm.stellio.search.entity.service import arrow.core.Either -import com.egm.stellio.search.model.EntityPayload -import com.egm.stellio.search.model.UpdateOperationResult -import com.egm.stellio.search.model.UpdateResult -import com.egm.stellio.search.model.UpdatedDetails +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.shared.model.* import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_TYPE import com.egm.stellio.shared.util.JsonUtils.serializeObject @@ -21,7 +21,7 @@ import java.net.URI @Component class EntityEventService( private val kafkaTemplate: KafkaTemplate, - private val entityPayloadService: EntityPayloadService + private val entityQueryService: EntityQueryService ) { private val catchAllTopic = "cim.entity._CatchAll" @@ -71,18 +71,18 @@ class EntityEventService( suspend fun publishEntityDeleteEvent( sub: String?, - entityPayload: EntityPayload + entity: Entity ): Job { val tenantName = getTenantFromContext() return coroutineScope.launch { - logger.debug("Sending delete event for entity {} in tenant {}", entityPayload.entityId, tenantName) + logger.debug("Sending delete event for entity {} in tenant {}", entity.entityId, tenantName) publishEntityEvent( EntityDeleteEvent( sub, tenantName, - entityPayload.entityId, - entityPayload.types, - entityPayload.payload.asString(), + entity.entityId, + entity.types, + entity.payload.asString(), emptyList() ) ) @@ -232,7 +232,7 @@ class EntityEventService( internal suspend fun getSerializedEntity( entityId: URI ): Either, String>> = - entityPayloadService.retrieve(entityId) + entityQueryService.retrieve(entityId) .map { Pair(it.types, it.payload.asString()) } diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityOperationService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityOperationService.kt similarity index 73% rename from search-service/src/main/kotlin/com/egm/stellio/search/service/EntityOperationService.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityOperationService.kt index 809a2c7f1..284339302 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityOperationService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityOperationService.kt @@ -1,13 +1,11 @@ -package com.egm.stellio.search.service +package com.egm.stellio.search.entity.service import arrow.core.Either import arrow.core.left import arrow.core.raise.either import arrow.core.right -import com.egm.stellio.search.authorization.AuthorizationService -import com.egm.stellio.search.model.EntityPayload -import com.egm.stellio.search.model.UpdateResult -import com.egm.stellio.search.web.* +import com.egm.stellio.search.entity.model.UpdateResult +import com.egm.stellio.search.entity.web.* import com.egm.stellio.shared.model.APIException import com.egm.stellio.shared.model.BadRequestDataException import com.egm.stellio.shared.util.Sub @@ -20,10 +18,9 @@ import java.net.URI */ @Component class EntityOperationService( - private val entityPayloadService: EntityPayloadService, - private val temporalEntityAttributeService: TemporalEntityAttributeService, - private val authorizationService: AuthorizationService, - private val entityEventService: EntityEventService + private val entityService: EntityService, + private val entityQueryService: EntityQueryService, + private val entityAttributeService: EntityAttributeService, ) { /** @@ -49,7 +46,7 @@ class EntityOperationService( extractIdFunc: (T) -> URI ): Pair, List> { val existingEntitiesIds = - entityPayloadService.filterExistingEntitiesAsIds(entities.map { extractIdFunc.invoke(it) }) + entityQueryService.filterExistingEntitiesAsIds(entities.map { extractIdFunc.invoke(it) }) return entities.partition { existingEntitiesIds.contains(extractIdFunc.invoke(it)) } } @@ -86,18 +83,11 @@ class EntityOperationService( ): BatchOperationResult { val creationResults = entities.map { jsonLdNgsiLdEntity -> either { - entityPayloadService.createEntity(jsonLdNgsiLdEntity.second, jsonLdNgsiLdEntity.first, sub) - .onRight { - entityEventService.publishEntityCreateEvent( - sub, - jsonLdNgsiLdEntity.second.id, - jsonLdNgsiLdEntity.second.types - ) - }.map { - BatchEntitySuccess(jsonLdNgsiLdEntity.entityId()) - }.mapLeft { apiException -> - BatchEntityError(jsonLdNgsiLdEntity.entityId(), arrayListOf(apiException.message)) - }.bind() + entityService.createEntity(jsonLdNgsiLdEntity.second, jsonLdNgsiLdEntity.first, sub).map { + BatchEntitySuccess(jsonLdNgsiLdEntity.entityId()) + }.mapLeft { apiException -> + BatchEntityError(jsonLdNgsiLdEntity.entityId(), arrayListOf(apiException.message)) + }.bind() } }.fold( initial = Pair(listOf(), listOf()), @@ -112,22 +102,15 @@ class EntityOperationService( return BatchOperationResult(creationResults.second.toMutableList(), creationResults.first.toMutableList()) } - suspend fun delete(entities: Set, sub: Sub?): BatchOperationResult { - val deletionResults = entities.map { entity -> - val entityId = entity.entityId + suspend fun delete(entitiesId: List, sub: Sub?): BatchOperationResult { + val deletionResults = entitiesId.map { id -> either { - entityPayloadService.deleteEntity(entityId) - .onRight { - authorizationService.removeRightsOnEntity(entityId) - } - .onRight { - entityEventService.publishEntityDeleteEvent(sub, entity) - } + entityService.deleteEntity(id, sub) .map { - BatchEntitySuccess(entityId) + BatchEntitySuccess(id) } .mapLeft { apiException -> - BatchEntityError(entityId, arrayListOf(apiException.message)) + BatchEntityError(id, arrayListOf(apiException.message)) }.bind() } }.fold( @@ -155,6 +138,42 @@ class EntityOperationService( suspend fun replace(entities: List, sub: Sub?): BatchOperationResult = processEntities(entities, false, sub, ::replaceEntity) + /** + * Upsert a batch of [entities] + * + * @return a [BatchOperationResult] with list of replaced ids and list of errors. + */ + @Transactional + suspend fun upsert( + entities: List, + options: String?, + sub: Sub? + ): Pair> { + val (existingEntities, newEntities) = splitEntitiesByExistence(entities) + + val (newUniqueEntities, duplicatedEntities) = splitEntitiesByUniqueness(newEntities) + val existingOrDuplicatedEntities = existingEntities.plus(duplicatedEntities) + val batchOperationResult = BatchOperationResult() + + val createdIds = if (newUniqueEntities.isNotEmpty()) { + val createOperationResult = create(newUniqueEntities, sub) + batchOperationResult.errors.addAll(createOperationResult.errors) + batchOperationResult.success.addAll(createOperationResult.success) + createOperationResult.success.map { it.entityId } + } else emptyList() + + if (existingOrDuplicatedEntities.isNotEmpty()) { + val updateOperationResult = when (options) { + "update" -> update(existingOrDuplicatedEntities, false, sub) + else -> replace(existingOrDuplicatedEntities, sub) + } + + batchOperationResult.errors.addAll(updateOperationResult.errors) + batchOperationResult.success.addAll(updateOperationResult.success) + } + return batchOperationResult to createdIds + } + /** * Updates a batch of [entities] * @@ -234,19 +253,13 @@ class EntityOperationService( sub: Sub? ): Either = either { val (jsonLdEntity, ngsiLdEntity) = entity - temporalEntityAttributeService.deleteTemporalAttributesOfEntity(ngsiLdEntity.id).bind() - entityPayloadService.appendAttributes( + entityAttributeService.deleteAttributes(ngsiLdEntity.id).bind() + entityService.appendAttributes( ngsiLdEntity.id, jsonLdEntity.getModifiableMembers(), disallowOverwrite, sub - ).bind().also { - entityEventService.publishEntityReplaceEvent( - sub, - ngsiLdEntity.id, - ngsiLdEntity.types - ) - } + ).bind() } suspend fun updateEntity( @@ -255,20 +268,12 @@ class EntityOperationService( sub: Sub? ): Either = either { val (jsonLdEntity, ngsiLdEntity) = entity - entityPayloadService.appendAttributes( + entityService.appendAttributes( ngsiLdEntity.id, jsonLdEntity.getModifiableMembers(), disallowOverwrite, sub - ).bind().also { - entityEventService.publishAttributeChangeEvents( - sub, - ngsiLdEntity.id, - jsonLdEntity.members, - it, - true - ) - } + ).bind() } @SuppressWarnings("UnusedParameter") @@ -278,19 +283,11 @@ class EntityOperationService( sub: Sub? ): Either = either { val (jsonLdEntity, ngsiLdEntity) = entity - entityPayloadService.mergeEntity( + entityService.mergeEntity( ngsiLdEntity.id, jsonLdEntity.getModifiableMembers(), null, sub - ).bind().also { - entityEventService.publishAttributeChangeEvents( - sub, - ngsiLdEntity.id, - jsonLdEntity.members, - it, - true - ) - } + ).bind() } } 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 new file mode 100644 index 000000000..e7961bc90 --- /dev/null +++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityQueryService.kt @@ -0,0 +1,225 @@ +package com.egm.stellio.search.entity.service + +import arrow.core.* +import arrow.core.raise.either +import com.egm.stellio.search.authorization.service.AuthorizationService +import com.egm.stellio.search.common.util.* +import com.egm.stellio.search.entity.model.EntitiesQuery +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.* +import org.springframework.r2dbc.core.DatabaseClient +import org.springframework.stereotype.Service +import java.net.URI + +@Service +class EntityQueryService( + private val databaseClient: DatabaseClient, + private val authorizationService: AuthorizationService +) { + suspend fun queryEntity( + entityId: URI, + sub: Sub? = null + ): Either = either { + checkEntityExistence(entityId).bind() + authorizationService.userCanReadEntity(entityId, sub.toOption()).bind() + + val entityPayload = retrieve(entityId).bind() + toJsonLdEntity(entityPayload) + } + + suspend fun queryEntities( + entitiesQuery: EntitiesQuery, + sub: Sub? = null + ): Either, Int>> = either { + val accessRightFilter = authorizationService.computeAccessRightFilter(sub.toOption()) + + val entitiesIds = queryEntities(entitiesQuery, accessRightFilter) + val count = queryEntitiesCount(entitiesQuery, accessRightFilter).bind() + + // we can have an empty list of entities with a non-zero count (e.g., offset too high) + if (entitiesIds.isEmpty()) + return@either Pair, Int>(emptyList(), count) + + val entitiesPayloads = retrieve(entitiesIds).map { toJsonLdEntity(it) } + + Pair(entitiesPayloads, count).right().bind() + } + + private fun toJsonLdEntity(entity: Entity): ExpandedEntity { + val deserializedEntity = entity.payload.deserializeAsMap() + return ExpandedEntity(deserializedEntity) + } + + suspend fun queryEntities( + entitiesQuery: EntitiesQuery, + accessRightFilter: () -> String? + ): List { + val filterQuery = buildFullEntitiesFilter(entitiesQuery, accessRightFilter) + + val selectQuery = + """ + SELECT DISTINCT(entity_payload.entity_id) + FROM entity_payload + LEFT JOIN temporal_entity_attribute tea + ON tea.entity_id = entity_payload.entity_id + WHERE $filterQuery + ORDER BY entity_id + LIMIT :limit + OFFSET :offset + """.trimIndent() + + return databaseClient + .sql(selectQuery) + .bind("limit", entitiesQuery.paginationQuery.limit) + .bind("offset", entitiesQuery.paginationQuery.offset) + .allToMappedList { toUri(it["entity_id"]) } + } + + suspend fun queryEntitiesCount( + entitiesQuery: EntitiesQuery, + accessRightFilter: () -> String? + ): Either { + val filterQuery = buildFullEntitiesFilter(entitiesQuery, accessRightFilter) + + val countQuery = + """ + SELECT count(distinct(entity_payload.entity_id)) as count_entity + FROM entity_payload + LEFT JOIN temporal_entity_attribute tea + ON tea.entity_id = entity_payload.entity_id + WHERE $filterQuery + """.trimIndent() + + return databaseClient + .sql(countQuery) + .oneToResult { it["count_entity"] as Long } + .map { it.toInt() } + } + + private fun buildFullEntitiesFilter(entitiesQuery: EntitiesQuery, accessRightFilter: () -> String?): String = + buildEntitiesQueryFilter( + entitiesQuery, + accessRightFilter + ).let { + if (entitiesQuery.q != null) + it.wrapToAndClause(buildQQuery(entitiesQuery.q, entitiesQuery.contexts)) + else it + }.let { + if (entitiesQuery.scopeQ != null) + it.wrapToAndClause(buildScopeQQuery(entitiesQuery.scopeQ)) + else it + }.let { + if (entitiesQuery.geoQuery != null) + it.wrapToAndClause(buildGeoQuery(entitiesQuery.geoQuery)) + else it + } + + fun buildEntitiesQueryFilter( + entitiesQuery: EntitiesQuery, + accessRightFilter: () -> String? + ): String { + val formattedIds = + if (entitiesQuery.ids.isNotEmpty()) + entitiesQuery.ids.joinToString( + separator = ",", + prefix = "entity_payload.entity_id in(", + postfix = ")" + ) { "'$it'" } + else null + val formattedIdPattern = + if (!entitiesQuery.idPattern.isNullOrEmpty()) + "entity_payload.entity_id ~ '${entitiesQuery.idPattern}'" + else null + val formattedType = entitiesQuery.typeSelection?.let { "(" + buildTypeQuery(it) + ")" } + val formattedAttrs = + if (entitiesQuery.attrs.isNotEmpty()) + entitiesQuery.attrs.joinToString( + separator = ",", + prefix = "attribute_name in (", + postfix = ")" + ) { "'$it'" } + else null + + val queryFilter = + listOfNotNull( + formattedIds, + formattedIdPattern, + formattedType, + formattedAttrs, + accessRightFilter() + ) + + return queryFilter.joinToString(separator = " AND ") + } + + suspend fun retrieve(entityId: URI): Either = + databaseClient.sql( + """ + SELECT * from entity_payload + WHERE entity_id = :entity_id + """.trimIndent() + ) + .bind("entity_id", entityId) + .oneToResult { it.rowToEntity() } + + suspend fun retrieve(entitiesIds: List): List = + databaseClient.sql( + """ + SELECT * from entity_payload + WHERE entity_id IN (:entities_ids) + """.trimIndent() + ) + .bind("entities_ids", entitiesIds) + .allToMappedList { it.rowToEntity() } + + suspend fun checkEntityExistence( + entityId: URI, + inverse: Boolean = false + ): Either { + val selectQuery = + """ + select + exists( + select 1 + from entity_payload + where entity_id = :entity_id + ) as entityExists; + """.trimIndent() + + return databaseClient + .sql(selectQuery) + .bind("entity_id", entityId) + .oneToResult { it["entityExists"] as Boolean } + .flatMap { + if (it && !inverse || !it && inverse) + Unit.right() + else if (it) + AlreadyExistsException(entityAlreadyExistsMessage(entityId.toString())).left() + else + ResourceNotFoundException(entityNotFoundMessage(entityId.toString())).left() + } + } + + suspend fun filterExistingEntitiesAsIds(entitiesIds: List): List { + if (entitiesIds.isEmpty()) { + return emptyList() + } + + val query = + """ + select entity_id + from entity_payload + where entity_id in (:entities_ids) + """.trimIndent() + + return databaseClient + .sql(query) + .bind("entities_ids", entitiesIds) + .allToMappedList { toUri(it["entity_id"]) } + } +} diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityPayloadService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityService.kt similarity index 51% rename from search-service/src/main/kotlin/com/egm/stellio/search/service/EntityPayloadService.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityService.kt index 58c9bc2d5..eeffa1164 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityPayloadService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityService.kt @@ -1,17 +1,17 @@ -package com.egm.stellio.search.service +package com.egm.stellio.search.entity.service -import arrow.core.Either -import arrow.core.flatMap -import arrow.core.left +import arrow.core.* import arrow.core.raise.either -import arrow.core.right -import com.egm.stellio.search.model.* -import com.egm.stellio.search.model.OperationType.* +import com.egm.stellio.search.authorization.service.AuthorizationService +import com.egm.stellio.search.common.util.* +import com.egm.stellio.search.entity.model.* +import com.egm.stellio.search.entity.model.Attribute +import com.egm.stellio.search.entity.model.OperationType.* +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.util.* import com.egm.stellio.shared.model.* import com.egm.stellio.shared.util.* -import com.egm.stellio.shared.util.AuthContextModel.SpecificAccessPolicy import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_EXPANDED_ENTITY_SPECIFIC_MEMBERS import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_TYPE import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_SCOPE_PROPERTY @@ -27,10 +27,13 @@ import java.time.ZoneOffset import java.time.ZonedDateTime @Service -class EntityPayloadService( +class EntityService( private val databaseClient: DatabaseClient, - private val temporalEntityAttributeService: TemporalEntityAttributeService, - private val scopeService: ScopeService + private val entityQueryService: EntityQueryService, + private val entityAttributeService: EntityAttributeService, + private val scopeService: ScopeService, + private val entityEventService: EntityEventService, + private val authorizationService: AuthorizationService ) { private val logger = LoggerFactory.getLogger(javaClass) @@ -38,20 +41,30 @@ class EntityPayloadService( suspend fun createEntity( ngsiLdEntity: NgsiLdEntity, expandedEntity: ExpandedEntity, - sub: String? = null + sub: Sub? = null ): Either = either { + authorizationService.userCanCreateEntities(sub.toOption()).bind() + entityQueryService.checkEntityExistence(ngsiLdEntity.id, true).bind() + val createdAt = ZonedDateTime.now(ZoneOffset.UTC) - val attributesMetadata = ngsiLdEntity.prepareTemporalAttributes().bind() + val attributesMetadata = ngsiLdEntity.prepareAttributes().bind() logger.debug("Creating entity {}", ngsiLdEntity.id) - createEntityPayload(ngsiLdEntity, expandedEntity, createdAt, sub = sub).bind() - temporalEntityAttributeService.createEntityTemporalReferences( + createEntityPayload(ngsiLdEntity, expandedEntity, createdAt, sub).bind() + entityAttributeService.createAttributes( ngsiLdEntity, expandedEntity, attributesMetadata, createdAt, sub ).bind() + authorizationService.createOwnerRight(ngsiLdEntity.id, sub.toOption()).bind() + + entityEventService.publishEntityCreateEvent( + sub, + ngsiLdEntity.id, + ngsiLdEntity.types + ) } @Transactional @@ -85,16 +98,18 @@ class EntityPayloadService( entityId: URI, expandedAttributes: ExpandedAttributes, observedAt: ZonedDateTime?, - sub: Sub? + sub: Sub? = null ): Either = either { - logger.debug("Merging entity {}", entityId) + entityQueryService.checkEntityExistence(entityId).bind() + authorizationService.userCanUpdateEntity(entityId, sub.toOption()).bind() val (coreAttrs, otherAttrs) = expandedAttributes.toList().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 = temporalEntityAttributeService.mergeEntityAttributes( + val attrsUpdateResult = entityAttributeService.mergeAttributes( entityId, otherAttrs.toMap().toNgsiLdAttributes().bind(), expandedAttributes, @@ -106,9 +121,20 @@ class EntityPayloadService( val updateResult = coreUpdateResult.mergeWith(attrsUpdateResult) // update modifiedAt in entity if at least one attribute has been merged if (updateResult.hasSuccessfulUpdate()) { - val teas = temporalEntityAttributeService.getForEntity(entityId, emptySet(), emptySet()) - updateState(entityId, mergedAt, teas).bind() + val attributes = entityAttributeService.getForEntity(entityId, emptySet(), emptySet()) + updateState(entityId, mergedAt, attributes).bind() + } + + if (updateResult.updated.isNotEmpty()) { + entityEventService.publishAttributeChangeEvents( + sub, + entityId, + expandedAttributes, + updateResult, + true + ) } + updateResult } @@ -117,22 +143,31 @@ class EntityPayloadService( entityId: URI, ngsiLdEntity: NgsiLdEntity, expandedEntity: ExpandedEntity, - sub: String? = null + sub: Sub? = null ): Either = either { + entityQueryService.checkEntityExistence(entityId).bind() + authorizationService.userCanUpdateEntity(entityId, sub.toOption()).bind() + val replacedAt = ngsiLdDateTime() - val attributesMetadata = ngsiLdEntity.prepareTemporalAttributes().bind() + val attributesMetadata = ngsiLdEntity.prepareAttributes().bind() logger.debug("Replacing entity {}", ngsiLdEntity.id) - temporalEntityAttributeService.deleteTemporalAttributesOfEntity(entityId) + entityAttributeService.deleteAttributes(entityId) replaceEntityPayload(ngsiLdEntity, expandedEntity, replacedAt, sub).bind() - temporalEntityAttributeService.createEntityTemporalReferences( + entityAttributeService.createAttributes( ngsiLdEntity, expandedEntity, attributesMetadata, replacedAt, sub ).bind() + + entityEventService.publishEntityReplaceEvent( + sub, + ngsiLdEntity.id, + ngsiLdEntity.types + ) } @Transactional @@ -170,7 +205,7 @@ class EntityPayloadService( } } - suspend fun retrieveCreatedAt(entityId: URI): Either = + private suspend fun retrieveCreatedAt(entityId: URI): Either = databaseClient.sql( """ SELECT created_at from entity_payload @@ -180,221 +215,6 @@ class EntityPayloadService( .bind("entity_id", entityId) .oneToResult { toZonedDateTime(it["created_at"]) } - suspend fun retrieve(entityId: URI): Either = - databaseClient.sql( - """ - SELECT * from entity_payload - WHERE entity_id = :entity_id - """.trimIndent() - ) - .bind("entity_id", entityId) - .oneToResult { rowToEntityPaylaod(it) } - - suspend fun retrieve(entitiesIds: List): List = - databaseClient.sql( - """ - SELECT * from entity_payload - WHERE entity_id IN (:entities_ids) - """.trimIndent() - ) - .bind("entities_ids", entitiesIds) - .allToMappedList { rowToEntityPaylaod(it) } - - private fun rowToEntityPaylaod(row: Map): EntityPayload = - EntityPayload( - entityId = toUri(row["entity_id"]), - types = toList(row["types"]), - scopes = toOptionalList(row["scopes"]), - createdAt = toZonedDateTime(row["created_at"]), - modifiedAt = toOptionalZonedDateTime(row["modified_at"]), - payload = toJson(row["payload"]), - specificAccessPolicy = toOptionalEnum(row["specific_access_policy"]) - ) - - suspend fun checkEntityExistence( - entityId: URI, - inverse: Boolean = false - ): Either { - val selectQuery = - """ - select - exists( - select 1 - from entity_payload - where entity_id = :entity_id - ) as entityExists; - """.trimIndent() - - return databaseClient - .sql(selectQuery) - .bind("entity_id", entityId) - .oneToResult { it["entityExists"] as Boolean } - .flatMap { - if (it && !inverse || !it && inverse) - Unit.right() - else if (it) - AlreadyExistsException(entityAlreadyExistsMessage(entityId.toString())).left() - else - ResourceNotFoundException(entityNotFoundMessage(entityId.toString())).left() - } - } - - suspend fun queryEntities( - entitiesQuery: EntitiesQuery, - accessRightFilter: () -> String? - ): List { - val filterQuery = buildFullEntitiesFilter(entitiesQuery, accessRightFilter) - - val selectQuery = - """ - SELECT DISTINCT(entity_payload.entity_id) - FROM entity_payload - LEFT JOIN temporal_entity_attribute tea - ON tea.entity_id = entity_payload.entity_id - WHERE $filterQuery - ORDER BY entity_id - LIMIT :limit - OFFSET :offset - """.trimIndent() - - return databaseClient - .sql(selectQuery) - .bind("limit", entitiesQuery.paginationQuery.limit) - .bind("offset", entitiesQuery.paginationQuery.offset) - .allToMappedList { toUri(it["entity_id"]) } - } - - suspend fun queryEntitiesCount( - entitiesQuery: EntitiesQuery, - accessRightFilter: () -> String? - ): Either { - val filterQuery = buildFullEntitiesFilter(entitiesQuery, accessRightFilter) - - val countQuery = - """ - SELECT count(distinct(entity_payload.entity_id)) as count_entity - FROM entity_payload - LEFT JOIN temporal_entity_attribute tea - ON tea.entity_id = entity_payload.entity_id - WHERE $filterQuery - """.trimIndent() - - return databaseClient - .sql(countQuery) - .oneToResult { it["count_entity"] as Long } - .map { it.toInt() } - } - - private fun buildFullEntitiesFilter(entitiesQuery: EntitiesQuery, accessRightFilter: () -> String?): String = - buildEntitiesQueryFilter( - entitiesQuery, - accessRightFilter - ).let { - if (entitiesQuery.q != null) - it.wrapToAndClause(buildQQuery(entitiesQuery.q, entitiesQuery.contexts)) - else it - }.let { - if (entitiesQuery.scopeQ != null) - it.wrapToAndClause(buildScopeQQuery(entitiesQuery.scopeQ)) - else it - }.let { - if (entitiesQuery.geoQuery != null) - it.wrapToAndClause(buildGeoQuery(entitiesQuery.geoQuery)) - else it - } - - fun buildEntitiesQueryFilter( - entitiesQuery: EntitiesQuery, - accessRightFilter: () -> String? - ): String { - val formattedIds = - if (entitiesQuery.ids.isNotEmpty()) - entitiesQuery.ids.joinToString( - separator = ",", - prefix = "entity_payload.entity_id in(", - postfix = ")" - ) { "'$it'" } - else null - val formattedIdPattern = - if (!entitiesQuery.idPattern.isNullOrEmpty()) - "entity_payload.entity_id ~ '${entitiesQuery.idPattern}'" - else null - val formattedType = entitiesQuery.typeSelection?.let { "(" + buildTypeQuery(it) + ")" } - val formattedAttrs = - if (entitiesQuery.attrs.isNotEmpty()) - entitiesQuery.attrs.joinToString( - separator = ",", - prefix = "attribute_name in (", - postfix = ")" - ) { "'$it'" } - else null - - val queryFilter = - listOfNotNull( - formattedIds, - formattedIdPattern, - formattedType, - formattedAttrs, - accessRightFilter() - ) - - return queryFilter.joinToString(separator = " AND ") - } - - suspend fun hasSpecificAccessPolicies( - entityId: URI, - specificAccessPolicies: List - ): Either { - if (specificAccessPolicies.isEmpty()) - return either { false } - - return databaseClient.sql( - """ - SELECT count(entity_id) as count - FROM entity_payload - WHERE entity_id = :entity_id - AND specific_access_policy IN (:specific_access_policies) - """.trimIndent() - ) - .bind("entity_id", entityId) - .bind("specific_access_policies", specificAccessPolicies.map { it.toString() }) - .oneToResult { it["count"] as Long > 0 } - } - - suspend fun filterExistingEntitiesAsIds(entitiesIds: List): List { - if (entitiesIds.isEmpty()) { - return emptyList() - } - - val query = - """ - select entity_id - from entity_payload - where entity_id in (:entities_ids) - """.trimIndent() - - return databaseClient - .sql(query) - .bind("entities_ids", entitiesIds) - .allToMappedList { toUri(it["entity_id"]) } - } - - suspend fun getTypes(entityId: URI): Either> { - val selectQuery = - """ - SELECT types - FROM entity_payload - WHERE entity_id = :entity_id - """.trimIndent() - - return databaseClient - .sql(selectQuery) - .bind("entity_id", entityId) - .oneToResult(ResourceNotFoundException(entityNotFoundMessage(entityId.toString()))) { - (it["types"] as Array).toList() - } - } - @Transactional suspend fun updateCoreAttributes( entityId: URI, @@ -424,7 +244,7 @@ class EntityPayloadService( modifiedAt: ZonedDateTime, allowEmptyListOfTypes: Boolean = true ): Either = either { - val entityPayload = retrieve(entityId).bind() + 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) @@ -466,11 +286,14 @@ class EntityPayloadService( @Transactional suspend fun appendAttributes( - entityUri: URI, + entityId: URI, expandedAttributes: ExpandedAttributes, disallowOverwrite: Boolean, - sub: Sub? + sub: Sub? = null ): Either = either { + entityQueryService.checkEntityExistence(entityId).bind() + authorizationService.userCanUpdateEntity(entityId, sub.toOption()).bind() + val (coreAttrs, otherAttrs) = expandedAttributes.toList().partition { JSONLD_EXPANDED_ENTITY_SPECIFIC_MEMBERS.contains(it.first) } val createdAt = ngsiLdDateTime() @@ -478,9 +301,9 @@ class EntityPayloadService( val operationType = if (disallowOverwrite) APPEND_ATTRIBUTES else APPEND_ATTRIBUTES_OVERWRITE_ALLOWED - val coreUpdateResult = updateCoreAttributes(entityUri, coreAttrs, createdAt, operationType).bind() - val attrsUpdateResult = temporalEntityAttributeService.appendEntityAttributes( - entityUri, + val coreUpdateResult = updateCoreAttributes(entityId, coreAttrs, createdAt, operationType).bind() + val attrsUpdateResult = entityAttributeService.appendAttributes( + entityId, otherAttrs.toMap().toNgsiLdAttributes().bind(), expandedAttributes, disallowOverwrite, @@ -491,25 +314,39 @@ class EntityPayloadService( val updateResult = coreUpdateResult.mergeWith(attrsUpdateResult) // update modifiedAt in entity if at least one attribute has been added if (updateResult.hasSuccessfulUpdate()) { - val teas = temporalEntityAttributeService.getForEntity(entityUri, emptySet(), emptySet()) - updateState(entityUri, createdAt, teas).bind() + val attributes = entityAttributeService.getForEntity(entityId, emptySet(), emptySet()) + updateState(entityId, createdAt, attributes).bind() + } + + if (updateResult.hasSuccessfulUpdate()) { + entityEventService.publishAttributeChangeEvents( + sub, + entityId, + expandedAttributes, + updateResult, + true + ) } + updateResult } @Transactional suspend fun updateAttributes( - entityUri: URI, + entityId: URI, expandedAttributes: ExpandedAttributes, - sub: Sub? + sub: Sub? = null ): Either = either { + entityQueryService.checkEntityExistence(entityId).bind() + authorizationService.userCanUpdateEntity(entityId, sub.toOption()).bind() + val (coreAttrs, otherAttrs) = expandedAttributes.toList().partition { JSONLD_EXPANDED_ENTITY_SPECIFIC_MEMBERS.contains(it.first) } val createdAt = ngsiLdDateTime() - val coreUpdateResult = updateCoreAttributes(entityUri, coreAttrs, createdAt, UPDATE_ATTRIBUTES).bind() - val attrsUpdateResult = temporalEntityAttributeService.updateEntityAttributes( - entityUri, + val coreUpdateResult = updateCoreAttributes(entityId, coreAttrs, createdAt, UPDATE_ATTRIBUTES).bind() + val attrsUpdateResult = entityAttributeService.updateAttributes( + entityId, otherAttrs.toMap().toNgsiLdAttributes().bind(), expandedAttributes, createdAt, @@ -519,9 +356,20 @@ class EntityPayloadService( val updateResult = coreUpdateResult.mergeWith(attrsUpdateResult) // update modifiedAt in entity if at least one attribute has been added if (updateResult.hasSuccessfulUpdate()) { - val teas = temporalEntityAttributeService.getForEntity(entityUri, emptySet(), emptySet()) - updateState(entityUri, createdAt, teas).bind() + val attributes = entityAttributeService.getForEntity(entityId, emptySet(), emptySet()) + updateState(entityId, createdAt, attributes).bind() } + + if (updateResult.updated.isNotEmpty()) { + entityEventService.publishAttributeChangeEvents( + sub, + entityId, + expandedAttributes, + updateResult, + true + ) + } + updateResult } @@ -529,19 +377,34 @@ class EntityPayloadService( suspend fun partialUpdateAttribute( entityId: URI, expandedAttribute: ExpandedAttribute, - sub: Sub? + sub: Sub? = null ): Either = either { + entityQueryService.checkEntityExistence(entityId).bind() + authorizationService.userCanUpdateEntity(entityId, sub.toOption()).bind() + val modifiedAt = ngsiLdDateTime() - val updateResult = temporalEntityAttributeService.partialUpdateEntityAttribute( + + val updateResult = entityAttributeService.partialUpdateAttribute( entityId, expandedAttribute, modifiedAt, sub ).bind() + if (updateResult.isSuccessful()) { - val teas = temporalEntityAttributeService.getForEntity(entityId, emptySet(), emptySet()) - updateState(entityId, modifiedAt, teas).bind() + val attributes = entityAttributeService.getForEntity(entityId, emptySet(), emptySet()) + updateState(entityId, modifiedAt, attributes).bind() } + + if (updateResult.updated.isNotEmpty()) + entityEventService.publishAttributeChangeEvents( + sub, + entityId, + expandedAttribute.toExpandedAttributes(), + updateResult, + false + ) + updateResult } @@ -549,41 +412,43 @@ class EntityPayloadService( suspend fun upsertAttributes( entityId: URI, expandedAttributes: ExpandedAttributes, - sub: Sub? - ): Either = - either { - val createdAt = ZonedDateTime.now(ZoneOffset.UTC) - expandedAttributes.forEach { (attributeName, expandedAttributeInstances) -> - expandedAttributeInstances.forEach { expandedAttributeInstance -> - val jsonLdAttribute = mapOf(attributeName to listOf(expandedAttributeInstance)) - val ngsiLdAttribute = jsonLdAttribute.toNgsiLdAttributes().bind()[0] - - temporalEntityAttributeService.upsertEntityAttributes( - entityId, - ngsiLdAttribute, - jsonLdAttribute, - createdAt, - sub - ).bind() - } + sub: Sub? = null + ): Either = either { + val createdAt = ZonedDateTime.now(ZoneOffset.UTC) + expandedAttributes.forEach { (attributeName, expandedAttributeInstances) -> + expandedAttributeInstances.forEach { expandedAttributeInstance -> + val jsonLdAttribute = mapOf(attributeName to listOf(expandedAttributeInstance)) + val ngsiLdAttribute = jsonLdAttribute.toNgsiLdAttributes().bind()[0] + + entityAttributeService.upsertAttributes( + entityId, + ngsiLdAttribute, + jsonLdAttribute, + createdAt, + sub + ).bind() } - updateState( - entityId, - createdAt, - temporalEntityAttributeService.getForEntity(entityId, emptySet(), emptySet()) - ).bind() } + updateState( + entityId, + createdAt, + entityAttributeService.getForEntity(entityId, emptySet(), emptySet()) + ).bind() + } @Transactional suspend fun replaceAttribute( entityId: URI, expandedAttribute: ExpandedAttribute, - sub: Sub? + sub: Sub? = null ): Either = either { + entityQueryService.checkEntityExistence(entityId).bind() + authorizationService.userCanUpdateEntity(entityId, sub.toOption()).bind() + val ngsiLdAttribute = listOf(expandedAttribute).toMap().toNgsiLdAttributes().bind()[0] val replacedAt = ngsiLdDateTime() - val updateResult = temporalEntityAttributeService.replaceEntityAttribute( + val updateResult = entityAttributeService.replaceAttribute( entityId, ngsiLdAttribute, expandedAttribute, @@ -593,9 +458,19 @@ class EntityPayloadService( // update modifiedAt in entity if at least one attribute has been added if (updateResult.hasSuccessfulUpdate()) { - val teas = temporalEntityAttributeService.getForEntity(entityId, emptySet(), emptySet()) - updateState(entityId, replacedAt, teas).bind() + val attributes = entityAttributeService.getForEntity(entityId, emptySet(), emptySet()) + updateState(entityId, replacedAt, attributes).bind() } + + if (updateResult.updated.isNotEmpty()) + entityEventService.publishAttributeChangeEvents( + sub, + entityId, + expandedAttribute.toExpandedAttributes(), + updateResult, + false + ) + updateResult } @@ -603,12 +478,12 @@ class EntityPayloadService( suspend fun updateState( entityUri: URI, modifiedAt: ZonedDateTime, - temporalEntityAttributes: List + attributes: List ): Either = - retrieve(entityUri) + entityQueryService.retrieve(entityUri) .map { entityPayload -> val payload = buildJsonLdEntity( - temporalEntityAttributes, + attributes, entityPayload.copy(modifiedAt = modifiedAt) ) databaseClient.sql( @@ -626,18 +501,18 @@ class EntityPayloadService( } private fun buildJsonLdEntity( - temporalEntityAttributes: List, - entityPayload: EntityPayload + attributes: List, + entity: Entity ): Map { - val entityCoreAttributes = entityPayload.serializeProperties() - val expandedAttributes = temporalEntityAttributes - .groupBy { tea -> - tea.attributeName + val entityCoreAttributes = entity.serializeProperties() + val expandedAttributes = attributes + .groupBy { attribute -> + attribute.attributeName } - .mapValues { (_, teas) -> - teas.map { tea -> - tea.payload.deserializeExpandedPayload() - .addSysAttrs(withSysAttrs = true, tea.createdAt, tea.modifiedAt) + .mapValues { (_, attributes) -> + attributes.map { attribute -> + attribute.payload.deserializeExpandedPayload() + .addSysAttrs(withSysAttrs = true, attribute.createdAt, attribute.modifiedAt) } } @@ -659,7 +534,21 @@ class EntityPayloadService( .execute() @Transactional - suspend fun deleteEntity(entityId: URI): Either = either { + suspend fun deleteEntity(entityId: URI, sub: Sub? = null): Either = either { + entityQueryService.checkEntityExistence(entityId).bind() + authorizationService.userCanAdminEntity(entityId, sub.toOption()).bind() + + val entity = deleteEntityPayload(entityId).bind() + + entityAttributeService.deleteAttributes(entityId).bind() + scopeService.deleteHistory(entityId).bind() + authorizationService.removeRightsOnEntity(entityId).bind() + + entityEventService.publishEntityDeleteEvent(sub, entity) + } + + @Transactional + suspend fun deleteEntityPayload(entityId: URI): Either = either { val entity = databaseClient.sql( """ DELETE FROM entity_payload @@ -669,12 +558,9 @@ class EntityPayloadService( ) .bind("entity_id", entityId) .oneToResult { - rowToEntityPaylaod(it) + it.rowToEntity() } .bind() - - temporalEntityAttributeService.deleteTemporalAttributesOfEntity(entityId).bind() - scopeService.deleteHistory(entityId).bind() entity } @@ -683,17 +569,20 @@ class EntityPayloadService( entityId: URI, attributeName: ExpandedTerm, datasetId: URI?, - deleteAll: Boolean = false + deleteAll: Boolean = false, + sub: Sub? = null ): Either = either { + authorizationService.userCanUpdateEntity(entityId, sub.toOption()).bind() + when (attributeName) { NGSILD_SCOPE_PROPERTY -> scopeService.delete(entityId).bind() else -> { - temporalEntityAttributeService.checkEntityAndAttributeExistence( + entityAttributeService.checkEntityAndAttributeExistence( entityId, attributeName, datasetId ).bind() - temporalEntityAttributeService.deleteTemporalAttribute( + entityAttributeService.deleteAttribute( entityId, attributeName, datasetId, @@ -704,36 +593,15 @@ class EntityPayloadService( updateState( entityId, ngsiLdDateTime(), - temporalEntityAttributeService.getForEntity(entityId, emptySet(), emptySet()) + entityAttributeService.getForEntity(entityId, emptySet(), emptySet()) ).bind() - } - suspend fun updateSpecificAccessPolicy( - entityId: URI, - ngsiLdAttribute: NgsiLdAttribute - ): Either = either { - val specificAccessPolicy = ngsiLdAttribute.getSpecificAccessPolicy().bind() - databaseClient.sql( - """ - UPDATE entity_payload - SET specific_access_policy = :specific_access_policy - WHERE entity_id = :entity_id - """.trimIndent() + entityEventService.publishAttributeDeleteEvent( + sub, + entityId, + attributeName, + datasetId, + deleteAll ) - .bind("entity_id", entityId) - .bind("specific_access_policy", specificAccessPolicy.toString()) - .execute() - .bind() } - - suspend fun removeSpecificAccessPolicy(entityId: URI): Either = - databaseClient.sql( - """ - UPDATE entity_payload - SET specific_access_policy = null - WHERE entity_id = :entity_id - """.trimIndent() - ) - .bind("entity_id", entityId) - .execute() } 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 new file mode 100644 index 000000000..32d2c2c35 --- /dev/null +++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/util/AttributeUtils.kt @@ -0,0 +1,190 @@ +package com.egm.stellio.search.entity.util + +import arrow.core.Either +import arrow.core.left +import arrow.core.raise.either +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.AttributeMetadata +import com.egm.stellio.shared.model.APIException +import com.egm.stellio.shared.model.BadRequestDataException +import com.egm.stellio.shared.model.ExpandedAttributeInstance +import com.egm.stellio.shared.model.NgsiLdAttributeInstance +import com.egm.stellio.shared.model.NgsiLdEntity +import com.egm.stellio.shared.model.NgsiLdGeoPropertyInstance +import com.egm.stellio.shared.model.NgsiLdJsonPropertyInstance +import com.egm.stellio.shared.model.NgsiLdLanguagePropertyInstance +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.getPropertyValue +import com.egm.stellio.shared.util.JsonLdUtils +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.time.LocalDate +import java.time.LocalTime +import java.time.ZonedDateTime + +fun NgsiLdEntity.prepareAttributes(): Either>> { + val ngsiLdEntity = this + return either { + ngsiLdEntity.attributes + .flatMap { ngsiLdAttribute -> + ngsiLdAttribute.getAttributeInstances().map { Pair(ngsiLdAttribute, it) } + } + .map { + Pair(it.first.name, it.second.toAttributeMetadata().bind()) + } + } +} + +fun NgsiLdAttributeInstance.toAttributeMetadata(): Either { + val (attributeType, attributeValueType, attributeValue) = when (this) { + is NgsiLdPropertyInstance -> + guessPropertyValueType(this).let { + Triple(Attribute.AttributeType.Property, it.first, it.second) + } + is NgsiLdRelationshipInstance -> + Triple( + Attribute.AttributeType.Relationship, + Attribute.AttributeValueType.URI, + Triple(this.objectId.toString(), null, null) + ) + is NgsiLdGeoPropertyInstance -> + Triple( + Attribute.AttributeType.GeoProperty, + Attribute.AttributeValueType.GEOMETRY, + Triple(null, null, this.coordinates) + ) + is NgsiLdJsonPropertyInstance -> + Triple( + Attribute.AttributeType.JsonProperty, + Attribute.AttributeValueType.JSON, + Triple(JsonUtils.serializeObject(this.json), null, null) + ) + is NgsiLdLanguagePropertyInstance -> + Triple( + Attribute.AttributeType.LanguageProperty, + Attribute.AttributeValueType.ARRAY, + Triple(JsonUtils.serializeObject(this.languageMap), null, null) + ) + is NgsiLdVocabPropertyInstance -> + Triple( + Attribute.AttributeType.VocabProperty, + Attribute.AttributeValueType.ARRAY, + Triple(JsonUtils.serializeObject(this.vocab), null, null) + ) + } + if (attributeValue == Triple(null, null, null)) { + JsonLdUtils.logger.warn("Unable to get a value from attribute: $this") + return BadRequestDataException("Unable to get a value from attribute: $this").left() + } + + return AttributeMetadata( + measuredValue = attributeValue.second, + value = attributeValue.first, + geoValue = attributeValue.third, + valueType = attributeValueType, + datasetId = this.datasetId, + type = attributeType, + observedAt = this.observedAt + ).right() +} + +fun guessAttributeValueType( + attributeType: Attribute.AttributeType, + expandedAttributeInstance: ExpandedAttributeInstance +): Attribute.AttributeValueType = + when (attributeType) { + Attribute.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 + } + +fun guessPropertyValueType( + ngsiLdPropertyInstance: NgsiLdPropertyInstance +): Pair> = + guessPropertyValueType(ngsiLdPropertyInstance.value) + +fun guessPropertyValueType( + value: Any +): Pair> = + when (value) { + is Double -> Pair(Attribute.AttributeValueType.NUMBER, Triple(null, valueToDoubleOrNull(value), null)) + is Int -> Pair(Attribute.AttributeValueType.NUMBER, Triple(null, valueToDoubleOrNull(value), null)) + is Map<*, *> -> Pair(Attribute.AttributeValueType.OBJECT, Triple(JsonUtils.serializeObject(value), null, null)) + is List<*> -> Pair(Attribute.AttributeValueType.ARRAY, Triple(JsonUtils.serializeObject(value), null, null)) + is String -> Pair(Attribute.AttributeValueType.STRING, Triple(value, null, null)) + is Boolean -> Pair(Attribute.AttributeValueType.BOOLEAN, Triple(value.toString(), null, null)) + is LocalDate -> Pair(Attribute.AttributeValueType.DATE, Triple(value.toString(), null, null)) + is ZonedDateTime -> Pair(Attribute.AttributeValueType.DATETIME, Triple(value.toString(), null, null)) + is LocalTime -> Pair(Attribute.AttributeValueType.TIME, Triple(value.toString(), null, null)) + else -> Pair(Attribute.AttributeValueType.STRING, Triple(value.toString(), null, null)) + } + +fun Json.toExpandedAttributeInstance(): ExpandedAttributeInstance = + this.deserializeAsMap() as ExpandedAttributeInstance + +fun partialUpdatePatch( + source: ExpandedAttributeInstance, + update: ExpandedAttributeInstance +): Pair { + val target = source.plus(update) + return Pair(JsonUtils.serializeObject(target), target) +} + +fun mergePatch( + source: ExpandedAttributeInstance, + update: ExpandedAttributeInstance +): Pair { + val target = source.toMutableMap() + update.forEach { (attrName, attrValue) -> + if (!source.containsKey(attrName)) { + target[attrName] = attrValue + } else if ( + listOf( + JsonLdUtils.NGSILD_JSONPROPERTY_VALUE, + JsonLdUtils.NGSILD_VOCABPROPERTY_VALUE, + JsonLdUtils.NGSILD_PROPERTY_VALUE + ).contains(attrName) + ) { + if (attrValue.size > 1) { + // a Property holding an array of value or a JsonPropery holding an array of JSON objects + // cannot be safely merged patch, so copy the whole value from the update + target[attrName] = attrValue + } else { + target[attrName] = listOf( + JsonMerger().merge( + JsonUtils.serializeObject(source[attrName]!![0]), + JsonUtils.serializeObject(attrValue[0]) + ).deserializeAsMap() + ) + } + } else if (listOf(JsonLdUtils.NGSILD_LANGUAGEPROPERTY_VALUE).contains(attrName)) { + val sourceLangEntries = source[attrName] as List> + val targetLangEntries = sourceLangEntries.toMutableList() + (attrValue as List>).forEach { langEntry -> + // remove any previously existing entry for this language + targetLangEntries.removeIf { + it[JsonLdUtils.JSONLD_LANGUAGE] == langEntry[JsonLdUtils.JSONLD_LANGUAGE] + } + targetLangEntries.add(langEntry) + } + + target[attrName] = targetLangEntries + } else { + target[attrName] = attrValue + } + } + + return Pair(JsonUtils.serializeObject(target), target) +} diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/util/EntitiesQueryUtils.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/util/EntitiesQueryUtils.kt new file mode 100644 index 000000000..1c32efcf5 --- /dev/null +++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/util/EntitiesQueryUtils.kt @@ -0,0 +1,105 @@ +package com.egm.stellio.search.entity.util + +import arrow.core.* +import arrow.core.raise.either +import com.egm.stellio.search.common.model.Query +import com.egm.stellio.search.entity.model.EntitiesQuery +import com.egm.stellio.shared.config.ApplicationProperties +import com.egm.stellio.shared.model.APIException +import com.egm.stellio.shared.model.BadRequestDataException +import com.egm.stellio.shared.util.* +import org.springframework.util.MultiValueMap + +fun composeEntitiesQuery( + defaultPagination: ApplicationProperties.Pagination, + requestParams: MultiValueMap, + contexts: List +): Either = either { + val ids = requestParams.getFirst(QUERY_PARAM_ID)?.split(",").orEmpty().toListOfUri().toSet() + val typeSelection = expandTypeSelection(requestParams.getFirst(QUERY_PARAM_TYPE), contexts) + val idPattern = validateIdPattern(requestParams.getFirst(QUERY_PARAM_ID_PATTERN)).bind() + + /** + * Decoding query parameters is not supported by default so a call to a decode function was added query + * with the right parameters values + */ + val q = requestParams.getFirst(QUERY_PARAM_Q)?.decode() + val scopeQ = requestParams.getFirst(QUERY_PARAM_SCOPEQ) + val attrs = parseAndExpandRequestParameter(requestParams.getFirst(QUERY_PARAM_ATTRS), contexts) + val datasetId = parseRequestParameter(requestParams.getFirst(QUERY_PARAM_DATASET_ID)) + val paginationQuery = parsePaginationParameters( + requestParams, + defaultPagination.limitDefault, + defaultPagination.limitMax + ).bind() + + val geoQuery = parseGeoQueryParameters(requestParams.toSingleValueMap(), contexts).bind() + + EntitiesQuery( + ids = ids, + typeSelection = typeSelection, + idPattern = idPattern, + q = q, + scopeQ = scopeQ, + paginationQuery = paginationQuery, + attrs = attrs, + datasetId = datasetId, + geoQuery = geoQuery, + contexts = contexts + ) +} + +fun EntitiesQuery.validateMinimalQueryEntitiesParameters(): Either = either { + if ( + geoQuery == null && + q.isNullOrEmpty() && + typeSelection.isNullOrEmpty() && + attrs.isEmpty() + ) + return@either BadRequestDataException( + "One of 'type', 'attrs', 'q', 'geoQ' must be provided in the query" + ).left().bind() + + this@validateMinimalQueryEntitiesParameters +} + +fun composeEntitiesQueryFromPostRequest( + defaultPagination: ApplicationProperties.Pagination, + query: Query, + requestParams: MultiValueMap, + contexts: List +): Either = either { + val entitySelector = query.entities?.get(0) + val typeSelection = expandTypeSelection(entitySelector?.typeSelection, contexts) + val idPattern = validateIdPattern(entitySelector?.idPattern).bind() + val attrs = query.attrs.orEmpty().map { JsonLdUtils.expandJsonLdTerm(it.trim(), contexts) }.toSet() + val datasetId = query.datasetId.orEmpty().toSet() + val geoQuery = if (query.geoQ != null) { + val geoQueryElements = mapOf( + "geometry" to query.geoQ.geometry, + "coordinates" to query.geoQ.coordinates.toString(), + "georel" to query.geoQ.georel, + "geoproperty" to query.geoQ.geoproperty + ) + parseGeoQueryParameters(geoQueryElements, contexts).bind() + } else null + + val paginationQuery = parsePaginationParameters( + requestParams, + defaultPagination.limitDefault, + defaultPagination.limitMax + ).bind() + + EntitiesQuery( + ids = setOfNotNull(entitySelector?.id), + typeSelection = typeSelection, + idPattern = idPattern, + q = query.q?.decode(), + scopeQ = query.scopeQ, + paginationQuery = paginationQuery, + attrs = attrs, + datasetId = datasetId, + geoQuery = geoQuery, + contexts = contexts + ) +} diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/util/EntityUtils.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/util/EntityUtils.kt new file mode 100644 index 000000000..2673b3615 --- /dev/null +++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/util/EntityUtils.kt @@ -0,0 +1,16 @@ +package com.egm.stellio.search.entity.util + +import com.egm.stellio.search.common.util.* +import com.egm.stellio.search.entity.model.Entity +import com.egm.stellio.shared.util.AuthContextModel.SpecificAccessPolicy + +fun Map.rowToEntity(): Entity = + Entity( + entityId = toUri(this["entity_id"]), + types = toList(this["types"]), + scopes = toOptionalList(this["scopes"]), + createdAt = toZonedDateTime(this["created_at"]), + modifiedAt = toOptionalZonedDateTime(this["modified_at"]), + payload = toJson(this["payload"]), + specificAccessPolicy = toOptionalEnum(this["specific_access_policy"]) + ) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/web/BatchAPIResponses.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/web/BatchAPIResponses.kt similarity index 95% rename from search-service/src/main/kotlin/com/egm/stellio/search/web/BatchAPIResponses.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/entity/web/BatchAPIResponses.kt index 421b50bcc..f1c2d3414 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/web/BatchAPIResponses.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/web/BatchAPIResponses.kt @@ -1,6 +1,6 @@ -package com.egm.stellio.search.web +package com.egm.stellio.search.entity.web -import com.egm.stellio.search.model.UpdateResult +import com.egm.stellio.search.entity.model.UpdateResult import com.egm.stellio.shared.model.APIException import com.egm.stellio.shared.model.ExpandedEntity import com.egm.stellio.shared.model.NgsiLdEntity diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/web/EntityHandler.kt similarity index 73% rename from search-service/src/main/kotlin/com/egm/stellio/search/web/EntityHandler.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/entity/web/EntityHandler.kt index 179278523..793539e56 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/web/EntityHandler.kt @@ -1,15 +1,13 @@ -package com.egm.stellio.search.web +package com.egm.stellio.search.entity.web import arrow.core.getOrElse import arrow.core.left import arrow.core.raise.either import arrow.core.right -import com.egm.stellio.search.authorization.AuthorizationService -import com.egm.stellio.search.service.EntityEventService -import com.egm.stellio.search.service.EntityPayloadService -import com.egm.stellio.search.service.QueryService -import com.egm.stellio.search.util.composeEntitiesQuery -import com.egm.stellio.search.util.validateMinimalQueryEntitiesParameters +import com.egm.stellio.search.entity.service.EntityQueryService +import com.egm.stellio.search.entity.service.EntityService +import com.egm.stellio.search.entity.util.composeEntitiesQuery +import com.egm.stellio.search.entity.util.validateMinimalQueryEntitiesParameters import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.model.* import com.egm.stellio.shared.util.* @@ -35,10 +33,8 @@ import java.util.Optional @RequestMapping("/ngsi-ld/v1/entities") class EntityHandler( private val applicationProperties: ApplicationProperties, - private val entityPayloadService: EntityPayloadService, - private val queryService: QueryService, - private val authorizationService: AuthorizationService, - private val entityEventService: EntityEventService + private val entityService: EntityService, + private val entityQueryService: EntityQueryService ) : BaseHandler() { /** @@ -55,21 +51,7 @@ class EntityHandler( val expandedEntity = expandJsonLdEntity(body, contexts) val ngsiLdEntity = expandedEntity.toNgsiLdEntity().bind() - authorizationService.userCanCreateEntities(sub).bind() - entityPayloadService.checkEntityExistence(ngsiLdEntity.id, true).bind() - - entityPayloadService.createEntity( - ngsiLdEntity, - expandedEntity, - sub.getOrNull() - ).bind() - authorizationService.createOwnerRight(ngsiLdEntity.id, sub).bind() - - entityEventService.publishEntityCreateEvent( - sub.getOrNull(), - ngsiLdEntity.id, - ngsiLdEntity.types - ) + entityService.createEntity(ngsiLdEntity, expandedEntity, sub.getOrNull()).bind() ResponseEntity.status(HttpStatus.CREATED) .location(URI("/ngsi-ld/v1/entities/${ngsiLdEntity.id}")) @@ -90,36 +72,20 @@ class EntityHandler( @RequestBody requestBody: Mono ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() - - entityPayloadService.checkEntityExistence(entityId).bind() - authorizationService.userCanUpdateEntity(entityId, sub).bind() - val (body, contexts) = extractPayloadAndContexts(requestBody, httpHeaders, applicationProperties.contexts.core).bind() - val observedAt = options.getFirst(QUERY_PARAM_OPTIONS_OBSERVEDAT_VALUE) ?.parseTimeParameter("'observedAt' parameter is not a valid date") ?.getOrElse { return@either BadRequestDataException(it).left().bind>() } - val expandedAttributes = expandAttributes(body, contexts) - val updateResult = entityPayloadService.mergeEntity( + val updateResult = entityService.mergeEntity( entityId, expandedAttributes, observedAt, sub.getOrNull() ).bind() - if (updateResult.updated.isNotEmpty()) { - entityEventService.publishAttributeChangeEvents( - sub.getOrNull(), - entityId, - expandedAttributes, - updateResult, - true - ) - } - if (updateResult.notUpdated.isEmpty()) ResponseEntity.status(HttpStatus.NO_CONTENT).build() else @@ -145,30 +111,20 @@ class EntityHandler( val sub = getSubFromSecurityContext() val (body, contexts) = extractPayloadAndContexts(requestBody, httpHeaders, applicationProperties.contexts.core).bind() - val expandedEntity = expandJsonLdEntity(body, contexts) val ngsiLdEntity = expandedEntity.toNgsiLdEntity().bind() - entityPayloadService.checkEntityExistence(entityId).bind() - authorizationService.userCanUpdateEntity(entityId, sub).bind() - if (ngsiLdEntity.id != entityId) BadRequestDataException("The id contained in the body is not the same as the one provided in the URL") .left().bind>() - entityPayloadService.replaceEntity( + entityService.replaceEntity( entityId, ngsiLdEntity, expandedEntity, sub.getOrNull() ).bind() - entityEventService.publishEntityReplaceEvent( - sub.getOrNull(), - ngsiLdEntity.id, - ngsiLdEntity.types - ) - ResponseEntity.status(HttpStatus.NO_CONTENT).build() }.fold( { it.toErrorResponse() }, @@ -198,8 +154,7 @@ class EntityHandler( ).bind() .validateMinimalQueryEntitiesParameters().bind() - val accessRightFilter = authorizationService.computeAccessRightFilter(sub) - val (entities, count) = queryService.queryEntities(entitiesQuery, accessRightFilter).bind() + val (entities, count) = entityQueryService.queryEntities(entitiesQuery, sub.getOrNull()).bind() val filteredEntities = entities.filterAttributes(entitiesQuery.attrs, entitiesQuery.datasetId) @@ -239,11 +194,7 @@ class EntityHandler( contexts ).bind() - entityPayloadService.checkEntityExistence(entityId).bind() - - authorizationService.userCanReadEntity(entityId, sub).bind() - - val expandedEntity = queryService.queryEntity(entityId).bind() + val expandedEntity = entityQueryService.queryEntity(entityId, sub.getOrNull()).bind() expandedEntity.checkContainsAnyOf(queryParams.attrs).bind() @@ -269,14 +220,7 @@ class EntityHandler( ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() - entityPayloadService.checkEntityExistence(entityId).bind() - authorizationService.userCanAdminEntity(entityId, sub).bind() - - val entity = entityPayloadService.retrieve(entityId).bind() - entityPayloadService.deleteEntity(entityId).bind() - authorizationService.removeRightsOnEntity(entityId).bind() - - entityEventService.publishEntityDeleteEvent(sub.getOrNull(), entity) + entityService.deleteEntity(entityId, sub.getOrNull()).bind() ResponseEntity.status(HttpStatus.NO_CONTENT).build() }.fold( @@ -298,31 +242,17 @@ class EntityHandler( val sub = getSubFromSecurityContext() val disallowOverwrite = options.map { it == QUERY_PARAM_OPTIONS_NOOVERWRITE_VALUE }.orElse(false) - entityPayloadService.checkEntityExistence(entityId).bind() - val (body, contexts) = extractPayloadAndContexts(requestBody, httpHeaders, applicationProperties.contexts.core).bind() val expandedAttributes = expandAttributes(body, contexts) - authorizationService.userCanUpdateEntity(entityId, sub).bind() - - val updateResult = entityPayloadService.appendAttributes( + val updateResult = entityService.appendAttributes( entityId, expandedAttributes, disallowOverwrite, sub.getOrNull() ).bind() - if (updateResult.hasSuccessfulUpdate()) { - entityEventService.publishAttributeChangeEvents( - sub.getOrNull(), - entityId, - expandedAttributes, - updateResult, - true - ) - } - if (updateResult.notUpdated.isEmpty()) ResponseEntity.status(HttpStatus.NO_CONTENT).build() else @@ -354,25 +284,12 @@ class EntityHandler( extractPayloadAndContexts(requestBody, httpHeaders, applicationProperties.contexts.core).bind() val expandedAttributes = expandAttributes(body, contexts) - entityPayloadService.checkEntityExistence(entityId).bind() - authorizationService.userCanUpdateEntity(entityId, sub).bind() - - val updateResult = entityPayloadService.updateAttributes( + val updateResult = entityService.updateAttributes( entityId, expandedAttributes, sub.getOrNull() ).bind() - if (updateResult.updated.isNotEmpty()) { - entityEventService.publishAttributeChangeEvents( - sub.getOrNull(), - entityId, - expandedAttributes, - updateResult, - true - ) - } - if (updateResult.notUpdated.isEmpty()) ResponseEntity.status(HttpStatus.NO_CONTENT).build() else @@ -402,16 +319,13 @@ class EntityHandler( ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() - entityPayloadService.checkEntityExistence(entityId).bind() - authorizationService.userCanUpdateEntity(entityId, sub).bind() - // We expect an NGSI-LD Attribute Fragment which should be a JSON-LD Object (see 5.4) val (body, contexts) = extractPayloadAndContexts(requestBody, httpHeaders, applicationProperties.contexts.core).bind() val expandedAttribute = expandAttribute(attrId, body, contexts) - entityPayloadService.partialUpdateAttribute( + entityService.partialUpdateAttribute( entityId, expandedAttribute, sub.getOrNull() @@ -420,17 +334,8 @@ class EntityHandler( .let { if (it.updated.isEmpty()) ResourceNotFoundException("Unknown attribute in entity $entityId").left() - else { - entityEventService.publishAttributeChangeEvents( - sub.getOrNull(), - entityId, - expandedAttribute.toExpandedAttributes(), - it, - false - ) - + else ResponseEntity.status(HttpStatus.NO_CONTENT).build().right() - } }.bind() }.fold( { it.toErrorResponse() }, @@ -458,22 +363,14 @@ class EntityHandler( val contexts = getContextFromLinkHeaderOrDefault(httpHeaders, applicationProperties.contexts.core).bind() val expandedAttrId = expandJsonLdTerm(attrId, contexts) - authorizationService.userCanUpdateEntity(entityId, sub).bind() - - entityPayloadService.deleteAttribute( + entityService.deleteAttribute( entityId, expandedAttrId, datasetId, - deleteAll + deleteAll, + sub.getOrNull() ).bind() - entityEventService.publishAttributeDeleteEvent( - sub.getOrNull(), - entityId, - expandedAttrId, - datasetId, - deleteAll - ) ResponseEntity.status(HttpStatus.NO_CONTENT).build() }.fold( { it.toErrorResponse() }, @@ -498,26 +395,14 @@ class EntityHandler( val (body, contexts) = extractPayloadAndContexts(requestBody, httpHeaders, applicationProperties.contexts.core).bind() - entityPayloadService.checkEntityExistence(entityId).bind() - authorizationService.userCanUpdateEntity(entityId, sub).bind() - val expandedAttribute = expandAttribute(attrId, body, contexts) - entityPayloadService.replaceAttribute(entityId, expandedAttribute, sub.getOrNull()).bind() + entityService.replaceAttribute(entityId, expandedAttribute, sub.getOrNull()).bind() .let { if (it.updated.isEmpty()) ResourceNotFoundException(it.notUpdated[0].reason).left() - else { - entityEventService.publishAttributeChangeEvents( - sub.getOrNull(), - entityId, - expandedAttribute.toExpandedAttributes(), - it, - false - ) - + else ResponseEntity.status(HttpStatus.NO_CONTENT).build().right() - } }.bind() }.fold( { it.toErrorResponse() }, diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityOperationHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/web/EntityOperationHandler.kt similarity index 63% rename from search-service/src/main/kotlin/com/egm/stellio/search/web/EntityOperationHandler.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/entity/web/EntityOperationHandler.kt index 92a7152a9..73dfd5bbf 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityOperationHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/web/EntityOperationHandler.kt @@ -1,14 +1,12 @@ -package com.egm.stellio.search.web +package com.egm.stellio.search.entity.web import arrow.core.* import arrow.core.raise.either -import com.egm.stellio.search.authorization.AuthorizationService -import com.egm.stellio.search.model.Query -import com.egm.stellio.search.service.EntityOperationService -import com.egm.stellio.search.service.EntityPayloadService -import com.egm.stellio.search.service.QueryService -import com.egm.stellio.search.util.composeEntitiesQueryFromPostRequest -import com.egm.stellio.search.util.validateMinimalQueryEntitiesParameters +import com.egm.stellio.search.common.model.Query +import com.egm.stellio.search.entity.service.EntityOperationService +import com.egm.stellio.search.entity.service.EntityQueryService +import com.egm.stellio.search.entity.util.composeEntitiesQueryFromPostRequest +import com.egm.stellio.search.entity.util.validateMinimalQueryEntitiesParameters import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.model.* import com.egm.stellio.shared.util.* @@ -33,9 +31,7 @@ import java.util.Optional class EntityOperationHandler( private val applicationProperties: ApplicationProperties, private val entityOperationService: EntityOperationService, - private val entityPayloadService: EntityPayloadService, - private val queryService: QueryService, - private val authorizationService: AuthorizationService + private val entityQueryService: EntityQueryService, ) { /** @@ -50,21 +46,15 @@ class EntityOperationHandler( val (parsedEntities, unparsableEntities) = prepareEntitiesFromRequestBody(requestBody, httpHeaders).bind() - val (uniqueEntities, duplicateEntities) = - entityOperationService.splitEntitiesByUniqueness(parsedEntities) - val (existingEntities, newEntities) = entityOperationService.splitEntitiesByExistence(uniqueEntities) - val (unauthorizedEntities, authorizedEntities) = newEntities.partition { - authorizationService.userCanCreateEntities(sub).isLeft() - } val batchOperationResult = BatchOperationResult().apply { addEntitiesToErrors(unparsableEntities) - addEntitiesToErrors(duplicateEntities.extractNgsiLdEntities(), ENTITY_ALREADY_EXISTS_MESSAGE) - addEntitiesToErrors(existingEntities.extractNgsiLdEntities(), ENTITY_ALREADY_EXISTS_MESSAGE) - addEntitiesToErrors(unauthorizedEntities.extractNgsiLdEntities(), ENTITIY_CREATION_FORBIDDEN_MESSAGE) } - doBatchCreation(authorizedEntities, batchOperationResult, sub) - + if (parsedEntities.isNotEmpty()) { + val createOperationResult = entityOperationService.create(parsedEntities, sub.getOrNull()) + batchOperationResult.errors.addAll(createOperationResult.errors) + batchOperationResult.success.addAll(createOperationResult.success) + } if (batchOperationResult.errors.isEmpty()) ResponseEntity.status(HttpStatus.CREATED).body(batchOperationResult.getSuccessfulEntitiesIds()) else @@ -87,43 +77,24 @@ class EntityOperationHandler( val (parsedEntities, unparsableEntities) = prepareEntitiesFromRequestBody(requestBody, httpHeaders).bind() - val (existingEntities, newEntities) = entityOperationService.splitEntitiesByExistence(parsedEntities) - - val (newUnauthorizedEntities, newAuthorizedEntities) = newEntities.partition { - authorizationService.userCanCreateEntities(sub).isLeft() - } val batchOperationResult = BatchOperationResult().apply { addEntitiesToErrors(unparsableEntities) - addEntitiesToErrors(newUnauthorizedEntities.extractNgsiLdEntities(), ENTITIY_CREATION_FORBIDDEN_MESSAGE) } - val (newUniqueEntities, duplicatedEntities) = - entityOperationService.splitEntitiesByUniqueness(newAuthorizedEntities) - val existingOrDuplicatedEntities = existingEntities.plus(duplicatedEntities) - - doBatchCreation(newUniqueEntities, batchOperationResult, sub) - - val (existingEntitiesUnauthorized, existingEntitiesAuthorized) = - existingOrDuplicatedEntities.partition { - authorizationService.userCanUpdateEntity(it.entityId(), sub).isLeft() - } - batchOperationResult.addEntitiesToErrors( - existingEntitiesUnauthorized.extractNgsiLdEntities(), - ENTITY_UPDATE_FORBIDDEN_MESSAGE - ) - - if (existingEntitiesAuthorized.isNotEmpty()) { - val updateOperationResult = when (options) { - "update" -> entityOperationService.update(existingEntitiesAuthorized, false, sub.getOrNull()) - else -> entityOperationService.replace(existingEntitiesAuthorized, sub.getOrNull()) - } + val newUniqueEntities = if (parsedEntities.isNotEmpty()) { + val (updateOperationResult, newUniqueEntities) = entityOperationService.upsert( + parsedEntities, + options, + sub.getOrNull() + ) batchOperationResult.errors.addAll(updateOperationResult.errors) batchOperationResult.success.addAll(updateOperationResult.success) - } + newUniqueEntities + } else emptyList() if (batchOperationResult.errors.isEmpty() && newUniqueEntities.isNotEmpty()) - ResponseEntity.status(HttpStatus.CREATED).body(newUniqueEntities.map { it.entityId() }) + ResponseEntity.status(HttpStatus.CREATED).body(newUniqueEntities.map { it }) else if (batchOperationResult.errors.isEmpty()) ResponseEntity.status(HttpStatus.NO_CONTENT).build() else @@ -146,28 +117,21 @@ class EntityOperationHandler( val (parsedEntities, unparsableEntities) = prepareEntitiesFromRequestBody(requestBody, httpHeaders).bind() - val (existingEntities, unknownEntities) = entityOperationService.splitEntitiesByExistence(parsedEntities) - val disallowOverwrite = options.map { it == QUERY_PARAM_OPTIONS_NOOVERWRITE_VALUE }.orElse(false) - val (existingEntitiesUnauthorized, existingEntitiesAuthorized) = - existingEntities.partition { authorizationService.userCanUpdateEntity(it.entityId(), sub).isLeft() } - val batchOperationResult = BatchOperationResult().apply { addEntitiesToErrors(unparsableEntities) - addEntitiesToErrors(unknownEntities.extractNgsiLdEntities(), ENTITY_DOES_NOT_EXIST_MESSAGE) - addEntitiesToErrors(existingEntitiesUnauthorized.extractNgsiLdEntities(), ENTITY_UPDATE_FORBIDDEN_MESSAGE) } - if (existingEntitiesAuthorized.isNotEmpty()) { + if (parsedEntities.isNotEmpty()) { val updateOperationResult = - entityOperationService.update(existingEntitiesAuthorized, disallowOverwrite, sub.getOrNull()) + entityOperationService.update(parsedEntities, disallowOverwrite, sub.getOrNull()) batchOperationResult.errors.addAll(updateOperationResult.errors) batchOperationResult.success.addAll(updateOperationResult.success) } - if (batchOperationResult.errors.isEmpty() && unknownEntities.isEmpty()) + if (batchOperationResult.errors.isEmpty()) ResponseEntity.status(HttpStatus.NO_CONTENT).build() else ResponseEntity.status(HttpStatus.MULTI_STATUS).body(batchOperationResult) @@ -188,26 +152,18 @@ class EntityOperationHandler( val (parsedEntities, unparsableEntities) = prepareEntitiesFromRequestBody(requestBody, httpHeaders).bind() - val (existingEntities, unknownEntities) = entityOperationService.splitEntitiesByExistence(parsedEntities) - - val (existingEntitiesUnauthorized, existingEntitiesAuthorized) = - existingEntities.partition { authorizationService.userCanUpdateEntity(it.entityId(), sub).isLeft() } - val batchOperationResult = BatchOperationResult().apply { addEntitiesToErrors(unparsableEntities) - addEntitiesToErrors(unknownEntities.extractNgsiLdEntities(), ENTITY_DOES_NOT_EXIST_MESSAGE) - addEntitiesToErrors(existingEntitiesUnauthorized.extractNgsiLdEntities(), ENTITY_UPDATE_FORBIDDEN_MESSAGE) } - if (existingEntitiesAuthorized.isNotEmpty()) { + if (parsedEntities.isNotEmpty()) { val mergeOperationResult = - entityOperationService.merge(existingEntitiesAuthorized, sub.getOrNull()) - + entityOperationService.merge(parsedEntities, sub.getOrNull()) batchOperationResult.errors.addAll(mergeOperationResult.errors) batchOperationResult.success.addAll(mergeOperationResult.success) } - if (batchOperationResult.errors.isEmpty() && unknownEntities.isEmpty()) + if (batchOperationResult.errors.isEmpty()) ResponseEntity.status(HttpStatus.NO_CONTENT).build() else ResponseEntity.status(HttpStatus.MULTI_STATUS).body(batchOperationResult) @@ -225,35 +181,15 @@ class EntityOperationHandler( val body = requestBody.awaitFirst() checkBatchRequestBody(body).bind() + val entitiesId = body.toListOfUri() - val (uniqueEntitiesId, duplicateEntitiesId) = - entityOperationService.splitEntitiesIdsByUniqueness(body.toListOfUri()) - - val (existingEntities, unknownEntities) = - entityOperationService.splitEntitiesIdsByExistence(uniqueEntitiesId) - - val (entitiesUserCannotDelete, entitiesUserCanDelete) = - existingEntities.partition { - authorizationService.userCanAdminEntity(it, sub).isLeft() - } - - val entitiesBeforeDelete = - if (entitiesUserCanDelete.isNotEmpty()) - entityPayloadService.retrieve(entitiesUserCanDelete.toList()) - else emptyList() - - val batchOperationResult = BatchOperationResult().apply { - addIdsToErrors(duplicateEntitiesId, ENTITY_DOES_NOT_EXIST_MESSAGE) - addIdsToErrors(unknownEntities, ENTITY_DOES_NOT_EXIST_MESSAGE) - addIdsToErrors(entitiesUserCannotDelete, ENTITY_DELETE_FORBIDDEN_MESSAGE) - } - - if (entitiesUserCanDelete.isNotEmpty()) { - val deleteOperationResult = entityOperationService.delete(entitiesBeforeDelete.toSet(), sub.getOrNull()) - - batchOperationResult.errors.addAll(deleteOperationResult.errors) - batchOperationResult.success.addAll(deleteOperationResult.success) - } + val batchOperationResult = if (entitiesId.isNotEmpty()) { + val deleteOperationResult = entityOperationService.delete(entitiesId, sub.getOrNull()) + BatchOperationResult( + errors = deleteOperationResult.errors, + success = deleteOperationResult.success + ) + } else { BatchOperationResult() } if (batchOperationResult.errors.isEmpty()) ResponseEntity.status(HttpStatus.NO_CONTENT).build() @@ -286,8 +222,7 @@ class EntityOperationHandler( ).bind() .validateMinimalQueryEntitiesParameters().bind() - val accessRightFilter = authorizationService.computeAccessRightFilter(sub) - val (entities, count) = queryService.queryEntities(entitiesQuery, accessRightFilter).bind() + val (entities, count) = entityQueryService.queryEntities(entitiesQuery, sub.getOrNull()).bind() val filteredEntities = entities.filterAttributes(entitiesQuery.attrs, entitiesQuery.datasetId) @@ -362,17 +297,4 @@ class EntityOperationHandler( val context = getContextFromLinkHeader(httpHeaders.getOrEmpty(HttpHeaders.LINK)).bind() expandAndPrepareBatchOfEntities(body, context, httpHeaders.contentType).bind() } - - private suspend fun doBatchCreation( - entitiesToCreate: List, - batchOperationResult: BatchOperationResult, - sub: Option - ) { - if (entitiesToCreate.isNotEmpty()) { - val createOperationResult = entityOperationService.create(entitiesToCreate, sub.getOrNull()) - authorizationService.createOwnerRights(createOperationResult.getSuccessfulEntitiesIds(), sub) - batchOperationResult.errors.addAll(createOperationResult.errors) - batchOperationResult.success.addAll(createOperationResult.success) - } - } } diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/model/EntityTemporalResult.kt b/search-service/src/main/kotlin/com/egm/stellio/search/model/EntityTemporalResult.kt deleted file mode 100644 index e37414feb..000000000 --- a/search-service/src/main/kotlin/com/egm/stellio/search/model/EntityTemporalResult.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.egm.stellio.search.model - -import com.egm.stellio.search.scope.ScopeInstanceResult -import com.egm.stellio.search.util.TemporalEntityAttributeInstancesResult - -data class EntityTemporalResult( - val entityPayload: EntityPayload, - val scopeHistory: List, - val teaInstancesResult: TemporalEntityAttributeInstancesResult -) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/scope/ScopeInstanceResult.kt b/search-service/src/main/kotlin/com/egm/stellio/search/scope/ScopeInstanceResult.kt index a8208d3e1..9c2d1cf3c 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/scope/ScopeInstanceResult.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/scope/ScopeInstanceResult.kt @@ -1,6 +1,6 @@ package com.egm.stellio.search.scope -import com.egm.stellio.search.model.TemporalQuery +import com.egm.stellio.search.temporal.model.TemporalQuery import java.net.URI import java.time.ZonedDateTime diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/scope/ScopeService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/scope/ScopeService.kt index 15a76be1e..7f41a4a33 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/scope/ScopeService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/scope/ScopeService.kt @@ -4,10 +4,14 @@ import arrow.core.Either import arrow.core.left import arrow.core.raise.either import arrow.core.right -import com.egm.stellio.search.model.* -import com.egm.stellio.search.model.AttributeInstance.TemporalProperty -import com.egm.stellio.search.model.TemporalEntityAttribute.AttributeValueType -import com.egm.stellio.search.util.* +import com.egm.stellio.search.common.util.* +import com.egm.stellio.search.entity.model.* +import com.egm.stellio.search.entity.model.Attribute.AttributeValueType +import com.egm.stellio.search.temporal.model.AttributeInstance.TemporalProperty +import com.egm.stellio.search.temporal.model.TemporalEntitiesQuery +import com.egm.stellio.search.temporal.model.TemporalQuery +import com.egm.stellio.search.temporal.util.WHOLE_TIME_RANGE_DURATION +import com.egm.stellio.search.temporal.util.composeAggregationSelectClause import com.egm.stellio.shared.model.* import com.egm.stellio.shared.util.INCONSISTENT_VALUES_IN_AGGREGATION_MESSAGE import com.egm.stellio.shared.util.JsonLdUtils diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/scope/TemporalScopeBuilder.kt b/search-service/src/main/kotlin/com/egm/stellio/search/scope/TemporalScopeBuilder.kt index f4f37d764..0ab5b7113 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/scope/TemporalScopeBuilder.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/scope/TemporalScopeBuilder.kt @@ -1,8 +1,8 @@ package com.egm.stellio.search.scope -import com.egm.stellio.search.model.EntityPayload -import com.egm.stellio.search.model.TemporalEntitiesQuery -import com.egm.stellio.search.model.TemporalQuery +import com.egm.stellio.search.entity.model.Entity +import com.egm.stellio.search.temporal.model.TemporalEntitiesQuery +import com.egm.stellio.search.temporal.model.TemporalQuery import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_LIST import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_TYPE import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_VALUE @@ -17,12 +17,12 @@ import com.egm.stellio.shared.util.JsonLdUtils.buildNonReifiedTemporalValue object TemporalScopeBuilder { fun buildScopeAttributeInstances( - entityPayload: EntityPayload, + entity: Entity, scopeInstances: List, temporalEntitiesQuery: TemporalEntitiesQuery ): Map = // if no history, add an empty map only if entity has a scope - if (entityPayload.scopes == null && scopeInstances.isEmpty()) + if (entity.scopes == null && scopeInstances.isEmpty()) emptyMap() // if no history but entity has a scope, add an empty scope list (no history in the given time range) else if (scopeInstances.isEmpty()) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/model/AttributeInstance.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/model/AttributeInstance.kt similarity index 91% rename from search-service/src/main/kotlin/com/egm/stellio/search/model/AttributeInstance.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/temporal/model/AttributeInstance.kt index afdfe71fc..d8ab30296 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/model/AttributeInstance.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/model/AttributeInstance.kt @@ -1,6 +1,7 @@ -package com.egm.stellio.search.model +package com.egm.stellio.search.temporal.model -import com.egm.stellio.search.util.toJson +import com.egm.stellio.search.common.util.toJson +import com.egm.stellio.search.entity.model.AttributeMetadata import com.egm.stellio.shared.model.ExpandedAttributeInstance import com.egm.stellio.shared.model.WKTCoordinates import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_INSTANCE_ID_PROPERTY @@ -14,7 +15,7 @@ import java.time.ZonedDateTime import java.util.UUID data class AttributeInstance private constructor( - val temporalEntityAttribute: UUID, + val attributeUuid: UUID, val instanceId: URI, val timeProperty: TemporalProperty? = TemporalProperty.OBSERVED_AT, val time: ZonedDateTime, @@ -27,7 +28,7 @@ data class AttributeInstance private constructor( companion object { operator fun invoke( - temporalEntityAttribute: UUID, + attributeUuid: UUID, instanceId: URI = generateRandomInstanceId(), timeProperty: TemporalProperty? = TemporalProperty.OBSERVED_AT, modifiedAt: ZonedDateTime? = null, @@ -36,7 +37,7 @@ data class AttributeInstance private constructor( time: ZonedDateTime, sub: String? = null ): AttributeInstance = AttributeInstance( - temporalEntityAttribute = temporalEntityAttribute, + attributeUuid = attributeUuid, instanceId = instanceId, timeProperty = timeProperty, time = time, @@ -48,14 +49,14 @@ data class AttributeInstance private constructor( ) operator fun invoke( - temporalEntityAttribute: UUID, + attributeUuid: UUID, instanceId: URI = generateRandomInstanceId(), timeAndProperty: Pair, value: Triple, payload: ExpandedAttributeInstance, sub: String? ): AttributeInstance = AttributeInstance( - temporalEntityAttribute = temporalEntityAttribute, + attributeUuid = attributeUuid, instanceId = instanceId, timeProperty = timeAndProperty.second, time = timeAndProperty.first, diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/model/AttributeInstanceResult.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/model/AttributeInstanceResult.kt similarity index 67% rename from search-service/src/main/kotlin/com/egm/stellio/search/model/AttributeInstanceResult.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/temporal/model/AttributeInstanceResult.kt index 1d32f7ab0..ad417fbff 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/model/AttributeInstanceResult.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/model/AttributeInstanceResult.kt @@ -1,9 +1,9 @@ -package com.egm.stellio.search.model +package com.egm.stellio.search.temporal.model import java.time.ZonedDateTime import java.util.UUID -sealed class AttributeInstanceResult(open val temporalEntityAttribute: UUID) : Comparable { +sealed class AttributeInstanceResult(open val attributeUuid: UUID) : Comparable { abstract fun getComparableTime(): ZonedDateTime override fun compareTo(other: AttributeInstanceResult): Int = @@ -11,27 +11,27 @@ sealed class AttributeInstanceResult(open val temporalEntityAttribute: UUID) : C } data class FullAttributeInstanceResult( - override val temporalEntityAttribute: UUID, + override val attributeUuid: UUID, val payload: String, val time: ZonedDateTime, val timeproperty: String, val sub: String? -) : AttributeInstanceResult(temporalEntityAttribute) { +) : AttributeInstanceResult(attributeUuid) { override fun getComparableTime(): ZonedDateTime = time } data class SimplifiedAttributeInstanceResult( - override val temporalEntityAttribute: UUID, + override val attributeUuid: UUID, val value: Any, val time: ZonedDateTime -) : AttributeInstanceResult(temporalEntityAttribute) { +) : AttributeInstanceResult(attributeUuid) { override fun getComparableTime(): ZonedDateTime = time } data class AggregatedAttributeInstanceResult( - override val temporalEntityAttribute: UUID, + override val attributeUuid: UUID, val values: List -) : AttributeInstanceResult(temporalEntityAttribute) { +) : AttributeInstanceResult(attributeUuid) { override fun getComparableTime(): ZonedDateTime = values.first().startDateTime data class AggregateResult( diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/temporal/model/EntityTemporalResult.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/model/EntityTemporalResult.kt new file mode 100644 index 000000000..21f47c850 --- /dev/null +++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/model/EntityTemporalResult.kt @@ -0,0 +1,11 @@ +package com.egm.stellio.search.temporal.model + +import com.egm.stellio.search.entity.model.Entity +import com.egm.stellio.search.scope.ScopeInstanceResult +import com.egm.stellio.search.temporal.util.AttributesWithInstances + +data class EntityTemporalResult( + val entity: Entity, + val scopeHistory: List, + val attributesWithInstances: AttributesWithInstances +) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/model/TemporalEntitiesQuery.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/model/TemporalEntitiesQuery.kt similarity index 90% rename from search-service/src/main/kotlin/com/egm/stellio/search/model/TemporalEntitiesQuery.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/temporal/model/TemporalEntitiesQuery.kt index 6063f3a10..adc92fce3 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/model/TemporalEntitiesQuery.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/model/TemporalEntitiesQuery.kt @@ -1,5 +1,6 @@ -package com.egm.stellio.search.model +package com.egm.stellio.search.temporal.model +import com.egm.stellio.search.entity.model.EntitiesQuery import java.time.Duration import java.time.Period import java.time.temporal.TemporalAmount diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/model/TemporalQuery.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/model/TemporalQuery.kt similarity index 96% rename from search-service/src/main/kotlin/com/egm/stellio/search/model/TemporalQuery.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/temporal/model/TemporalQuery.kt index e54f245e3..e3e016ead 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/model/TemporalQuery.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/model/TemporalQuery.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.model +package com.egm.stellio.search.temporal.model import java.time.ZonedDateTime diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/AttributeInstanceService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/AttributeInstanceService.kt similarity index 86% rename from search-service/src/main/kotlin/com/egm/stellio/search/service/AttributeInstanceService.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/AttributeInstanceService.kt index 2997bbe79..ba6b53544 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/AttributeInstanceService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/AttributeInstanceService.kt @@ -1,14 +1,19 @@ -package com.egm.stellio.search.service +package com.egm.stellio.search.temporal.service import arrow.core.Either import arrow.core.left import arrow.core.raise.either import arrow.core.right import arrow.fx.coroutines.parMap -import com.egm.stellio.search.model.* -import com.egm.stellio.search.model.AggregatedAttributeInstanceResult.AggregateResult -import com.egm.stellio.search.model.TemporalQuery.Timerel -import com.egm.stellio.search.util.* +import com.egm.stellio.search.common.util.* +import com.egm.stellio.search.entity.model.Attribute +import com.egm.stellio.search.entity.model.AttributeMetadata +import com.egm.stellio.search.entity.util.toAttributeMetadata +import com.egm.stellio.search.temporal.model.* +import com.egm.stellio.search.temporal.model.AggregatedAttributeInstanceResult.AggregateResult +import com.egm.stellio.search.temporal.model.TemporalQuery.Timerel +import com.egm.stellio.search.temporal.util.WHOLE_TIME_RANGE_DURATION +import com.egm.stellio.search.temporal.util.composeAggregationSelectClause import com.egm.stellio.shared.model.* import com.egm.stellio.shared.util.INCONSISTENT_VALUES_IN_AGGREGATION_MESSAGE import com.egm.stellio.shared.util.attributeOrInstanceNotFoundMessage @@ -39,7 +44,7 @@ class AttributeInstanceService( (time, measured_value, value, geo_value, temporal_entity_attribute, instance_id, payload) VALUES - (:time, :measured_value, :value, public.ST_GeomFromText(:geo_value), :temporal_entity_attribute, + (:time, :measured_value, :value, public.ST_GeomFromText(:geo_value), :attribute, :instance_id, :payload) ON CONFLICT (time, temporal_entity_attribute) DO UPDATE SET value = :value, measured_value = :measured_value, payload = :payload, @@ -51,7 +56,7 @@ class AttributeInstanceService( (time, measured_value, value, temporal_entity_attribute, instance_id, payload) VALUES - (:time, :measured_value, :value, :temporal_entity_attribute, + (:time, :measured_value, :value, :attribute, :instance_id, :payload) ON CONFLICT (time, temporal_entity_attribute) DO UPDATE SET value = :value, measured_value = :measured_value, payload = :payload, @@ -64,7 +69,7 @@ class AttributeInstanceService( temporal_entity_attribute, instance_id, payload, sub) VALUES (:time, :time_property, :measured_value, :value, public.ST_GeomFromText(:geo_value), - :temporal_entity_attribute, :instance_id, :payload, :sub) + :attribute, :instance_id, :payload, :sub) """.trimIndent() else """ @@ -73,7 +78,7 @@ class AttributeInstanceService( temporal_entity_attribute, instance_id, payload, sub) VALUES (:time, :time_property, :measured_value, :value, - :temporal_entity_attribute, :instance_id, :payload, :sub) + :attribute, :instance_id, :payload, :sub) """.trimIndent() return databaseClient.sql(insertStatement) @@ -85,7 +90,7 @@ class AttributeInstanceService( it.bind("geo_value", attributeInstance.geoValue.value) else it } - .bind("temporal_entity_attribute", attributeInstance.temporalEntityAttribute) + .bind("attribute", attributeInstance.attributeUuid) .bind("instance_id", attributeInstance.instanceId) .bind("payload", attributeInstance.payload) .let { @@ -99,12 +104,12 @@ class AttributeInstanceService( @Transactional suspend fun addAttributeInstance( - temporalEntityAttributeUuid: UUID, + attributeUuid: UUID, attributeMetadata: AttributeMetadata, attributeValues: Map> ): Either { val attributeInstance = AttributeInstance( - temporalEntityAttribute = temporalEntityAttributeUuid, + attributeUuid = attributeUuid, time = attributeMetadata.observedAt!!, attributeMetadata = attributeMetadata, payload = attributeValues @@ -114,20 +119,20 @@ class AttributeInstanceService( suspend fun search( temporalEntitiesQuery: TemporalEntitiesQuery, - temporalEntityAttribute: TemporalEntityAttribute, + attribute: Attribute, origin: ZonedDateTime? = null ): Either> = - search(temporalEntitiesQuery, listOf(temporalEntityAttribute), origin) + search(temporalEntitiesQuery, listOf(attribute), origin) suspend fun search( temporalEntitiesQuery: TemporalEntitiesQuery, - temporalEntityAttributes: List, + attributes: List, origin: ZonedDateTime? = null ): Either> { val temporalQuery = temporalEntitiesQuery.temporalQuery val sqlQueryBuilder = StringBuilder() - sqlQueryBuilder.append(composeSearchSelectStatement(temporalQuery, temporalEntityAttributes, origin)) + sqlQueryBuilder.append(composeSearchSelectStatement(temporalQuery, attributes, origin)) if (!temporalEntitiesQuery.withTemporalValues && !temporalEntitiesQuery.withAggregatedValues) sqlQueryBuilder.append(", payload") @@ -170,7 +175,7 @@ class AttributeInstanceService( sqlQueryBuilder.append(" LIMIT ${temporalQuery.instanceLimit}") - val finalTemporalQuery = composeFinalTemporalQuery(temporalEntityAttributes, sqlQueryBuilder.toString()) + val finalTemporalQuery = composeFinalTemporalQuery(attributes, sqlQueryBuilder.toString()) return databaseClient.sql(finalTemporalQuery) .runCatching { @@ -186,16 +191,16 @@ class AttributeInstanceService( } private fun composeFinalTemporalQuery( - temporalEntityAttributes: List, + attributes: List, aiLateralQuery: String ): String { - val temporalEntityAttributesIds = temporalEntityAttributes.joinToString(",") { "('${it.id}'::uuid)" } + val attributesIds = attributes.joinToString(",") { "('${it.id}'::uuid)" } return """ SELECT ai_limited.* FROM ( SELECT distinct(id) - FROM (VALUES $temporalEntityAttributesIds) as t (id) + FROM (VALUES $attributesIds) as t (id) ) teas JOIN LATERAL ( $aiLateralQuery @@ -205,13 +210,13 @@ class AttributeInstanceService( private fun composeSearchSelectStatement( temporalQuery: TemporalQuery, - temporalEntityAttributes: List, + attributes: List, origin: ZonedDateTime? ) = when { temporalQuery.aggrPeriodDuration != null -> { val aggrPeriodDuration = temporalQuery.aggrPeriodDuration val allAggregates = temporalQuery.aggrMethods - ?.composeAggregationSelectClause(temporalEntityAttributes[0].attributeValueType) + ?.composeAggregationSelectClause(attributes[0].attributeValueType) // if retrieving a temporal entity, origin is calculated beforehand as timeAt is optional in this case // if querying temporal entities, timeAt is mandatory and will be used if origin is null if (aggrPeriodDuration != WHOLE_TIME_RANGE_DURATION) { @@ -225,9 +230,9 @@ class AttributeInstanceService( "SELECT temporal_entity_attribute, min(time) as start, max(time) as end, $allAggregates " } else -> { - val valueColumn = when (temporalEntityAttributes[0].attributeValueType) { - TemporalEntityAttribute.AttributeValueType.NUMBER -> "measured_value as value" - TemporalEntityAttribute.AttributeValueType.GEOMETRY -> "public.ST_AsText(geo_value) as value" + val valueColumn = when (attributes[0].attributeValueType) { + Attribute.AttributeValueType.NUMBER -> "measured_value as value" + Attribute.AttributeValueType.GEOMETRY -> "public.ST_AsText(geo_value) as value" else -> "value" } val subColumn = when (temporalQuery.timeproperty) { @@ -241,9 +246,9 @@ class AttributeInstanceService( suspend fun selectOldestDate( temporalQuery: TemporalQuery, - temporalEntityAttributes: List + attributes: List ): ZonedDateTime? { - val temporalEntityAttributesIds = temporalEntityAttributes.joinToString(",") { "'${it.id}'" } + val attributesIds = attributes.joinToString(",") { "'${it.id}'" } var selectQuery = """ SELECT min(time) as first @@ -254,14 +259,14 @@ class AttributeInstanceService( selectQuery.plus( """ FROM attribute_instance - WHERE temporal_entity_attribute IN($temporalEntityAttributesIds) + WHERE temporal_entity_attribute IN($attributesIds) """ ) else selectQuery.plus( """ FROM attribute_instance_audit - WHERE temporal_entity_attribute IN($temporalEntityAttributesIds) + WHERE temporal_entity_attribute IN($attributesIds) AND time_property = '${temporalQuery.timeproperty.name}' """ ) @@ -289,19 +294,19 @@ class AttributeInstanceService( AggregateResult(it, value, startDateTime, endDateTime) } AggregatedAttributeInstanceResult( - temporalEntityAttribute = toUuid(row["temporal_entity_attribute"]), + attributeUuid = toUuid(row["temporal_entity_attribute"]), values = values ) } else if (temporalEntitiesQuery.withTemporalValues) SimplifiedAttributeInstanceResult( - temporalEntityAttribute = toUuid(row["temporal_entity_attribute"]), + attributeUuid = toUuid(row["temporal_entity_attribute"]), // the type of the value of a property may have changed in the history (e.g., from number to string) // in this case, just display an empty value (something happened, but we can't display it) value = row["value"] ?: "", time = toZonedDateTime(row["start"]) ) else FullAttributeInstanceResult( - temporalEntityAttribute = toUuid(row["temporal_entity_attribute"]), + attributeUuid = toUuid(row["temporal_entity_attribute"]), payload = toJsonString(row["payload"]), time = toZonedDateTime(row["start"]), timeproperty = temporalEntitiesQuery.temporalQuery.timeproperty.propertyName, @@ -315,15 +320,15 @@ class AttributeInstanceService( instanceId: URI, expandedAttributeInstances: ExpandedAttributeInstances ): Either = either { - val teaUUID = retrieveTeaUUID(entityId, attributeName, instanceId).bind() + val attributeUUID = retrieveAttributeUUID(entityId, attributeName, instanceId).bind() val ngsiLdAttribute = expandedAttributeInstances.toNgsiLdAttribute(attributeName).bind() val ngsiLdAttributeInstance = ngsiLdAttribute.getAttributeInstances()[0] - val attributeMetadata = ngsiLdAttributeInstance.toTemporalAttributeMetadata().bind() + val attributeMetadata = ngsiLdAttributeInstance.toAttributeMetadata().bind() deleteInstance(entityId, attributeName, instanceId).bind() create( AttributeInstance( - temporalEntityAttribute = teaUUID, + attributeUuid = attributeUUID, time = attributeMetadata.observedAt!!, attributeMetadata = attributeMetadata, modifiedAt = ngsiLdDateTime(), @@ -333,7 +338,7 @@ class AttributeInstanceService( ).bind() } - private suspend fun retrieveTeaUUID( + private suspend fun retrieveAttributeUUID( entityId: URI, attributeName: ExpandedTerm, instanceId: URI diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/TemporalPaginationService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/TemporalPaginationService.kt similarity index 70% rename from search-service/src/main/kotlin/com/egm/stellio/search/service/TemporalPaginationService.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/TemporalPaginationService.kt index 772bacd29..cb0d45e15 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/TemporalPaginationService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/TemporalPaginationService.kt @@ -1,41 +1,42 @@ -package com.egm.stellio.search.service +package com.egm.stellio.search.temporal.service -import com.egm.stellio.search.model.AggregatedAttributeInstanceResult -import com.egm.stellio.search.model.AttributeInstanceResult -import com.egm.stellio.search.model.TemporalEntitiesQuery -import com.egm.stellio.search.model.TemporalQuery +import com.egm.stellio.search.temporal.model.AggregatedAttributeInstanceResult +import com.egm.stellio.search.temporal.model.AttributeInstanceResult +import com.egm.stellio.search.temporal.model.TemporalEntitiesQuery +import com.egm.stellio.search.temporal.model.TemporalQuery +import com.egm.stellio.search.temporal.util.AttributesWithInstances import java.time.ZonedDateTime typealias Range = Pair object TemporalPaginationService { - fun getRangeAndPaginatedTEA( - teaWithInstances: TEAWithInstances, + fun getPaginatedAttributeWithInstancesAndRange( + attributesWithInstances: AttributesWithInstances, query: TemporalEntitiesQuery, - ): Pair { + ): Pair { val temporalQuery = query.temporalQuery if (temporalQuery.isLastNTheLimit()) { - return teaWithInstances to null + return attributesWithInstances to null } - val attributeInstancesWhoReachedLimit = getAttributesWhoReachedLimit(teaWithInstances, query) + val attributeInstancesWhoReachedLimit = getAttributesWhoReachedLimit(attributesWithInstances, query) if (attributeInstancesWhoReachedLimit.isEmpty()) { - return teaWithInstances to null + return attributesWithInstances to null } val range = getTemporalPaginationRange(attributeInstancesWhoReachedLimit, query) - val paginatedTEAWithinstances = filterInRange(teaWithInstances, range) + val paginatedAttributesWithInstances = filterInRange(attributesWithInstances, range) - return paginatedTEAWithinstances to range + return paginatedAttributesWithInstances to range } private fun getAttributesWhoReachedLimit( - teaWithInstances: TEAWithInstances, + attributesWithInstances: AttributesWithInstances, query: TemporalEntitiesQuery ): List> = - teaWithInstances.values.filter { instances -> + attributesWithInstances.values.filter { instances -> instances.size >= query.temporalQuery.instanceLimit } @@ -76,10 +77,10 @@ object TemporalPaginationService { } private fun filterInRange( - teaWithInstances: TEAWithInstances, + attributesWithInstances: AttributesWithInstances, range: Range, - ): TEAWithInstances = - teaWithInstances.mapValues { (_, value) -> + ): AttributesWithInstances = + attributesWithInstances.mapValues { (_, value) -> value.filter { range.contain(it.getComparableTime()) } } diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/QueryService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/TemporalQueryService.kt similarity index 54% rename from search-service/src/main/kotlin/com/egm/stellio/search/service/QueryService.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/TemporalQueryService.kt index bf0c20426..cac484390 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/QueryService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/TemporalQueryService.kt @@ -1,64 +1,51 @@ -package com.egm.stellio.search.service +package com.egm.stellio.search.temporal.service -import arrow.core.Either -import arrow.core.getOrElse -import arrow.core.left +import arrow.core.* import arrow.core.raise.either -import arrow.core.right -import com.egm.stellio.search.model.* +import com.egm.stellio.search.authorization.service.AuthorizationService +import com.egm.stellio.search.entity.model.Attribute +import com.egm.stellio.search.entity.service.EntityAttributeService +import com.egm.stellio.search.entity.service.EntityQueryService import com.egm.stellio.search.scope.ScopeService -import com.egm.stellio.search.service.TemporalPaginationService.getRangeAndPaginatedTEA -import com.egm.stellio.search.util.TemporalEntityBuilder -import com.egm.stellio.search.util.deserializeAsMap -import com.egm.stellio.search.web.Range +import com.egm.stellio.search.temporal.model.EntityTemporalResult +import com.egm.stellio.search.temporal.model.SimplifiedAttributeInstanceResult +import com.egm.stellio.search.temporal.model.TemporalEntitiesQuery +import com.egm.stellio.search.temporal.service.TemporalPaginationService.getPaginatedAttributeWithInstancesAndRange +import com.egm.stellio.search.temporal.util.AttributesWithInstances +import com.egm.stellio.search.temporal.util.TemporalEntityBuilder +import com.egm.stellio.search.temporal.web.Range import com.egm.stellio.shared.model.APIException import com.egm.stellio.shared.model.ExpandedEntity import com.egm.stellio.shared.model.ResourceNotFoundException import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_SCOPE_PROPERTY +import com.egm.stellio.shared.util.Sub import com.egm.stellio.shared.util.entityOrAttrsNotFoundMessage import com.egm.stellio.shared.util.wktToGeoJson import org.springframework.stereotype.Service import java.net.URI import java.time.ZonedDateTime - -typealias TEAWithInstances = Map> +import kotlin.collections.flatten @Service -class QueryService( - private val entityPayloadService: EntityPayloadService, +class TemporalQueryService( + private val entityQueryService: EntityQueryService, private val scopeService: ScopeService, private val attributeInstanceService: AttributeInstanceService, - private val temporalEntityAttributeService: TemporalEntityAttributeService + private val entityAttributeService: EntityAttributeService, + private val authorizationService: AuthorizationService ) { - suspend fun queryEntity(entityId: URI): Either = - either { - val entityPayload = entityPayloadService.retrieve(entityId).bind() - toJsonLdEntity(entityPayload) - } - - suspend fun queryEntities( - entitiesQuery: EntitiesQuery, - accessRightFilter: () -> String? - ): Either, Int>> = either { - val entitiesIds = entityPayloadService.queryEntities(entitiesQuery, accessRightFilter) - val count = entityPayloadService.queryEntitiesCount(entitiesQuery, accessRightFilter).bind() - - // we can have an empty list of entities with a non-zero count (e.g., offset too high) - if (entitiesIds.isEmpty()) - return@either Pair, Int>(emptyList(), count) - - val entitiesPayloads = entityPayloadService.retrieve(entitiesIds).map { toJsonLdEntity(it) } - - Pair(entitiesPayloads, count).right().bind() - } suspend fun queryTemporalEntity( entityId: URI, - temporalEntitiesQuery: TemporalEntitiesQuery + temporalEntitiesQuery: TemporalEntitiesQuery, + sub: Sub? = null ): Either> = either { + entityQueryService.checkEntityExistence(entityId).bind() + authorizationService.userCanReadEntity(entityId, sub.toOption()).bind() + val attrs = temporalEntitiesQuery.entitiesQuery.attrs val datasetIds = temporalEntitiesQuery.entitiesQuery.datasetId - val temporalEntityAttributes = temporalEntityAttributeService.getForEntity(entityId, attrs, datasetIds).let { + val attributes = entityAttributeService.getForEntity(entityId, attrs, datasetIds).let { if (it.isEmpty()) ResourceNotFoundException( entityOrAttrsNotFoundMessage(entityId.toString(), temporalEntitiesQuery.entitiesQuery.attrs) @@ -66,27 +53,27 @@ class QueryService( else it.right() }.bind() - val entityPayload = entityPayloadService.retrieve(entityId).bind() - val origin = calculateOldestTimestamp(entityId, temporalEntitiesQuery, temporalEntityAttributes) + val entityPayload = entityQueryService.retrieve(entityId).bind() + val origin = calculateOldestTimestamp(entityId, temporalEntitiesQuery, attributes) val scopeHistory = if (attrs.isEmpty() || attrs.contains(NGSILD_SCOPE_PROPERTY)) scopeService.retrieveHistory(listOf(entityId), temporalEntitiesQuery, origin).bind() else emptyList() - val temporalEntityAttributesWithMatchingInstances = - searchInstancesForTemporalEntityAttributes(temporalEntityAttributes, temporalEntitiesQuery, origin).bind() + val attributesWithMatchingInstances = + searchInstancesForAttributes(attributes, temporalEntitiesQuery, origin).bind() - val (paginatedTEA, range) = getRangeAndPaginatedTEA( - temporalEntityAttributesWithMatchingInstances, + val (paginatedAttributesWithInstances, range) = getPaginatedAttributeWithInstancesAndRange( + attributesWithMatchingInstances, temporalEntitiesQuery ) - val temporalEntityAttributesWithInstances = - fillWithTEAWithEmptyInstances(temporalEntityAttributes, paginatedTEA) + val attributesWithInstances = + fillWithAttributesWithEmptyInstances(attributes, paginatedAttributesWithInstances) TemporalEntityBuilder.buildTemporalEntity( - EntityTemporalResult(entityPayload, scopeHistory, temporalEntityAttributesWithInstances), + EntityTemporalResult(entityPayload, scopeHistory, attributesWithInstances), temporalEntitiesQuery ) to range } @@ -94,7 +81,7 @@ class QueryService( internal suspend fun calculateOldestTimestamp( entityId: URI, temporalEntitiesQuery: TemporalEntitiesQuery, - temporalEntityAttributes: List + attributes: List ): ZonedDateTime? { val temporalQuery = temporalEntitiesQuery.temporalQuery @@ -109,8 +96,8 @@ class QueryService( else if (temporalQuery.timeAt != null) return temporalQuery.timeAt else { - val originForTemporalEntityAttributes = - attributeInstanceService.selectOldestDate(temporalQuery, temporalEntityAttributes) + val originForAttributes = + attributeInstanceService.selectOldestDate(temporalQuery, attributes) val attrs = temporalEntitiesQuery.entitiesQuery.attrs val originForScope = @@ -119,27 +106,28 @@ class QueryService( else null return when { - originForTemporalEntityAttributes == null -> originForScope - originForScope == null -> originForTemporalEntityAttributes - else -> minOf(originForTemporalEntityAttributes, originForScope) + originForAttributes == null -> originForScope + originForScope == null -> originForAttributes + else -> minOf(originForAttributes, originForScope) } } } suspend fun queryTemporalEntities( temporalEntitiesQuery: TemporalEntitiesQuery, - accessRightFilter: () -> String? + sub: Sub? = null ): Either, Int, Range?>> = either { + val accessRightFilter = authorizationService.computeAccessRightFilter(sub.toOption()) val attrs = temporalEntitiesQuery.entitiesQuery.attrs - val entitiesIds = entityPayloadService.queryEntities(temporalEntitiesQuery.entitiesQuery, accessRightFilter) - val count = entityPayloadService.queryEntitiesCount(temporalEntitiesQuery.entitiesQuery, accessRightFilter) + val entitiesIds = entityQueryService.queryEntities(temporalEntitiesQuery.entitiesQuery, accessRightFilter) + val count = entityQueryService.queryEntitiesCount(temporalEntitiesQuery.entitiesQuery, accessRightFilter) .getOrElse { 0 } // we can have an empty list of entities with a non-zero count (e.g., offset too high) if (entitiesIds.isEmpty()) return@either Triple, Int, Range?>(emptyList(), count, null) - val temporalEntityAttributes = temporalEntityAttributeService.getForTemporalEntities( + val attributes = entityAttributeService.getForEntities( entitiesIds, temporalEntitiesQuery.entitiesQuery ) @@ -149,31 +137,31 @@ class QueryService( scopeService.retrieveHistory(entitiesIds, temporalEntitiesQuery).bind().groupBy { it.entityId } else emptyMap() - val temporalEntityAttributesWithMatchingInstances = - searchInstancesForTemporalEntityAttributes(temporalEntityAttributes, temporalEntitiesQuery).bind() + val attributesWithMatchingInstances = + searchInstancesForAttributes(attributes, temporalEntitiesQuery).bind() - val (paginatedTEA, range) = getRangeAndPaginatedTEA( - temporalEntityAttributesWithMatchingInstances, + val (paginatedAttributesWithInstances, range) = getPaginatedAttributeWithInstancesAndRange( + attributesWithMatchingInstances, temporalEntitiesQuery ) - val temporalEntityAttributesWithInstances = - fillWithTEAWithEmptyInstances(temporalEntityAttributes, paginatedTEA) + val attributesWithInstances = + fillWithAttributesWithEmptyInstances(attributes, paginatedAttributesWithInstances) val attributeInstancesPerEntityAndAttribute = - temporalEntityAttributesWithInstances + attributesWithInstances .toList() .groupBy { // then, group them by entity it.first.entityId }.mapKeys { - entityPayloadService.retrieve(it.key).bind() + entityQueryService.retrieve(it.key).bind() } .mapValues { it.value.toMap() } .toList() - // the ordering made when searching matching temporal entity attributes is lost - // since we are now iterating over the map of TEAs with their instances + // the ordering made when searching matching attributes is lost + // since we are now iterating over the map of attributes with their instances .sortedBy { it.first.entityId } .map { EntityTemporalResult(it.first, scopesHistory[it.first.entityId] ?: emptyList(), it.second) @@ -189,14 +177,14 @@ class QueryService( ) } - private suspend fun searchInstancesForTemporalEntityAttributes( - temporalEntityAttributes: List, + private suspend fun searchInstancesForAttributes( + attributes: List, temporalEntitiesQuery: TemporalEntitiesQuery, origin: ZonedDateTime? = null - ): Either = either { + ): Either = either { // split the group according to attribute type as this currently triggers 2 different queries // then do one search for each type of attribute (fewer queries for improved performance) - temporalEntityAttributes + attributes .groupBy { it.attributeValueType }.mapValues { @@ -205,7 +193,7 @@ class QueryService( .mapValues { // when retrieved from DB, values of geo-properties are encoded in WKT and won't be automatically // transformed during compaction as it is not done for temporal values, so it is done now - if (it.key == TemporalEntityAttribute.AttributeValueType.GEOMETRY && + if (it.key == Attribute.AttributeValueType.GEOMETRY && temporalEntitiesQuery.withTemporalValues ) { it.value.map { attributeInstanceResult -> @@ -220,30 +208,25 @@ class QueryService( .flatten() .groupBy { attributeInstanceResult -> // group them by temporal entity attribute - temporalEntityAttributes.find { tea -> - tea.id == attributeInstanceResult.temporalEntityAttribute + attributes.find { attribute -> + attribute.id == attributeInstanceResult.attributeUuid }!! } .mapValues { it.value.sorted() } } - private fun fillWithTEAWithEmptyInstances( - temporalEntityAttributes: List, - temporalEntityAttributesWithInstances: TEAWithInstances - ): TEAWithInstances { + private fun fillWithAttributesWithEmptyInstances( + attributes: List, + attributesWithInstances: AttributesWithInstances + ): AttributesWithInstances { // filter the temporal entity attributes for which there are no attribute instances - val temporalEntityAttributesWithoutInstances = - temporalEntityAttributes.filter { - !temporalEntityAttributesWithInstances.keys.contains(it) + val attributesWithoutInstances = + attributes.filter { + !attributesWithInstances.keys.contains(it) } // add them in the result set accompanied by an empty list - return temporalEntityAttributesWithInstances.plus( - temporalEntityAttributesWithoutInstances.map { it to emptyList() } + return attributesWithInstances.plus( + attributesWithoutInstances.map { it to emptyList() } ) } - - private fun toJsonLdEntity(entityPayload: EntityPayload): ExpandedEntity { - val deserializedEntity = entityPayload.payload.deserializeAsMap() - return ExpandedEntity(deserializedEntity) - } } diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/TemporalService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/TemporalService.kt new file mode 100644 index 000000000..59e32e776 --- /dev/null +++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/TemporalService.kt @@ -0,0 +1,148 @@ +package com.egm.stellio.search.temporal.service + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.toOption +import com.egm.stellio.search.authorization.service.AuthorizationService +import com.egm.stellio.search.entity.service.EntityQueryService +import com.egm.stellio.search.entity.service.EntityService +import com.egm.stellio.shared.model.* +import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_OBSERVED_AT_PROPERTY +import com.egm.stellio.shared.util.Sub +import org.springframework.stereotype.Service +import java.net.URI + +@Service +class TemporalService( + private val entityService: EntityService, + private val entityQueryService: EntityQueryService, + private val attributeInstanceService: AttributeInstanceService, + private val authorizationService: AuthorizationService, +) { + + enum class CreateOrUpdateResult { CREATED, UPSERTED } + + suspend fun createOrUpdateTemporalEntity( + entityId: URI, + jsonLdTemporalEntity: ExpandedEntity, + sub: Sub? = null + ): Either = either { + val entityDoesNotExist = entityQueryService.checkEntityExistence(entityId, true).isRight() + + if (entityDoesNotExist) { + createTemporalEntity( + entityId, + jsonLdTemporalEntity, + jsonLdTemporalEntity.getAttributes().sorted(), + sub + ).bind() + + CreateOrUpdateResult.CREATED + } else { + upsertTemporalEntity( + entityId, + jsonLdTemporalEntity.getAttributes().sorted(), + sub + ).bind() + + CreateOrUpdateResult.UPSERTED + } + } + + internal suspend fun createTemporalEntity( + entityId: URI, + jsonLdTemporalEntity: ExpandedEntity, + sortedJsonLdInstances: ExpandedAttributes, + sub: Sub? = null + ): Either = either { + authorizationService.userCanCreateEntities(sub.toOption()).bind() + + // create a view of the entity containing only the most recent instance of each attribute + val expandedEntity = ExpandedEntity( + sortedJsonLdInstances + .keepFirstInstances() + .addCoreMembers(jsonLdTemporalEntity.id, jsonLdTemporalEntity.types) + ) + val ngsiLdEntity = expandedEntity.toNgsiLdEntity().bind() + + entityService.createEntity(ngsiLdEntity, expandedEntity, sub).bind() + entityService.upsertAttributes( + entityId, + sortedJsonLdInstances.removeFirstInstances(), + sub + ).bind() + authorizationService.createOwnerRight(entityId, sub.toOption()).bind() + } + + internal suspend fun upsertTemporalEntity( + entityId: URI, + sortedJsonLdInstances: ExpandedAttributes, + sub: Sub? = null + ): Either = either { + authorizationService.userCanUpdateEntity(entityId, sub.toOption()).bind() + entityService.upsertAttributes( + entityId, + sortedJsonLdInstances, + sub + ).bind() + } + + private fun ExpandedAttributes.keepFirstInstances(): ExpandedAttributes = + this.mapValues { listOf(it.value.first()) } + + private fun ExpandedAttributes.removeFirstInstances(): ExpandedAttributes = + this.mapValues { + it.value.drop(1) + } + + private fun ExpandedAttributes.sorted(): ExpandedAttributes = + this.mapValues { + it.value.sortedByDescending { expandedAttributePayloadEntry -> + expandedAttributePayloadEntry.getMemberValueAsDateTime(NGSILD_OBSERVED_AT_PROPERTY) + } + } + + suspend fun upsertAttributes( + entityId: URI, + jsonLdInstances: ExpandedAttributes, + sub: Sub? = null + ): Either = either { + entityQueryService.checkEntityExistence(entityId).bind() + authorizationService.userCanUpdateEntity(entityId, sub.toOption()).bind() + + entityService.upsertAttributes( + entityId, + jsonLdInstances.sorted(), + sub + ).bind() + } + + suspend fun modifyAttributeInstance( + entityId: URI, + instanceId: URI, + expandedAttribute: ExpandedAttribute, + sub: Sub? = null + ): Either = either { + entityQueryService.checkEntityExistence(entityId).bind() + authorizationService.userCanUpdateEntity(entityId, sub.toOption()).bind() + + attributeInstanceService.modifyAttributeInstance( + entityId, + expandedAttribute.first, + instanceId, + expandedAttribute.second + ).bind() + } + + suspend fun deleteAttributeInstance( + entityId: URI, + attributeName: ExpandedTerm, + instanceId: URI, + sub: Sub? = null + ): Either = either { + entityQueryService.checkEntityExistence(entityId).bind() + authorizationService.userCanUpdateEntity(entityId, sub.toOption()).bind() + + attributeInstanceService.deleteInstance(entityId, attributeName, instanceId).bind() + } +} diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/util/DBAggregationUtils.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/util/DBAggregationUtils.kt similarity index 96% rename from search-service/src/main/kotlin/com/egm/stellio/search/util/DBAggregationUtils.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/temporal/util/DBAggregationUtils.kt index a3bc977da..f8431ec66 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/util/DBAggregationUtils.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/util/DBAggregationUtils.kt @@ -1,7 +1,7 @@ -package com.egm.stellio.search.util +package com.egm.stellio.search.temporal.util -import com.egm.stellio.search.model.TemporalEntityAttribute.AttributeValueType -import com.egm.stellio.search.model.TemporalQuery +import com.egm.stellio.search.entity.model.Attribute.AttributeValueType +import com.egm.stellio.search.temporal.model.TemporalQuery fun aggrMethodToSqlAggregate( aggregate: TemporalQuery.Aggregate, diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/util/TemporalEntityBuilder.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/util/TemporalEntityBuilder.kt similarity index 88% rename from search-service/src/main/kotlin/com/egm/stellio/search/util/TemporalEntityBuilder.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/temporal/util/TemporalEntityBuilder.kt index e396c052d..a13ec13d0 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/util/TemporalEntityBuilder.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/util/TemporalEntityBuilder.kt @@ -1,7 +1,8 @@ -package com.egm.stellio.search.util +package com.egm.stellio.search.temporal.util -import com.egm.stellio.search.model.* +import com.egm.stellio.search.entity.model.Attribute import com.egm.stellio.search.scope.TemporalScopeBuilder +import com.egm.stellio.search.temporal.model.* import com.egm.stellio.shared.model.ExpandedEntity import com.egm.stellio.shared.model.ExpandedTerm import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_SUB @@ -24,7 +25,7 @@ import com.egm.stellio.shared.util.JsonUtils.deserializeObject import com.egm.stellio.shared.util.wktToGeoJson typealias SimplifiedTemporalAttribute = Map -typealias TemporalEntityAttributeInstancesResult = Map> +typealias AttributesWithInstances = Map> object TemporalEntityBuilder { @@ -41,24 +42,24 @@ object TemporalEntityBuilder { temporalEntitiesQuery: TemporalEntitiesQuery ): ExpandedEntity { val temporalAttributes = buildTemporalAttributes( - entityTemporalResult.teaInstancesResult, + entityTemporalResult.attributesWithInstances, temporalEntitiesQuery ) val scopeAttributeInstances = TemporalScopeBuilder.buildScopeAttributeInstances( - entityTemporalResult.entityPayload, + entityTemporalResult.entity, entityTemporalResult.scopeHistory, temporalEntitiesQuery ) - val expandedTemporalEntity = entityTemporalResult.entityPayload.serializeProperties() + val expandedTemporalEntity = entityTemporalResult.entity.serializeProperties() .plus(temporalAttributes) .plus(scopeAttributeInstances) return ExpandedEntity(expandedTemporalEntity) } private fun buildTemporalAttributes( - attributeAndResultsMap: TemporalEntityAttributeInstancesResult, + attributeAndResultsMap: AttributesWithInstances, temporalEntitiesQuery: TemporalEntitiesQuery, ): Map = if (temporalEntitiesQuery.withTemporalValues) { @@ -112,8 +113,8 @@ object TemporalEntityBuilder { * of the temporal entity attribute. */ private fun buildAttributesSimplifiedRepresentation( - attributeAndResultsMap: TemporalEntityAttributeInstancesResult - ): Map = + attributeAndResultsMap: AttributesWithInstances + ): Map = attributeAndResultsMap.mapValues { val attributeInstance = mutableMapOf( JSONLD_TYPE to listOf(it.key.attributeType.toExpandedName()) @@ -121,12 +122,12 @@ object TemporalEntityBuilder { it.key.datasetId?.let { datasetId -> attributeInstance[NGSILD_DATASET_ID_PROPERTY] = buildNonReifiedPropertyValue(datasetId.toString()) } - val valuesKey = it.key.attributeType.toSimpliedRepresentationKey() + val valuesKey = it.key.attributeType.toSimplifiedRepresentationKey() attributeInstance[valuesKey] = buildExpandedTemporalValue(it.value) { attributeInstanceResult -> attributeInstanceResult as SimplifiedAttributeInstanceResult when (it.key.attributeType) { - TemporalEntityAttribute.AttributeType.JsonProperty -> { + Attribute.AttributeType.JsonProperty -> { // flaky way to know if the serialized value is a JSON object or an array of JSON objects val deserializedJsonValue: Any = if ((attributeInstanceResult.value as String).startsWith("[")) @@ -144,7 +145,7 @@ object TemporalEntityBuilder { mapOf(JSONLD_VALUE to attributeInstanceResult.time) ) } - TemporalEntityAttribute.AttributeType.LanguageProperty -> { + Attribute.AttributeType.LanguageProperty -> { listOf( mapOf( NGSILD_LANGUAGEPROPERTY_VALUE to @@ -153,7 +154,7 @@ object TemporalEntityBuilder { mapOf(JSONLD_VALUE to attributeInstanceResult.time) ) } - TemporalEntityAttribute.AttributeType.VocabProperty -> { + Attribute.AttributeType.VocabProperty -> { listOf( mapOf( NGSILD_VOCABPROPERTY_VALUE to @@ -182,9 +183,9 @@ object TemporalEntityBuilder { * as described in 4.5.19.0 */ private fun buildAttributesAggregatedRepresentation( - attributeAndResultsMap: TemporalEntityAttributeInstancesResult, + attributeAndResultsMap: AttributesWithInstances, aggrMethods: List - ): Map { + ): Map { return attributeAndResultsMap.mapValues { val attributeInstance = mutableMapOf( JSONLD_TYPE to listOf(it.key.attributeType.toExpandedName()) @@ -193,7 +194,7 @@ object TemporalEntityBuilder { attributeInstance[NGSILD_DATASET_ID_PROPERTY] = buildNonReifiedPropertyValue(datasetId.toString()) } - val aggregatedResultsForTEA = it.value + val aggregatedResultsForAttributes = it.value .map { attributeInstanceResult -> attributeInstanceResult as AggregatedAttributeInstanceResult attributeInstanceResult.values @@ -201,7 +202,7 @@ object TemporalEntityBuilder { .flatten() aggrMethods.forEach { aggregate -> - val resultsForAggregate = aggregatedResultsForTEA.filter { aggregateResult -> + val resultsForAggregate = aggregatedResultsForAttributes.filter { aggregateResult -> aggregateResult.aggregate.method == aggregate.method } attributeInstance[NGSILD_PREFIX + aggregate.method] = @@ -224,11 +225,11 @@ object TemporalEntityBuilder { * - Value: list of the full representation of the attribute instances */ private fun mergeFullTemporalAttributesOnAttributeName( - attributeAndResultsMap: TemporalEntityAttributeInstancesResult + attributeAndResultsMap: AttributesWithInstances ): Map> = attributeAndResultsMap.toList() - .groupBy { (temporalEntityAttribute, _) -> - temporalEntityAttribute.attributeName + .groupBy { (attribute, _) -> + attribute.attributeName } .toMap() .mapValues { @@ -243,11 +244,11 @@ object TemporalEntityBuilder { * - Value: list of the simplified temporal representation of the attribute instances */ private fun mergeSimplifiedTemporalAttributesOnAttributeName( - attributeAndResultsMap: Map + attributeAndResultsMap: Map ): Map> = attributeAndResultsMap.toList() - .groupBy { (temporalEntityAttribute, _) -> - temporalEntityAttribute.attributeName + .groupBy { (attribute, _) -> + attribute.attributeName } .toMap() .mapValues { diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/util/TemporalQueryParamsUtils.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/util/TemporalQueryParamsUtils.kt similarity index 88% rename from search-service/src/main/kotlin/com/egm/stellio/search/util/TemporalQueryParamsUtils.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/temporal/util/TemporalQueryParamsUtils.kt index ffb995ef4..26f2e461e 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/util/TemporalQueryParamsUtils.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/util/TemporalQueryParamsUtils.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.util +package com.egm.stellio.search.temporal.util const val TIMEREL_PARAM = "timerel" const val TIMEAT_PARAM = "timeAt" diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/util/EntitiesQueryUtils.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/util/TemporalQueryUtils.kt similarity index 65% rename from search-service/src/main/kotlin/com/egm/stellio/search/util/EntitiesQueryUtils.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/temporal/util/TemporalQueryUtils.kt index 4c1790d83..724a72820 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/util/EntitiesQueryUtils.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/util/TemporalQueryUtils.kt @@ -1,112 +1,27 @@ -package com.egm.stellio.search.util +package com.egm.stellio.search.temporal.util import arrow.core.* import arrow.core.raise.either -import com.egm.stellio.search.model.* -import com.egm.stellio.search.model.TemporalQuery.Aggregate -import com.egm.stellio.search.model.TemporalQuery.Timerel +import com.egm.stellio.search.common.model.Query +import com.egm.stellio.search.entity.util.composeEntitiesQuery +import com.egm.stellio.search.entity.util.composeEntitiesQueryFromPostRequest +import com.egm.stellio.search.entity.util.validateMinimalQueryEntitiesParameters +import com.egm.stellio.search.temporal.model.AttributeInstance +import com.egm.stellio.search.temporal.model.TemporalEntitiesQuery +import com.egm.stellio.search.temporal.model.TemporalQuery +import com.egm.stellio.search.temporal.model.TemporalQuery.Aggregate +import com.egm.stellio.search.temporal.model.TemporalQuery.Timerel 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.util.* +import com.egm.stellio.shared.util.OptionsParamValue +import com.egm.stellio.shared.util.QUERY_PARAM_OPTIONS +import com.egm.stellio.shared.util.hasValueInOptionsParam +import com.egm.stellio.shared.util.parseTimeParameter import org.springframework.util.MultiValueMap import org.springframework.util.MultiValueMapAdapter import java.time.ZonedDateTime -import java.util.Optional - -fun composeEntitiesQuery( - defaultPagination: ApplicationProperties.Pagination, - requestParams: MultiValueMap, - contexts: List -): Either = either { - val ids = requestParams.getFirst(QUERY_PARAM_ID)?.split(",").orEmpty().toListOfUri().toSet() - val typeSelection = expandTypeSelection(requestParams.getFirst(QUERY_PARAM_TYPE), contexts) - val idPattern = validateIdPattern(requestParams.getFirst(QUERY_PARAM_ID_PATTERN)).bind() - - /** - * Decoding query parameters is not supported by default so a call to a decode function was added query - * with the right parameters values - */ - val q = requestParams.getFirst(QUERY_PARAM_Q)?.decode() - val scopeQ = requestParams.getFirst(QUERY_PARAM_SCOPEQ) - val attrs = parseAndExpandRequestParameter(requestParams.getFirst(QUERY_PARAM_ATTRS), contexts) - val datasetId = parseRequestParameter(requestParams.getFirst(QUERY_PARAM_DATASET_ID)) - val paginationQuery = parsePaginationParameters( - requestParams, - defaultPagination.limitDefault, - defaultPagination.limitMax - ).bind() - - val geoQuery = parseGeoQueryParameters(requestParams.toSingleValueMap(), contexts).bind() - - EntitiesQuery( - ids = ids, - typeSelection = typeSelection, - idPattern = idPattern, - q = q, - scopeQ = scopeQ, - paginationQuery = paginationQuery, - attrs = attrs, - datasetId = datasetId, - geoQuery = geoQuery, - contexts = contexts - ) -} - -fun EntitiesQuery.validateMinimalQueryEntitiesParameters(): Either = either { - if ( - geoQuery == null && - q.isNullOrEmpty() && - typeSelection.isNullOrEmpty() && - attrs.isEmpty() - ) - return@either BadRequestDataException( - "One of 'type', 'attrs', 'q', 'geoQ' must be provided in the query" - ).left().bind() - - this@validateMinimalQueryEntitiesParameters -} - -fun composeEntitiesQueryFromPostRequest( - defaultPagination: ApplicationProperties.Pagination, - query: Query, - requestParams: MultiValueMap, - contexts: List -): Either = either { - val entitySelector = query.entities?.get(0) - val typeSelection = expandTypeSelection(entitySelector?.typeSelection, contexts) - val idPattern = validateIdPattern(entitySelector?.idPattern).bind() - val attrs = query.attrs.orEmpty().map { JsonLdUtils.expandJsonLdTerm(it.trim(), contexts) }.toSet() - val datasetId = query.datasetId.orEmpty().toSet() - val geoQuery = if (query.geoQ != null) { - val geoQueryElements = mapOf( - "geometry" to query.geoQ.geometry, - "coordinates" to query.geoQ.coordinates.toString(), - "georel" to query.geoQ.georel, - "geoproperty" to query.geoQ.geoproperty - ) - parseGeoQueryParameters(geoQueryElements, contexts).bind() - } else null - - val paginationQuery = parsePaginationParameters( - requestParams, - defaultPagination.limitDefault, - defaultPagination.limitMax - ).bind() - - EntitiesQuery( - ids = setOfNotNull(entitySelector?.id), - typeSelection = typeSelection, - idPattern = idPattern, - q = query.q?.decode(), - scopeQ = query.scopeQ, - paginationQuery = paginationQuery, - attrs = attrs, - datasetId = datasetId, - geoQuery = geoQuery, - contexts = contexts - ) -} +import java.util.* fun composeTemporalEntitiesQuery( defaultPagination: ApplicationProperties.Pagination, diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalApiResponses.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalApiResponses.kt similarity index 94% rename from search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalApiResponses.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalApiResponses.kt index aa8646358..4fc12e749 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalApiResponses.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalApiResponses.kt @@ -1,7 +1,7 @@ -package com.egm.stellio.search.web +package com.egm.stellio.search.temporal.web -import com.egm.stellio.search.model.TemporalEntitiesQuery -import com.egm.stellio.search.model.TemporalQuery +import com.egm.stellio.search.temporal.model.TemporalEntitiesQuery +import com.egm.stellio.search.temporal.model.TemporalQuery import com.egm.stellio.shared.model.CompactedEntity import com.egm.stellio.shared.model.toFinalRepresentation import com.egm.stellio.shared.util.JsonUtils.serializeObject diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityHandler.kt similarity index 69% rename from search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityHandler.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityHandler.kt index 285d6f1d0..ad36e9611 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityHandler.kt @@ -1,17 +1,15 @@ -package com.egm.stellio.search.web +package com.egm.stellio.search.temporal.web import arrow.core.Either import arrow.core.left import arrow.core.raise.either import arrow.core.right -import com.egm.stellio.search.authorization.AuthorizationService -import com.egm.stellio.search.service.AttributeInstanceService -import com.egm.stellio.search.service.EntityPayloadService -import com.egm.stellio.search.service.QueryService -import com.egm.stellio.search.service.TemporalEntityAttributeService -import com.egm.stellio.search.util.composeTemporalEntitiesQuery -import com.egm.stellio.search.web.TemporalApiResponses.buildEntitiesTemporalResponse -import com.egm.stellio.search.web.TemporalApiResponses.buildEntityTemporalResponse +import com.egm.stellio.search.entity.service.EntityService +import com.egm.stellio.search.temporal.service.TemporalQueryService +import com.egm.stellio.search.temporal.service.TemporalService +import com.egm.stellio.search.temporal.util.composeTemporalEntitiesQuery +import com.egm.stellio.search.temporal.web.TemporalApiResponses.buildEntitiesTemporalResponse +import com.egm.stellio.search.temporal.web.TemporalApiResponses.buildEntityTemporalResponse import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.model.* import com.egm.stellio.shared.util.* @@ -36,11 +34,9 @@ import java.net.URI @RestController @RequestMapping("/ngsi-ld/v1/temporal/entities") class TemporalEntityHandler( - private val entityPayloadService: EntityPayloadService, - private val attributeInstanceService: AttributeInstanceService, - private val temporalEntityAttributeService: TemporalEntityAttributeService, - private val queryService: QueryService, - private val authorizationService: AuthorizationService, + private val temporalService: TemporalService, + private val temporalQueryService: TemporalQueryService, + private val entityService: EntityService, private val applicationProperties: ApplicationProperties ) : BaseHandler() { @@ -58,44 +54,22 @@ class TemporalEntityHandler( val jsonLdTemporalEntity = expandJsonLdEntity(body, contexts) val entityUri = jsonLdTemporalEntity.id.toUri() - val entityDoesNotExist = entityPayloadService.checkEntityExistence(entityUri, true).isRight() val jsonLdInstances = jsonLdTemporalEntity.getAttributes() jsonLdInstances.checkTemporalAttributeInstance().bind() - val sortedJsonLdInstances = jsonLdInstances.sorted() - - if (entityDoesNotExist) { - authorizationService.userCanCreateEntities(sub).bind() - - // create a view of the entity containing only the most recent instance of each attribute - val expandedEntity = ExpandedEntity( - sortedJsonLdInstances - .keepFirstInstances() - .addCoreMembers(jsonLdTemporalEntity.id, jsonLdTemporalEntity.types) - ) - val ngsiLdEntity = expandedEntity.toNgsiLdEntity().bind() - - entityPayloadService.createEntity(ngsiLdEntity, expandedEntity, sub.getOrNull()).bind() - entityPayloadService.upsertAttributes( - entityUri, - sortedJsonLdInstances.removeFirstInstances(), - sub.getOrNull() - ).bind() - authorizationService.createOwnerRight(entityUri, sub).bind() + val result = temporalService.createOrUpdateTemporalEntity( + entityUri, + jsonLdTemporalEntity, + sub.getOrNull() + ).bind() + + if (result == TemporalService.CreateOrUpdateResult.CREATED) ResponseEntity.status(HttpStatus.CREATED) .location(URI("/ngsi-ld/v1/temporal/entities/$entityUri")) .build() - } else { - authorizationService.userCanUpdateEntity(entityUri, sub).bind() - entityPayloadService.upsertAttributes( - entityUri, - sortedJsonLdInstances, - sub.getOrNull() - ).bind() - + else ResponseEntity.status(HttpStatus.NO_CONTENT).build() - } }.fold( { it.toErrorResponse() }, { it } @@ -112,20 +86,12 @@ class TemporalEntityHandler( ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() - entityPayloadService.checkEntityExistence(entityId).bind() - authorizationService.userCanUpdateEntity(entityId, sub).bind() - val (body, contexts) = extractPayloadAndContexts(requestBody, httpHeaders, applicationProperties.contexts.core).bind() val jsonLdInstances = expandAttributes(body, contexts) jsonLdInstances.checkTemporalAttributeInstance().bind() - val sortedJsonLdInstances = jsonLdInstances.sorted() - entityPayloadService.upsertAttributes( - entityId, - sortedJsonLdInstances, - sub.getOrNull() - ).bind() + temporalService.upsertAttributes(entityId, jsonLdInstances, sub.getOrNull()).bind() ResponseEntity.status(HttpStatus.NO_CONTENT).build() }.fold( @@ -152,11 +118,9 @@ class TemporalEntityHandler( val temporalEntitiesQuery = composeTemporalEntitiesQuery(applicationProperties.pagination, params, contexts, true).bind() - val accessRightFilter = authorizationService.computeAccessRightFilter(sub) - - val (temporalEntities, total, range) = queryService.queryTemporalEntities( + val (temporalEntities, total, range) = temporalQueryService.queryTemporalEntities( temporalEntitiesQuery, - accessRightFilter + sub.getOrNull() ).bind() val compactedEntities = compactEntities(temporalEntities, contexts) @@ -186,18 +150,17 @@ class TemporalEntityHandler( @RequestParam params: MultiValueMap ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() - - entityPayloadService.checkEntityExistence(entityId).bind() - val contexts = getContextFromLinkHeaderOrDefault(httpHeaders, applicationProperties.contexts.core).bind() val mediaType = getApplicableMediaType(httpHeaders).bind() - authorizationService.userCanReadEntity(entityId, sub).bind() - val temporalEntitiesQuery = composeTemporalEntitiesQuery(applicationProperties.pagination, params, contexts).bind() - val (temporalEntity, range) = queryService.queryTemporalEntity(entityId, temporalEntitiesQuery).bind() + val (temporalEntity, range) = temporalQueryService.queryTemporalEntity( + entityId, + temporalEntitiesQuery, + sub.getOrNull() + ).bind() val compactedEntity = compactEntity(temporalEntity, contexts) @@ -221,26 +184,22 @@ class TemporalEntityHandler( @RequestHeader httpHeaders: HttpHeaders, @PathVariable entityId: URI, @PathVariable attrId: String, - @PathVariable instanceId: String, + @PathVariable instanceId: URI, @RequestBody requestBody: Mono ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() val (body, contexts) = extractPayloadAndContexts(requestBody, httpHeaders, applicationProperties.contexts.core).bind() - val instanceUri = instanceId.toUri() attrId.checkNameIsNgsiLdSupported().bind() - entityPayloadService.checkEntityExistence(entityId).bind() - authorizationService.userCanUpdateEntity(entityId, sub).bind() - val expandedAttribute = expandAttribute(attrId, body, contexts) expandedAttribute.toExpandedAttributes().checkTemporalAttributeInstance().bind() - attributeInstanceService.modifyAttributeInstance( + temporalService.modifyAttributeInstance( entityId, - expandedAttribute.first, - instanceUri, - expandedAttribute.second + instanceId, + expandedAttribute, + sub.getOrNull() ).bind() ResponseEntity.status(HttpStatus.NO_CONTENT).build() @@ -267,10 +226,7 @@ class TemporalEntityHandler( ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() - entityPayloadService.checkEntityExistence(entityId).bind() - authorizationService.userCanAdminEntity(entityId, sub).bind() - entityPayloadService.deleteEntity(entityId).bind() - authorizationService.removeRightsOnEntity(entityId).bind() + entityService.deleteEntity(entityId, sub.getOrNull()).bind() ResponseEntity.status(HttpStatus.NO_CONTENT).build() }.fold( @@ -296,19 +252,12 @@ class TemporalEntityHandler( attrId.checkNameIsNgsiLdSupported().bind() val expandedAttrId = expandJsonLdTerm(attrId, contexts) - temporalEntityAttributeService.checkEntityAndAttributeExistence( - entityId, - expandedAttrId, - datasetId - ).bind() - - authorizationService.userCanUpdateEntity(entityId, sub).bind() - - entityPayloadService.deleteAttribute( + entityService.deleteAttribute( entityId, expandedAttrId, datasetId, - deleteAll + deleteAll, + sub.getOrNull() ).bind() ResponseEntity.status(HttpStatus.NO_CONTENT).build() @@ -336,11 +285,7 @@ class TemporalEntityHandler( attrId.checkNameIsNgsiLdSupported().bind() val expandedAttrId = expandJsonLdTerm(attrId, contexts) - entityPayloadService.checkEntityExistence(entityId).bind() - - authorizationService.userCanUpdateEntity(entityId, sub).bind() - - attributeInstanceService.deleteInstance(entityId, expandedAttrId, instanceId).bind() + temporalService.deleteAttributeInstance(entityId, expandedAttrId, instanceId, sub.getOrNull()).bind() ResponseEntity.status(HttpStatus.NO_CONTENT).build() }.fold( @@ -363,19 +308,4 @@ class TemporalEntityHandler( if (it) Unit.right() else BadRequestDataException(invalidTemporalInstanceMessage()).left() } - - private fun ExpandedAttributes.sorted(): ExpandedAttributes = - this.mapValues { - it.value.sortedByDescending { expandedAttributePayloadEntry -> - expandedAttributePayloadEntry.getMemberValueAsDateTime(NGSILD_OBSERVED_AT_PROPERTY) - } - } - - private fun ExpandedAttributes.keepFirstInstances(): ExpandedAttributes = - this.mapValues { listOf(it.value.first()) } - - private fun ExpandedAttributes.removeFirstInstances(): ExpandedAttributes = - this.mapValues { - it.value.drop(1) - } } diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityOperationsHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityOperationsHandler.kt similarity index 76% rename from search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityOperationsHandler.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityOperationsHandler.kt index d7a105d11..53cdff298 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityOperationsHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityOperationsHandler.kt @@ -1,11 +1,10 @@ -package com.egm.stellio.search.web +package com.egm.stellio.search.temporal.web import arrow.core.raise.either -import com.egm.stellio.search.authorization.AuthorizationService -import com.egm.stellio.search.model.Query -import com.egm.stellio.search.service.QueryService -import com.egm.stellio.search.util.composeTemporalEntitiesQueryFromPostRequest -import com.egm.stellio.search.web.TemporalApiResponses.buildEntitiesTemporalResponse +import com.egm.stellio.search.common.model.Query +import com.egm.stellio.search.temporal.service.TemporalQueryService +import com.egm.stellio.search.temporal.util.composeTemporalEntitiesQueryFromPostRequest +import com.egm.stellio.search.temporal.web.TemporalApiResponses.buildEntitiesTemporalResponse import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.util.* import com.egm.stellio.shared.util.JsonLdUtils.compactEntities @@ -20,8 +19,7 @@ import reactor.core.publisher.Mono @RestController @RequestMapping("/ngsi-ld/v1/temporal/entityOperations") class TemporalEntityOperationsHandler( - private val queryService: QueryService, - private val authorizationService: AuthorizationService, + private val temporalQueryService: TemporalQueryService, private val applicationProperties: ApplicationProperties ) { @@ -47,11 +45,9 @@ class TemporalEntityOperationsHandler( contexts ).bind() - val accessRightFilter = authorizationService.computeAccessRightFilter(sub) - - val (temporalEntities, total, range) = queryService.queryTemporalEntities( + val (temporalEntities, total, range) = temporalQueryService.queryTemporalEntities( temporalEntitiesQuery, - accessRightFilter + sub.getOrNull() ).bind() val compactedEntities = compactEntities(temporalEntities, contexts) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/util/AttributeInstanceUtils.kt b/search-service/src/main/kotlin/com/egm/stellio/search/util/AttributeInstanceUtils.kt deleted file mode 100644 index c26cea08b..000000000 --- a/search-service/src/main/kotlin/com/egm/stellio/search/util/AttributeInstanceUtils.kt +++ /dev/null @@ -1,192 +0,0 @@ -package com.egm.stellio.search.util - -import arrow.core.Either -import arrow.core.left -import arrow.core.raise.either -import arrow.core.right -import com.egm.stellio.search.model.AttributeMetadata -import com.egm.stellio.search.model.TemporalEntityAttribute -import com.egm.stellio.search.model.TemporalEntityAttribute.AttributeValueType -import com.egm.stellio.shared.model.* -import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_LANGUAGE -import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_JSONPROPERTY_VALUE -import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_LANGUAGEPROPERTY_VALUE -import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_PROPERTY_VALUE -import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_VOCABPROPERTY_VALUE -import com.egm.stellio.shared.util.JsonLdUtils.logger -import com.egm.stellio.shared.util.JsonUtils.deserializeAsMap -import com.egm.stellio.shared.util.JsonUtils.serializeObject -import com.savvasdalkitsis.jsonmerger.JsonMerger -import io.r2dbc.postgresql.codec.Json -import java.time.LocalDate -import java.time.LocalTime -import java.time.ZonedDateTime - -fun valueToDoubleOrNull(value: Any): Double? = - when (value) { - is Double -> value - is Int -> value.toDouble() - else -> null - } - -fun valueToStringOrNull(value: Any): String? = - when (value) { - is String -> value - is Boolean -> value.toString() - else -> null - } - -fun NgsiLdEntity.prepareTemporalAttributes(): Either>> { - val ngsiLdEntity = this - return either { - ngsiLdEntity.attributes - .flatMap { ngsiLdAttribute -> - ngsiLdAttribute.getAttributeInstances().map { Pair(ngsiLdAttribute, it) } - } - .map { - Pair(it.first.name, it.second.toTemporalAttributeMetadata().bind()) - } - } -} - -fun NgsiLdAttributeInstance.toTemporalAttributeMetadata(): Either { - val (attributeType, attributeValueType, attributeValue) = when (this) { - is NgsiLdPropertyInstance -> - guessPropertyValueType(this).let { - Triple(TemporalEntityAttribute.AttributeType.Property, it.first, it.second) - } - is NgsiLdRelationshipInstance -> - Triple( - TemporalEntityAttribute.AttributeType.Relationship, - AttributeValueType.URI, - Triple(this.objectId.toString(), null, null) - ) - is NgsiLdGeoPropertyInstance -> - Triple( - TemporalEntityAttribute.AttributeType.GeoProperty, - AttributeValueType.GEOMETRY, - Triple(null, null, this.coordinates) - ) - is NgsiLdJsonPropertyInstance -> - Triple( - TemporalEntityAttribute.AttributeType.JsonProperty, - AttributeValueType.JSON, - Triple(serializeObject(this.json), null, null) - ) - is NgsiLdLanguagePropertyInstance -> - Triple( - TemporalEntityAttribute.AttributeType.LanguageProperty, - AttributeValueType.ARRAY, - Triple(serializeObject(this.languageMap), null, null) - ) - is NgsiLdVocabPropertyInstance -> - Triple( - TemporalEntityAttribute.AttributeType.VocabProperty, - AttributeValueType.ARRAY, - Triple(serializeObject(this.vocab), null, null) - ) - } - if (attributeValue == Triple(null, null, null)) { - logger.warn("Unable to get a value from attribute: $this") - return BadRequestDataException("Unable to get a value from attribute: $this").left() - } - - return AttributeMetadata( - measuredValue = attributeValue.second, - value = attributeValue.first, - geoValue = attributeValue.third, - valueType = attributeValueType, - datasetId = this.datasetId, - type = attributeType, - observedAt = this.observedAt - ).right() -} - -fun guessAttributeValueType( - attributeType: TemporalEntityAttribute.AttributeType, - expandedAttributeInstance: ExpandedAttributeInstance -): AttributeValueType = - when (attributeType) { - TemporalEntityAttribute.AttributeType.Property -> - guessPropertyValueType(expandedAttributeInstance.getPropertyValue()!!).first - TemporalEntityAttribute.AttributeType.Relationship -> AttributeValueType.URI - TemporalEntityAttribute.AttributeType.GeoProperty -> AttributeValueType.GEOMETRY - TemporalEntityAttribute.AttributeType.JsonProperty -> AttributeValueType.JSON - TemporalEntityAttribute.AttributeType.LanguageProperty -> AttributeValueType.ARRAY - TemporalEntityAttribute.AttributeType.VocabProperty -> AttributeValueType.ARRAY - } - -fun guessPropertyValueType( - ngsiLdPropertyInstance: NgsiLdPropertyInstance -): Pair> = - guessPropertyValueType(ngsiLdPropertyInstance.value) - -fun guessPropertyValueType( - value: Any -): Pair> = - when (value) { - is Double -> Pair(AttributeValueType.NUMBER, Triple(null, valueToDoubleOrNull(value), null)) - is Int -> Pair(AttributeValueType.NUMBER, Triple(null, valueToDoubleOrNull(value), null)) - is Map<*, *> -> Pair(AttributeValueType.OBJECT, Triple(serializeObject(value), null, null)) - is List<*> -> Pair(AttributeValueType.ARRAY, Triple(serializeObject(value), null, null)) - is String -> Pair(AttributeValueType.STRING, Triple(value, null, null)) - is Boolean -> Pair(AttributeValueType.BOOLEAN, Triple(value.toString(), null, null)) - is LocalDate -> Pair(AttributeValueType.DATE, Triple(value.toString(), null, null)) - is ZonedDateTime -> Pair(AttributeValueType.DATETIME, Triple(value.toString(), null, null)) - is LocalTime -> Pair(AttributeValueType.TIME, Triple(value.toString(), null, null)) - else -> Pair(AttributeValueType.STRING, Triple(value.toString(), null, null)) - } - -fun Json.toExpandedAttributeInstance(): ExpandedAttributeInstance = - this.deserializeAsMap() as ExpandedAttributeInstance - -fun partialUpdatePatch( - source: ExpandedAttributeInstance, - update: ExpandedAttributeInstance -): Pair { - val target = source.plus(update) - return Pair(serializeObject(target), target) -} - -fun mergePatch( - source: ExpandedAttributeInstance, - update: ExpandedAttributeInstance -): Pair { - val target = source.toMutableMap() - update.forEach { (attrName, attrValue) -> - if (!source.containsKey(attrName)) { - target[attrName] = attrValue - } else if ( - listOf(NGSILD_JSONPROPERTY_VALUE, NGSILD_VOCABPROPERTY_VALUE, NGSILD_PROPERTY_VALUE).contains(attrName) - ) { - if (attrValue.size > 1) { - // a Property holding an array of value or a JsonPropery holding an array of JSON objects - // cannot be safely merged patch, so copy the whole value from the update - target[attrName] = attrValue - } else { - target[attrName] = listOf( - JsonMerger().merge( - serializeObject(source[attrName]!![0]), - serializeObject(attrValue[0]) - ).deserializeAsMap() - ) - } - } else if (listOf(NGSILD_LANGUAGEPROPERTY_VALUE).contains(attrName)) { - val sourceLangEntries = source[attrName] as List> - val targetLangEntries = sourceLangEntries.toMutableList() - (attrValue as List>).forEach { langEntry -> - // remove any previously existing entry for this language - targetLangEntries.removeIf { - it[JSONLD_LANGUAGE] == langEntry[JSONLD_LANGUAGE] - } - targetLangEntries.add(langEntry) - } - - target[attrName] = targetLangEntries - } else { - target[attrName] = attrValue - } - } - - return Pair(serializeObject(target), target) -} diff --git a/search-service/src/main/kotlin/db/migration/V0_29__JsonLd_migration.kt b/search-service/src/main/kotlin/db/migration/V0_29__JsonLd_migration.kt index badc28eb9..afb02cf85 100644 --- a/search-service/src/main/kotlin/db/migration/V0_29__JsonLd_migration.kt +++ b/search-service/src/main/kotlin/db/migration/V0_29__JsonLd_migration.kt @@ -1,10 +1,10 @@ package db.migration import arrow.core.Either -import com.egm.stellio.search.model.AttributeInstance -import com.egm.stellio.search.model.TemporalEntityAttribute -import com.egm.stellio.search.util.guessPropertyValueType -import com.egm.stellio.search.util.toTemporalAttributeMetadata +import com.egm.stellio.search.entity.model.Attribute +import com.egm.stellio.search.entity.util.guessPropertyValueType +import com.egm.stellio.search.entity.util.toAttributeMetadata +import com.egm.stellio.search.temporal.model.AttributeInstance import com.egm.stellio.shared.model.* import com.egm.stellio.shared.util.AuthContextModel import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_SAP @@ -203,7 +203,7 @@ class V0_29__JsonLd_migration : BaseJavaMigration() { ngsiLdAttributeInstance: NgsiLdAttributeInstance, defaultCreatedAt: ZonedDateTime ) { - when (val temporalAttributesMetadata = ngsiLdAttributeInstance.toTemporalAttributeMetadata()) { + when (val temporalAttributesMetadata = ngsiLdAttributeInstance.toAttributeMetadata()) { is Either.Left -> logger.warn("Unable to process attribute $attributeName ($datasetId) from entity $entityId") is Either.Right -> { @@ -267,11 +267,11 @@ class V0_29__JsonLd_migration : BaseJavaMigration() { val valueType = when (ngsiLdAttributeInstance) { is NgsiLdPropertyInstance -> guessPropertyValueType(ngsiLdAttributeInstance).first - is NgsiLdRelationshipInstance -> TemporalEntityAttribute.AttributeValueType.URI - is NgsiLdGeoPropertyInstance -> TemporalEntityAttribute.AttributeValueType.GEOMETRY - is NgsiLdJsonPropertyInstance -> TemporalEntityAttribute.AttributeValueType.OBJECT - is NgsiLdLanguagePropertyInstance -> TemporalEntityAttribute.AttributeValueType.ARRAY - is NgsiLdVocabPropertyInstance -> TemporalEntityAttribute.AttributeValueType.ARRAY + is NgsiLdRelationshipInstance -> Attribute.AttributeValueType.URI + is NgsiLdGeoPropertyInstance -> Attribute.AttributeValueType.GEOMETRY + is NgsiLdJsonPropertyInstance -> Attribute.AttributeValueType.OBJECT + is NgsiLdLanguagePropertyInstance -> Attribute.AttributeValueType.ARRAY + is NgsiLdVocabPropertyInstance -> Attribute.AttributeValueType.ARRAY } jdbcTemplate.execute( diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/listener/IAMListenerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/authorization/listener/IAMListenerTests.kt similarity index 92% rename from search-service/src/test/kotlin/com/egm/stellio/search/listener/IAMListenerTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/authorization/listener/IAMListenerTests.kt index d867a7b21..0132af224 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/listener/IAMListenerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/authorization/listener/IAMListenerTests.kt @@ -1,12 +1,10 @@ -package com.egm.stellio.search.listener +package com.egm.stellio.search.authorization.listener import arrow.core.right -import com.egm.stellio.search.authorization.EntityAccessRightsService -import com.egm.stellio.search.authorization.SubjectReferentialService -import com.egm.stellio.search.config.SearchProperties -import com.egm.stellio.search.model.EntityPayload -import com.egm.stellio.search.service.EntityEventService -import com.egm.stellio.search.service.EntityPayloadService +import com.egm.stellio.search.authorization.service.EntityAccessRightsService +import com.egm.stellio.search.authorization.service.SubjectReferentialService +import com.egm.stellio.search.common.config.SearchProperties +import com.egm.stellio.search.entity.service.EntityService import com.egm.stellio.shared.util.GlobalRole import com.egm.stellio.shared.util.SubjectType import com.egm.stellio.shared.util.loadSampleData @@ -15,8 +13,6 @@ import com.ninjasquad.springmockk.MockkBean import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every -import io.mockk.mockkClass -import kotlinx.coroutines.Job import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired @@ -34,7 +30,7 @@ class IAMListenerTests { private lateinit var entityAccessRightsService: EntityAccessRightsService @MockkBean(relaxed = true) - private lateinit var entityPayloadService: EntityPayloadService + private lateinit var entityService: EntityService @MockkBean(relaxed = true) private lateinit var searchProperties: SearchProperties @@ -42,9 +38,6 @@ class IAMListenerTests { @MockkBean(relaxed = true) private lateinit var subjectReferentialService: SubjectReferentialService - @MockkBean(relaxed = true) - private lateinit var entityEventService: EntityEventService - private val entityId = "urn:ngsi-ld:BeeHive:TESTC".toUri() @Test @@ -355,8 +348,7 @@ class IAMListenerTests { coEvery { entityAccessRightsService.getEntitiesIdsOwnedBySubject("6ad19fe0-fc11-4024-85f2-931c6fa6f7e0") } returns listOf(entityId).right() - coEvery { entityPayloadService.deleteEntity(entityId) } returns mockkClass(EntityPayload::class).right() - coEvery { entityEventService.publishEntityDeleteEvent(any(), any()) } returns Job() + coEvery { entityService.deleteEntity(entityId, "6ad19fe0-fc11-4024-85f2-931c6fa6f7e0") } returns Unit.right() iamListener.dispatchIamMessage(subjectDeleteEvent) @@ -368,14 +360,10 @@ class IAMListenerTests { ) } coVerify { - entityPayloadService.deleteEntity( - match { - it == entityId - } + entityService.deleteEntity( + eq(entityId), + eq("6ad19fe0-fc11-4024-85f2-931c6fa6f7e0") ) } - coVerify(timeout = 1000) { - entityEventService.publishEntityDeleteEvent(null, any()) - } } } diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/authorization/AuthorizationServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/authorization/service/AuthorizationServiceTests.kt similarity index 95% rename from search-service/src/test/kotlin/com/egm/stellio/search/authorization/AuthorizationServiceTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/authorization/service/AuthorizationServiceTests.kt index 663ed1dbf..febd20e8f 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/authorization/AuthorizationServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/authorization/service/AuthorizationServiceTests.kt @@ -1,7 +1,7 @@ -package com.egm.stellio.search.authorization +package com.egm.stellio.search.authorization.service import arrow.core.None -import com.egm.stellio.search.model.EntitiesQuery +import com.egm.stellio.search.entity.model.EntitiesQuery import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.model.PaginationQuery import com.egm.stellio.shared.util.AUTHZ_TEST_COMPOUND_CONTEXTS diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/authorization/EnabledAuthorizationServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/authorization/service/EnabledAuthorizationServiceTests.kt similarity index 98% rename from search-service/src/test/kotlin/com/egm/stellio/search/authorization/EnabledAuthorizationServiceTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/authorization/service/EnabledAuthorizationServiceTests.kt index 7442ae12e..63f309620 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/authorization/EnabledAuthorizationServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/authorization/service/EnabledAuthorizationServiceTests.kt @@ -1,10 +1,13 @@ -package com.egm.stellio.search.authorization +package com.egm.stellio.search.authorization.service import arrow.core.Either import arrow.core.Some import arrow.core.right -import com.egm.stellio.search.authorization.EntityAccessRights.SubjectRightInfo -import com.egm.stellio.search.model.EntitiesQuery +import com.egm.stellio.search.authorization.model.EntityAccessRights +import com.egm.stellio.search.authorization.model.EntityAccessRights.SubjectRightInfo +import com.egm.stellio.search.authorization.model.Group +import com.egm.stellio.search.authorization.model.User +import com.egm.stellio.search.entity.model.EntitiesQuery import com.egm.stellio.shared.model.AccessDeniedException import com.egm.stellio.shared.model.PaginationQuery import com.egm.stellio.shared.util.* diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/authorization/EntityAccessRightsServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/authorization/service/EntityAccessRightsServiceTests.kt similarity index 83% rename from search-service/src/test/kotlin/com/egm/stellio/search/authorization/EntityAccessRightsServiceTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/authorization/service/EntityAccessRightsServiceTests.kt index 1b3b816c4..9f40da51b 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/authorization/EntityAccessRightsServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/authorization/service/EntityAccessRightsServiceTests.kt @@ -1,10 +1,16 @@ -package com.egm.stellio.search.authorization +package com.egm.stellio.search.authorization.service import arrow.core.Some import arrow.core.right -import com.egm.stellio.search.model.EntityPayload -import com.egm.stellio.search.service.EntityPayloadService +import com.egm.stellio.search.authorization.getSubjectInfoForClient +import com.egm.stellio.search.authorization.getSubjectInfoForGroup +import com.egm.stellio.search.authorization.getSubjectInfoForUser +import com.egm.stellio.search.authorization.model.SubjectAccessRight +import com.egm.stellio.search.authorization.model.SubjectReferential +import com.egm.stellio.search.entity.model.Entity +import com.egm.stellio.search.entity.service.EntityService import com.egm.stellio.search.support.WithTimescaleContainer +import com.egm.stellio.search.support.buildSapAttribute import com.egm.stellio.shared.model.AccessDeniedException import com.egm.stellio.shared.model.ExpandedTerm import com.egm.stellio.shared.model.PaginationQuery @@ -14,12 +20,12 @@ import com.egm.stellio.shared.util.AuthContextModel.CLIENT_ENTITY_PREFIX import com.egm.stellio.shared.util.AuthContextModel.DATASET_ID_PREFIX import com.egm.stellio.shared.util.AuthContextModel.GROUP_ENTITY_PREFIX import com.egm.stellio.shared.util.AuthContextModel.SpecificAccessPolicy.AUTH_READ +import com.egm.stellio.shared.util.AuthContextModel.SpecificAccessPolicy.AUTH_WRITE import com.ninjasquad.springmockk.SpykBean import io.mockk.Called import io.mockk.coEvery import io.mockk.coVerify import io.r2dbc.postgresql.codec.Json -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.* @@ -48,7 +54,7 @@ class EntityAccessRightsServiceTests : WithTimescaleContainer { private lateinit var subjectReferentialService: SubjectReferentialService @SpykBean - private lateinit var entityPayloadService: EntityPayloadService + private lateinit var entityService: EntityService private val userUuid = "0768A6D5-D87B-4209-9A22-8C40A8961A79" private val groupUuid = "220FC854-3609-404B-BC77-F2DFE332B27B" @@ -67,25 +73,19 @@ class EntityAccessRightsServiceTests : WithTimescaleContainer { @AfterEach fun clearEntityAccessRightsTable() { r2dbcEntityTemplate.delete().from("entity_access_rights").all().block() - r2dbcEntityTemplate.delete(EntityPayload::class.java) - .all() - .block() + r2dbcEntityTemplate.delete().from("entity_payload").all().block() } @Test fun `it should add a new entity in the allowed list of read entities`() = runTest { - coEvery { entityPayloadService.hasSpecificAccessPolicies(any(), any()) } returns true.right() - entityAccessRightsService.setReadRoleOnEntity(userUuid, entityId01) - entityAccessRightsService.canReadEntity(Some(userUuid), "urn:ngsi-ld:Entity:1111".toUri()) + entityAccessRightsService.canReadEntity(Some(userUuid), entityId01) .shouldSucceed() } @Test fun `it should remove an entity from the allowed list of read entities`() = runTest { - coEvery { entityPayloadService.hasSpecificAccessPolicies(any(), any()) } returns false.right() - entityAccessRightsService.setReadRoleOnEntity(userUuid, entityId01) entityAccessRightsService.removeRoleOnEntity(userUuid, entityId01) @@ -97,8 +97,6 @@ class EntityAccessRightsServiceTests : WithTimescaleContainer { @Test fun `it should remove an entity from the list of known entities`() = runTest { - coEvery { entityPayloadService.hasSpecificAccessPolicies(any(), any()) } returns false.right() - entityAccessRightsService.setReadRoleOnEntity(userUuid, entityId01) entityAccessRightsService.canReadEntity(Some(userUuid), entityId01).shouldSucceed() @@ -113,8 +111,6 @@ class EntityAccessRightsServiceTests : WithTimescaleContainer { @Test fun `it should allow the owner of entity to administrate the entity`() = runTest { - coEvery { entityPayloadService.hasSpecificAccessPolicies(any(), any()) } returns false.right() - entityAccessRightsService.setOwnerRoleOnEntity(userUuid, entityId01) entityAccessRightsService.canWriteEntity(Some(userUuid), entityId01).shouldSucceed() @@ -128,8 +124,6 @@ class EntityAccessRightsServiceTests : WithTimescaleContainer { @Test fun `it should allow an user having a direct read role on a entity`() = runTest { - coEvery { entityPayloadService.hasSpecificAccessPolicies(any(), any()) } returns false.right() - entityAccessRightsService.setReadRoleOnEntity(userUuid, entityId01) entityAccessRightsService.setWriteRoleOnEntity(userUuid, "urn:ngsi-ld:Entity:6666".toUri()) @@ -148,7 +142,6 @@ class EntityAccessRightsServiceTests : WithTimescaleContainer { @Test fun `it should allow an user having a read role on a entity via a group membership`() = runTest { - coEvery { entityPayloadService.hasSpecificAccessPolicies(any(), any()) } returns false.right() coEvery { subjectReferentialService.getSubjectAndGroupsUUID(Some(userUuid)) } returns listOf(groupUuid, userUuid).right() @@ -173,7 +166,6 @@ class EntityAccessRightsServiceTests : WithTimescaleContainer { subjectReferentialService.getSubjectAndGroupsUUID(Some(userUuid)) } answers { listOf(groupUuid, userUuid).right() } coEvery { subjectReferentialService.hasStellioAdminRole(any()) } returns false.right() - coEvery { entityPayloadService.hasSpecificAccessPolicies(any(), any()) } returns false.right() entityAccessRightsService.setReadRoleOnEntity(groupUuid, entityId01) entityAccessRightsService.setReadRoleOnEntity(userUuid, entityId01) @@ -208,60 +200,19 @@ class EntityAccessRightsServiceTests : WithTimescaleContainer { entityAccessRightsService.setReadRoleOnEntity(userUuid, entityId01) entityAccessRightsService.delete(userUuid).shouldSucceed() - } - @Test - fun `it should allow user who have read right on entity`() = runTest { - coEvery { entityPayloadService.hasSpecificAccessPolicies(any(), any()) } returns false.right() - - entityAccessRightsService.setReadRoleOnEntity(userUuid, entityId01) - - entityAccessRightsService.canReadEntity(Some(userUuid), entityId01).shouldSucceed() + entityAccessRightsService.canReadEntity(Some(userUuid), entityId01).shouldFailWith { + it is AccessDeniedException + } } @Test fun `it should allow user who have write right on entity`() = runTest { - coEvery { entityPayloadService.hasSpecificAccessPolicies(any(), any()) } returns false.right() - entityAccessRightsService.setWriteRoleOnEntity(userUuid, entityId01) entityAccessRightsService.canWriteEntity(Some(userUuid), entityId01).shouldSucceed() } - @Test - fun `it should not allow user who have not read right on entity and entity has not a specific access policy`() = - runTest { - coEvery { entityPayloadService.hasSpecificAccessPolicies(any(), any()) } returns false.right() - - runBlocking { - entityAccessRightsService.canReadEntity(Some(userUuid), entityId01).fold( - { assertEquals("User forbidden read access to entity $entityId01", it.message) }, - { fail("it should have not read right on entity") } - ) - } - } - - @Test - fun `it should not allow user who have not write right on entity and entity has not a specific access policy`() = - runTest { - coEvery { entityPayloadService.hasSpecificAccessPolicies(any(), any()) } returns false.right() - - entityAccessRightsService.canWriteEntity(Some(userUuid), entityId01).fold( - { assertEquals("User forbidden write access to entity $entityId01", it.message) }, - { fail("it should have not write right on entity") } - ) - } - - @Test - fun `it should allow user it has no right on entity but entity has a specific access policy`() = runTest { - coEvery { entityPayloadService.hasSpecificAccessPolicies(any(), any()) } returns true.right() - - entityAccessRightsService.canWriteEntity(Some(userUuid), entityId01).shouldSucceed() - entityAccessRightsService.canReadEntity(Some(userUuid), entityId01).shouldSucceed() - - coVerify { subjectReferentialService.getSubjectAndGroupsUUID(Some(userUuid)) wasNot Called } - } - @Test fun `it should find if user has ownership on an entity`() = runTest { entityAccessRightsService.setOwnerRoleOnEntity(userUuid, entityId01).shouldSucceed() @@ -611,7 +562,7 @@ class EntityAccessRightsServiceTests : WithTimescaleContainer { } @Test - fun `it should get Ids of all entities owned by a user`() = runTest { + fun `it should get ids of all entities owned by a user`() = runTest { createEntityPayload(entityId01, setOf(BEEHIVE_TYPE)) createEntityPayload(entityId02, setOf(BEEHIVE_TYPE)) entityAccessRightsService.setOwnerRoleOnEntity(userUuid, entityId01).shouldSucceed() @@ -635,6 +586,126 @@ class EntityAccessRightsServiceTests : WithTimescaleContainer { entityAccessRightsService.getAccessRightsForEntities(Some(userUuid), entitiesIds) .shouldSucceedWith { assertTrue(it.isEmpty()) } } + + @Test + fun `it should create an entity payload from string with specificAccessPolicy`() = runTest { + createEntityPayload(entityId01, setOf(BEEHIVE_TYPE), AUTH_READ) + createEntityPayload(entityId02, setOf(BEEHIVE_TYPE), AUTH_WRITE) + + entityAccessRightsService.hasSpecificAccessPolicies( + entityId01, + listOf(AUTH_READ) + ).shouldSucceedWith { assertTrue(it) } + entityAccessRightsService.hasSpecificAccessPolicies( + entityId01, + listOf(AUTH_WRITE) + ).shouldSucceedWith { assertFalse(it) } + + entityAccessRightsService.hasSpecificAccessPolicies( + entityId02, + listOf(AUTH_READ) + ).shouldSucceedWith { assertFalse(it) } + entityAccessRightsService.hasSpecificAccessPolicies( + entityId02, + listOf(AUTH_WRITE) + ).shouldSucceedWith { assertTrue(it) } + } + + @Test + fun `it should create an entity payload from an NGSI-LD Entity with specificAccessPolicy`() = runTest { + createEntityPayload(entityId01, setOf(BEEHIVE_TYPE), AUTH_READ) + + entityAccessRightsService.hasSpecificAccessPolicies( + entityId01, + listOf(AUTH_READ) + ).shouldSucceedWith { assertTrue(it) } + entityAccessRightsService.hasSpecificAccessPolicies( + entityId01, + listOf(AUTH_WRITE) + ).shouldSucceedWith { assertFalse(it) } + } + + @Test + fun `it should allow to read an entity with an AUTH_READ specific access policy`() = runTest { + createEntityPayload(entityId01, setOf(BEEHIVE_TYPE), AUTH_READ) + createEntityPayload(entityId02, setOf(BEEHIVE_TYPE)) + + entityAccessRightsService.canReadEntity(Some(userUuid), entityId01).shouldSucceed() + entityAccessRightsService.canReadEntity(Some(userUuid), entityId02).shouldFailWith { + it is AccessDeniedException + } + } + + @Test + fun `it should allow to read an entity with an AUTH_READ or AUTH_WRITE specific access policy`() = runTest { + val entityId03 = "urn:ngsi-ld:Entity:03".toUri() + createEntityPayload(entityId01, setOf(BEEHIVE_TYPE), AUTH_READ) + createEntityPayload(entityId02, setOf(BEEHIVE_TYPE)) + createEntityPayload(entityId03, setOf(BEEHIVE_TYPE), AUTH_WRITE) + + entityAccessRightsService.canReadEntity(Some(userUuid), entityId01).shouldSucceed() + entityAccessRightsService.canReadEntity(Some(userUuid), entityId02).shouldFailWith { + it is AccessDeniedException + } + entityAccessRightsService.canReadEntity(Some(userUuid), entityId03).shouldSucceed() + } + + @Test + fun `it should allow to write an entity with an AUTH_WRITE specific access policy`() = runTest { + createEntityPayload(entityId01, setOf(BEEHIVE_TYPE), AUTH_WRITE) + createEntityPayload(entityId02, setOf(BEEHIVE_TYPE)) + + entityAccessRightsService.canWriteEntity(Some(userUuid), entityId01).shouldSucceed() + entityAccessRightsService.canWriteEntity(Some(userUuid), entityId02).shouldFailWith { + it is AccessDeniedException + } + } + + @Test + fun `it should update a specific access policy for a temporal entity`() = runTest { + createEntityPayload(entityId01, setOf(BEEHIVE_TYPE), AUTH_READ) + createEntityPayload(entityId02, setOf(BEEHIVE_TYPE), AUTH_READ) + + entityAccessRightsService.updateSpecificAccessPolicy( + entityId01, + buildSapAttribute(AUTH_WRITE) + ).shouldSucceed() + + entityAccessRightsService.hasSpecificAccessPolicies( + entityId01, + listOf(AUTH_READ) + ).shouldSucceedWith { assertFalse(it) } + entityAccessRightsService.hasSpecificAccessPolicies( + entityId01, + listOf(AUTH_WRITE) + ).shouldSucceedWith { assertTrue(it) } + entityAccessRightsService.hasSpecificAccessPolicies( + entityId02, + listOf(AUTH_WRITE) + ).shouldSucceedWith { assertFalse(it) } + } + + @Test + fun `it should remove a specific access policy from a entity payload`() = runTest { + createEntityPayload(entityId01, setOf(BEEHIVE_TYPE), AUTH_READ) + + entityAccessRightsService.hasSpecificAccessPolicies( + entityId01, + listOf(AUTH_READ) + ).shouldSucceedWith { assertTrue(it) } + entityAccessRightsService.removeSpecificAccessPolicy(entityId01).shouldSucceed() + entityAccessRightsService.hasSpecificAccessPolicies( + entityId01, + listOf(AUTH_READ) + ).shouldSucceedWith { assertFalse(it) } + } + + @Test + fun `it should return nothing when specific access policy list is empty`() = runTest { + entityAccessRightsService.hasSpecificAccessPolicies(entityId01, emptyList()) + .shouldSucceedWith { assertFalse(it) } + } + private suspend fun createEntityPayload( entityId: URI, types: Set, @@ -645,7 +716,7 @@ class EntityAccessRightsServiceTests : WithTimescaleContainer { loadMinimalEntityWithSap(entityId, types, specificAccessPolicy, AUTHZ_TEST_COMPOUND_CONTEXTS) else loadMinimalEntity(entityId, types) rawEntity.sampleDataToNgsiLdEntity().map { - entityPayloadService.createEntityPayload( + entityService.createEntityPayload( ngsiLdEntity = it.second, expandedEntity = it.first, createdAt = ngsiLdDateTime() diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/authorization/SubjectReferentialServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/authorization/service/SubjectReferentialServiceTests.kt similarity index 98% rename from search-service/src/test/kotlin/com/egm/stellio/search/authorization/SubjectReferentialServiceTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/authorization/service/SubjectReferentialServiceTests.kt index 3fac48ba3..6ea903491 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/authorization/SubjectReferentialServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/authorization/service/SubjectReferentialServiceTests.kt @@ -1,6 +1,10 @@ -package com.egm.stellio.search.authorization +package com.egm.stellio.search.authorization.service import arrow.core.Some +import com.egm.stellio.search.authorization.getSubjectInfoForClient +import com.egm.stellio.search.authorization.getSubjectInfoForGroup +import com.egm.stellio.search.authorization.getSubjectInfoForUser +import com.egm.stellio.search.authorization.model.SubjectReferential import com.egm.stellio.search.support.EMPTY_JSON_PAYLOAD import com.egm.stellio.search.support.WithTimescaleContainer import com.egm.stellio.shared.model.AccessDeniedException diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/web/AnonymousUserHandlerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/authorization/web/AnonymousUserHandlerTests.kt similarity index 70% rename from search-service/src/test/kotlin/com/egm/stellio/search/web/AnonymousUserHandlerTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/authorization/web/AnonymousUserHandlerTests.kt index d80f2b957..5cbf00059 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/web/AnonymousUserHandlerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/authorization/web/AnonymousUserHandlerTests.kt @@ -1,11 +1,11 @@ -package com.egm.stellio.search.web - -import com.egm.stellio.search.authorization.AuthorizationService -import com.egm.stellio.search.config.SearchProperties -import com.egm.stellio.search.service.EntityEventService -import com.egm.stellio.search.service.EntityPayloadService -import com.egm.stellio.search.service.QueryService -import com.egm.stellio.search.service.TemporalEntityAttributeService +package com.egm.stellio.search.authorization.web + +import com.egm.stellio.search.authorization.service.AuthorizationService +import com.egm.stellio.search.common.config.SearchProperties +import com.egm.stellio.search.entity.service.EntityEventService +import com.egm.stellio.search.entity.service.EntityQueryService +import com.egm.stellio.search.entity.service.EntityService +import com.egm.stellio.search.entity.web.EntityHandler import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.util.AQUAC_HEADER_LINK import com.ninjasquad.springmockk.MockkBean @@ -28,13 +28,7 @@ class AnonymousUserHandlerTests { private lateinit var webClient: WebTestClient @MockkBean - private lateinit var entityPayloadService: EntityPayloadService - - @MockkBean - private lateinit var temporalEntityAttributeService: TemporalEntityAttributeService - - @MockkBean - private lateinit var queryService: QueryService + private lateinit var entityService: EntityService @MockkBean(relaxed = true) private lateinit var authorizationService: AuthorizationService @@ -42,6 +36,9 @@ class AnonymousUserHandlerTests { @MockkBean private lateinit var entityEventService: EntityEventService + @MockkBean + private lateinit var entityQueryService: EntityQueryService + @Test @WithAnonymousUser fun `it should not authorize an anonymous to call the API`() { diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityAccessControlHandlerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/authorization/web/EntityAccessControlHandlerTests.kt similarity index 96% rename from search-service/src/test/kotlin/com/egm/stellio/search/web/EntityAccessControlHandlerTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/authorization/web/EntityAccessControlHandlerTests.kt index 7721f753a..263b8e6c2 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityAccessControlHandlerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/authorization/web/EntityAccessControlHandlerTests.kt @@ -1,13 +1,12 @@ -package com.egm.stellio.search.web +package com.egm.stellio.search.authorization.web import arrow.core.left import arrow.core.right -import com.egm.stellio.search.authorization.AuthorizationService -import com.egm.stellio.search.authorization.EntityAccessRights -import com.egm.stellio.search.authorization.EntityAccessRightsService -import com.egm.stellio.search.authorization.User -import com.egm.stellio.search.config.SearchProperties -import com.egm.stellio.search.service.EntityPayloadService +import com.egm.stellio.search.authorization.model.EntityAccessRights +import com.egm.stellio.search.authorization.model.User +import com.egm.stellio.search.authorization.service.AuthorizationService +import com.egm.stellio.search.authorization.service.EntityAccessRightsService +import com.egm.stellio.search.common.config.SearchProperties import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.model.* import com.egm.stellio.shared.util.* @@ -60,9 +59,6 @@ class EntityAccessControlHandlerTests { @MockkBean(relaxed = true) private lateinit var entityAccessRightsService: EntityAccessRightsService - @MockkBean(relaxed = true) - private lateinit var entityPayloadService: EntityPayloadService - @MockkBean(relaxed = true) private lateinit var authorizationService: AuthorizationService @@ -405,7 +401,7 @@ class EntityAccessControlHandlerTests { """.trimIndent() coEvery { authorizationService.userCanAdminEntity(any(), any()) } returns Unit.right() - coEvery { entityPayloadService.updateSpecificAccessPolicy(any(), any()) } returns Unit.right() + coEvery { entityAccessRightsService.updateSpecificAccessPolicy(any(), any()) } returns Unit.right() webClient.post() .uri("/ngsi-ld/v1/entityAccessControl/$entityUri1/attrs/specificAccessPolicy") @@ -418,7 +414,7 @@ class EntityAccessControlHandlerTests { coVerify { authorizationService.userCanAdminEntity(eq(entityUri1), eq(sub)) - entityPayloadService.updateSpecificAccessPolicy( + entityAccessRightsService.updateSpecificAccessPolicy( eq(entityUri1), match { it.getAttributeInstances().size == 1 && @@ -442,7 +438,7 @@ class EntityAccessControlHandlerTests { """.trimIndent() coEvery { authorizationService.userCanAdminEntity(any(), any()) } returns Unit.right() - coEvery { entityPayloadService.updateSpecificAccessPolicy(any(), any()) } returns Unit.right() + coEvery { entityAccessRightsService.updateSpecificAccessPolicy(any(), any()) } returns Unit.right() webClient.post() .uri("/ngsi-ld/v1/entityAccessControl/$entityUri1/attrs/specificAccessPolicy") @@ -453,7 +449,7 @@ class EntityAccessControlHandlerTests { coVerify { authorizationService.userCanAdminEntity(eq(entityUri1), eq(sub)) - entityPayloadService.updateSpecificAccessPolicy( + entityAccessRightsService.updateSpecificAccessPolicy( eq(entityUri1), match { it.getAttributeInstances().size == 1 && @@ -477,7 +473,7 @@ class EntityAccessControlHandlerTests { """.trimIndent() coEvery { authorizationService.userCanAdminEntity(any(), any()) } returns Unit.right() - coEvery { entityPayloadService.updateSpecificAccessPolicy(any(), any()) } returns Unit.right() + coEvery { entityAccessRightsService.updateSpecificAccessPolicy(any(), any()) } returns Unit.right() val expectedAttr = "https://uri.etsi.org/ngsi-ld/default-context/specificAccessPolicy" @@ -509,7 +505,7 @@ class EntityAccessControlHandlerTests { coEvery { authorizationService.userCanAdminEntity(any(), any()) } returns Unit.right() coEvery { - entityPayloadService.updateSpecificAccessPolicy(any(), any()) + entityAccessRightsService.updateSpecificAccessPolicy(any(), any()) } returns BadRequestDataException("Bad request").left() webClient.post() @@ -533,7 +529,7 @@ class EntityAccessControlHandlerTests { """.trimIndent() coEvery { authorizationService.userCanAdminEntity(any(), any()) } returns Unit.right() - coEvery { entityPayloadService.updateSpecificAccessPolicy(any(), any()) } throws RuntimeException() + coEvery { entityAccessRightsService.updateSpecificAccessPolicy(any(), any()) } throws RuntimeException() webClient.post() .uri("/ngsi-ld/v1/entityAccessControl/$entityUri1/attrs/specificAccessPolicy") @@ -562,7 +558,7 @@ class EntityAccessControlHandlerTests { @Test fun `it should allow an authorized user to delete the specific access policy on an entity`() { coEvery { authorizationService.userCanAdminEntity(any(), any()) } returns Unit.right() - coEvery { entityPayloadService.removeSpecificAccessPolicy(any()) } returns Unit.right() + coEvery { entityAccessRightsService.removeSpecificAccessPolicy(any()) } returns Unit.right() webClient.delete() .uri("/ngsi-ld/v1/entityAccessControl/$entityUri1/attrs/specificAccessPolicy") @@ -573,7 +569,7 @@ class EntityAccessControlHandlerTests { coVerify { authorizationService.userCanAdminEntity(eq(entityUri1), eq(sub)) - entityPayloadService.removeSpecificAccessPolicy(eq(entityUri1)) + entityAccessRightsService.removeSpecificAccessPolicy(eq(entityUri1)) } } diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/AttributeServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/discovery/service/AttributeServiceTests.kt similarity index 72% rename from search-service/src/test/kotlin/com/egm/stellio/search/service/AttributeServiceTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/discovery/service/AttributeServiceTests.kt index 324692660..0ddf6fc3a 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/AttributeServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/discovery/service/AttributeServiceTests.kt @@ -1,14 +1,17 @@ -package com.egm.stellio.search.service +package com.egm.stellio.search.discovery.service import arrow.core.Either -import com.egm.stellio.search.model.* -import com.egm.stellio.search.model.AttributeType +import com.egm.stellio.search.common.util.execute +import com.egm.stellio.search.common.util.toUri +import com.egm.stellio.search.discovery.model.AttributeDetails +import com.egm.stellio.search.discovery.model.AttributeType +import com.egm.stellio.search.discovery.model.AttributeTypeInfo +import com.egm.stellio.search.entity.model.Attribute +import com.egm.stellio.search.entity.model.Entity import com.egm.stellio.search.support.EMPTY_JSON_PAYLOAD import com.egm.stellio.search.support.WithKafkaContainer import com.egm.stellio.search.support.WithTimescaleContainer import com.egm.stellio.search.support.gimmeEntityPayload -import com.egm.stellio.search.util.execute -import com.egm.stellio.search.util.toUri import com.egm.stellio.shared.model.APIException import com.egm.stellio.shared.model.ResourceNotFoundException import com.egm.stellio.shared.util.* @@ -24,6 +27,7 @@ import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.data.r2dbc.core.R2dbcEntityTemplate +import org.springframework.data.r2dbc.core.delete import org.springframework.r2dbc.core.DatabaseClient import org.springframework.test.context.ActiveProfiles import java.time.Instant @@ -47,39 +51,35 @@ class AttributeServiceTests : WithTimescaleContainer, WithKafkaContainer { private val entityPayload1 = gimmeEntityPayload("urn:ngsi-ld:BeeHive:TESTA", listOf(BEEHIVE_TYPE, SENSOR_TYPE)) private val entityPayload2 = gimmeEntityPayload("urn:ngsi-ld:Sensor:TESTB", listOf(SENSOR_TYPE)) private val entityPayload3 = gimmeEntityPayload("urn:ngsi-ld:Apiary:TESTC", listOf(APIARY_TYPE)) - private val temporalEntityAttribute1 = newTemporalEntityAttribute( + private val attribute1 = newAttribute( "urn:ngsi-ld:BeeHive:TESTA", INCOMING_PROPERTY, - TemporalEntityAttribute.AttributeType.Property, - TemporalEntityAttribute.AttributeValueType.NUMBER + Attribute.AttributeType.Property, + Attribute.AttributeValueType.NUMBER ) - private val temporalEntityAttribute2 = newTemporalEntityAttribute( + private val attribute2 = newAttribute( "urn:ngsi-ld:BeeHive:TESTA", MANAGED_BY_RELATIONSHIP, - TemporalEntityAttribute.AttributeType.Relationship, - TemporalEntityAttribute.AttributeValueType.STRING + Attribute.AttributeType.Relationship, + Attribute.AttributeValueType.STRING ) - private val temporalEntityAttribute3 = newTemporalEntityAttribute( + private val attribute3 = newAttribute( "urn:ngsi-ld:Apiary:TESTC", NGSILD_LOCATION_PROPERTY, - TemporalEntityAttribute.AttributeType.GeoProperty, - TemporalEntityAttribute.AttributeValueType.GEOMETRY + Attribute.AttributeType.GeoProperty, + Attribute.AttributeValueType.GEOMETRY ) - private val temporalEntityAttribute4 = newTemporalEntityAttribute( + private val attribute4 = newAttribute( "urn:ngsi-ld:Sensor:TESTB", OUTGOING_PROPERTY, - TemporalEntityAttribute.AttributeType.Property, - TemporalEntityAttribute.AttributeValueType.GEOMETRY + Attribute.AttributeType.Property, + Attribute.AttributeValueType.GEOMETRY ) @AfterEach - fun clearPreviousTemporalEntityAttributesAndObservations() { - r2dbcEntityTemplate.delete(EntityPayload::class.java) - .all() - .block() - r2dbcEntityTemplate.delete(TemporalEntityAttribute::class.java) - .all() - .block() + fun clearPreviousAttributesAndObservations() { + r2dbcEntityTemplate.delete().from("entity_payload").all().block() + r2dbcEntityTemplate.delete().from("temporal_entity_attribute").all().block() } @BeforeEach @@ -87,10 +87,10 @@ class AttributeServiceTests : WithTimescaleContainer, WithKafkaContainer { createEntityPayload(entityPayload1) createEntityPayload(entityPayload2) createEntityPayload(entityPayload3) - createTemporalEntityAttribute(temporalEntityAttribute1) - createTemporalEntityAttribute(temporalEntityAttribute2) - createTemporalEntityAttribute(temporalEntityAttribute3) - createTemporalEntityAttribute(temporalEntityAttribute4) + createAttribute(attribute1) + createAttribute(attribute2) + createAttribute(attribute3) + createAttribute(attribute4) } @Test @@ -108,7 +108,7 @@ class AttributeServiceTests : WithTimescaleContainer, WithKafkaContainer { @Test fun `it should return an empty list of attributes if no attributes was found`() = runTest { - clearPreviousTemporalEntityAttributesAndObservations() + clearPreviousAttributesAndObservations() val attributeNames = attributeService.getAttributeList(APIC_COMPOUND_CONTEXTS) assert(attributeNames.attributeList.isEmpty()) @@ -148,7 +148,7 @@ class AttributeServiceTests : WithTimescaleContainer, WithKafkaContainer { @Test fun `it should return an empty list of AttributeDetails if no attribute was found`() = runTest { - clearPreviousTemporalEntityAttributesAndObservations() + clearPreviousAttributesAndObservations() val attributeDetails = attributeService.getAttributeDetails(APIC_COMPOUND_CONTEXTS) assertTrue(attributeDetails.isEmpty()) @@ -180,8 +180,8 @@ class AttributeServiceTests : WithTimescaleContainer, WithKafkaContainer { } } - private fun createTemporalEntityAttribute( - temporalEntityAttribute: TemporalEntityAttribute + private fun createAttribute( + attribute: Attribute ): Either = runBlocking { databaseClient.sql( @@ -192,22 +192,22 @@ class AttributeServiceTests : WithTimescaleContainer, WithKafkaContainer { (:id, :entity_id, :attribute_name, :attribute_type, :attribute_value_type, :created_at) """.trimIndent() ) - .bind("id", temporalEntityAttribute.id) - .bind("entity_id", temporalEntityAttribute.entityId) - .bind("attribute_name", temporalEntityAttribute.attributeName) - .bind("attribute_type", temporalEntityAttribute.attributeType.toString()) - .bind("attribute_value_type", temporalEntityAttribute.attributeValueType.toString()) - .bind("created_at", temporalEntityAttribute.createdAt) + .bind("id", attribute.id) + .bind("entity_id", attribute.entityId) + .bind("attribute_name", attribute.attributeName) + .bind("attribute_type", attribute.attributeType.toString()) + .bind("attribute_value_type", attribute.attributeValueType.toString()) + .bind("created_at", attribute.createdAt) .execute() } - private fun newTemporalEntityAttribute( + private fun newAttribute( id: String, attributeName: String, - attributeType: TemporalEntityAttribute.AttributeType, - attributeValueType: TemporalEntityAttribute.AttributeValueType - ): TemporalEntityAttribute = - TemporalEntityAttribute( + attributeType: Attribute.AttributeType, + attributeValueType: Attribute.AttributeValueType + ): Attribute = + Attribute( entityId = toUri(id), attributeName = attributeName, attributeType = attributeType, @@ -217,7 +217,7 @@ class AttributeServiceTests : WithTimescaleContainer, WithKafkaContainer { ) private fun createEntityPayload( - entityPayload: EntityPayload + entity: Entity ): Either = runBlocking { databaseClient.sql( @@ -226,8 +226,8 @@ class AttributeServiceTests : WithTimescaleContainer, WithKafkaContainer { VALUES (:entity_id, :types) """.trimIndent() ) - .bind("entity_id", entityPayload.entityId) - .bind("types", entityPayload.types.toTypedArray()) + .bind("entity_id", entity.entityId) + .bind("types", entity.types.toTypedArray()) .execute() } } diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityTypeServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/discovery/service/EntityTypeServiceTests.kt similarity index 71% rename from search-service/src/test/kotlin/com/egm/stellio/search/service/EntityTypeServiceTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/discovery/service/EntityTypeServiceTests.kt index 7e1adb21e..7c58562af 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityTypeServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/discovery/service/EntityTypeServiceTests.kt @@ -1,14 +1,18 @@ -package com.egm.stellio.search.service +package com.egm.stellio.search.discovery.service import arrow.core.Either -import com.egm.stellio.search.model.* -import com.egm.stellio.search.model.AttributeType +import com.egm.stellio.search.common.util.execute +import com.egm.stellio.search.common.util.toUri +import com.egm.stellio.search.discovery.model.AttributeInfo +import com.egm.stellio.search.discovery.model.AttributeType +import com.egm.stellio.search.discovery.model.EntityType +import com.egm.stellio.search.discovery.model.EntityTypeInfo +import com.egm.stellio.search.entity.model.Attribute +import com.egm.stellio.search.entity.model.Entity import com.egm.stellio.search.support.EMPTY_JSON_PAYLOAD import com.egm.stellio.search.support.WithKafkaContainer import com.egm.stellio.search.support.WithTimescaleContainer import com.egm.stellio.search.support.gimmeEntityPayload -import com.egm.stellio.search.util.execute -import com.egm.stellio.search.util.toUri import com.egm.stellio.shared.model.APIException import com.egm.stellio.shared.model.ResourceNotFoundException import com.egm.stellio.shared.util.* @@ -24,6 +28,7 @@ import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.data.r2dbc.core.R2dbcEntityTemplate +import org.springframework.data.r2dbc.core.delete import org.springframework.r2dbc.core.DatabaseClient import org.springframework.test.context.ActiveProfiles @@ -45,57 +50,53 @@ class EntityTypeServiceTests : WithTimescaleContainer, WithKafkaContainer { private val entityPayload1 = gimmeEntityPayload("urn:ngsi-ld:BeeHive:TESTA", listOf(BEEHIVE_TYPE, SENSOR_TYPE)) private val entityPayload2 = gimmeEntityPayload("urn:ngsi-ld:Sensor:TESTB", listOf(SENSOR_TYPE)) private val entityPayload3 = gimmeEntityPayload("urn:ngsi-ld:Apiary:TESTC", listOf(APIARY_TYPE)) - private val incomingProperty = newTemporalEntityAttribute( + private val incomingProperty = newAttribute( "urn:ngsi-ld:BeeHive:TESTA", INCOMING_PROPERTY, - TemporalEntityAttribute.AttributeType.Property, - TemporalEntityAttribute.AttributeValueType.NUMBER + Attribute.AttributeType.Property, + Attribute.AttributeValueType.NUMBER ) - private val managedByRelationship = newTemporalEntityAttribute( + private val managedByRelationship = newAttribute( "urn:ngsi-ld:BeeHive:TESTA", MANAGED_BY_RELATIONSHIP, - TemporalEntityAttribute.AttributeType.Relationship, - TemporalEntityAttribute.AttributeValueType.STRING + Attribute.AttributeType.Relationship, + Attribute.AttributeValueType.STRING ) - private val locationGeoProperty = newTemporalEntityAttribute( + private val locationGeoProperty = newAttribute( "urn:ngsi-ld:Apiary:TESTC", NGSILD_LOCATION_PROPERTY, - TemporalEntityAttribute.AttributeType.GeoProperty, - TemporalEntityAttribute.AttributeValueType.GEOMETRY + Attribute.AttributeType.GeoProperty, + Attribute.AttributeValueType.GEOMETRY ) - private val outgoingProperty = newTemporalEntityAttribute( + private val outgoingProperty = newAttribute( "urn:ngsi-ld:Sensor:TESTB", OUTGOING_PROPERTY, - TemporalEntityAttribute.AttributeType.Property, - TemporalEntityAttribute.AttributeValueType.GEOMETRY + Attribute.AttributeType.Property, + Attribute.AttributeValueType.GEOMETRY ) - private val luminosityJsonProperty = newTemporalEntityAttribute( + private val luminosityJsonProperty = newAttribute( "urn:ngsi-ld:Sensor:TESTB", LUMINOSITY_JSONPROPERTY, - TemporalEntityAttribute.AttributeType.JsonProperty, - TemporalEntityAttribute.AttributeValueType.JSON + Attribute.AttributeType.JsonProperty, + Attribute.AttributeValueType.JSON ) - private val friendlyNameLanguageProperty = newTemporalEntityAttribute( + private val friendlyNameLanguageProperty = newAttribute( "urn:ngsi-ld:BeeHive:TESTA", FRIENDLYNAME_LANGUAGEPROPERTY, - TemporalEntityAttribute.AttributeType.LanguageProperty, - TemporalEntityAttribute.AttributeValueType.OBJECT + Attribute.AttributeType.LanguageProperty, + Attribute.AttributeValueType.OBJECT ) - private val categoryVocabProperty = newTemporalEntityAttribute( + private val categoryVocabProperty = newAttribute( "urn:ngsi-ld:Apiary:TESTC", CATEGORY_VOCAPPROPERTY, - TemporalEntityAttribute.AttributeType.VocabProperty, - TemporalEntityAttribute.AttributeValueType.ARRAY + Attribute.AttributeType.VocabProperty, + Attribute.AttributeValueType.ARRAY ) @AfterEach - fun clearPreviousTemporalEntityAttributesAndObservations() { - r2dbcEntityTemplate.delete(EntityPayload::class.java) - .all() - .block() - r2dbcEntityTemplate.delete(TemporalEntityAttribute::class.java) - .all() - .block() + fun clearPreviousAttributesAndObservations() { + r2dbcEntityTemplate.delete().from("entity_payload").all().block() + r2dbcEntityTemplate.delete().from("temporal_entity_attribute").all().block() } @BeforeEach @@ -103,13 +104,13 @@ class EntityTypeServiceTests : WithTimescaleContainer, WithKafkaContainer { createEntityPayload(entityPayload1) createEntityPayload(entityPayload2) createEntityPayload(entityPayload3) - createTemporalEntityAttribute(incomingProperty) - createTemporalEntityAttribute(managedByRelationship) - createTemporalEntityAttribute(locationGeoProperty) - createTemporalEntityAttribute(outgoingProperty) - createTemporalEntityAttribute(luminosityJsonProperty) - createTemporalEntityAttribute(friendlyNameLanguageProperty) - createTemporalEntityAttribute(categoryVocabProperty) + createAttribute(incomingProperty) + createAttribute(managedByRelationship) + createAttribute(locationGeoProperty) + createAttribute(outgoingProperty) + createAttribute(luminosityJsonProperty) + createAttribute(friendlyNameLanguageProperty) + createAttribute(categoryVocabProperty) } @Test @@ -121,7 +122,7 @@ class EntityTypeServiceTests : WithTimescaleContainer, WithKafkaContainer { @Test fun `it should return an empty list of types if no entity exists`() = runTest { - clearPreviousTemporalEntityAttributesAndObservations() + clearPreviousAttributesAndObservations() val entityTypes = entityTypeService.getEntityTypeList(listOf(AQUAC_COMPOUND_CONTEXT)) assertThat(entityTypes.typeList).isEmpty() @@ -169,7 +170,7 @@ class EntityTypeServiceTests : WithTimescaleContainer, WithKafkaContainer { @Test fun `it should return an empty list of detailed entity types if no entity exists`() = runTest { - clearPreviousTemporalEntityAttributesAndObservations() + clearPreviousAttributesAndObservations() val entityTypes = entityTypeService.getEntityTypes(listOf(AQUAC_COMPOUND_CONTEXT)) assertThat(entityTypes).isEmpty() @@ -226,8 +227,8 @@ class EntityTypeServiceTests : WithTimescaleContainer, WithKafkaContainer { } } - private fun createTemporalEntityAttribute( - temporalEntityAttribute: TemporalEntityAttribute + private fun createAttribute( + attribute: Attribute ): Either = runBlocking { databaseClient.sql( @@ -238,22 +239,22 @@ class EntityTypeServiceTests : WithTimescaleContainer, WithKafkaContainer { (:id, :entity_id, :attribute_name, :attribute_type, :attribute_value_type, :created_at) """.trimIndent() ) - .bind("id", temporalEntityAttribute.id) - .bind("entity_id", temporalEntityAttribute.entityId) - .bind("attribute_name", temporalEntityAttribute.attributeName) - .bind("attribute_type", temporalEntityAttribute.attributeType.toString()) - .bind("attribute_value_type", temporalEntityAttribute.attributeValueType.toString()) - .bind("created_at", temporalEntityAttribute.createdAt) + .bind("id", attribute.id) + .bind("entity_id", attribute.entityId) + .bind("attribute_name", attribute.attributeName) + .bind("attribute_type", attribute.attributeType.toString()) + .bind("attribute_value_type", attribute.attributeValueType.toString()) + .bind("created_at", attribute.createdAt) .execute() } - private fun newTemporalEntityAttribute( + private fun newAttribute( id: String, attributeName: String, - attributeType: TemporalEntityAttribute.AttributeType, - attributeValueType: TemporalEntityAttribute.AttributeValueType - ): TemporalEntityAttribute = - TemporalEntityAttribute( + attributeType: Attribute.AttributeType, + attributeValueType: Attribute.AttributeValueType + ): Attribute = + Attribute( entityId = id.toUri(), attributeName = attributeName, attributeType = attributeType, @@ -263,7 +264,7 @@ class EntityTypeServiceTests : WithTimescaleContainer, WithKafkaContainer { ) private fun createEntityPayload( - entityPayload: EntityPayload + entity: Entity ): Either = runBlocking { databaseClient.sql( @@ -272,8 +273,8 @@ class EntityTypeServiceTests : WithTimescaleContainer, WithKafkaContainer { VALUES (:entity_id, :types) """.trimIndent() ) - .bind("entity_id", entityPayload.entityId) - .bind("types", entityPayload.types.toTypedArray()) + .bind("entity_id", entity.entityId) + .bind("types", entity.types.toTypedArray()) .execute() } } diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/web/AttributeHandlerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/discovery/web/AttributeHandlerTests.kt similarity index 95% rename from search-service/src/test/kotlin/com/egm/stellio/search/web/AttributeHandlerTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/discovery/web/AttributeHandlerTests.kt index 8cf090d52..0c8ee037c 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/web/AttributeHandlerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/discovery/web/AttributeHandlerTests.kt @@ -1,13 +1,13 @@ -package com.egm.stellio.search.web +package com.egm.stellio.search.discovery.web import arrow.core.left import arrow.core.right -import com.egm.stellio.search.config.SearchProperties -import com.egm.stellio.search.model.AttributeDetails -import com.egm.stellio.search.model.AttributeList -import com.egm.stellio.search.model.AttributeType -import com.egm.stellio.search.model.AttributeTypeInfo -import com.egm.stellio.search.service.AttributeService +import com.egm.stellio.search.common.config.SearchProperties +import com.egm.stellio.search.discovery.model.AttributeDetails +import com.egm.stellio.search.discovery.model.AttributeList +import com.egm.stellio.search.discovery.model.AttributeType +import com.egm.stellio.search.discovery.model.AttributeTypeInfo +import com.egm.stellio.search.discovery.service.AttributeService import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.model.ResourceNotFoundException import com.egm.stellio.shared.util.* diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityTypeHandlerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/discovery/web/EntityTypeHandlerTests.kt similarity index 97% rename from search-service/src/test/kotlin/com/egm/stellio/search/web/EntityTypeHandlerTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/discovery/web/EntityTypeHandlerTests.kt index 4f8425445..d2f3770a6 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityTypeHandlerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/discovery/web/EntityTypeHandlerTests.kt @@ -1,11 +1,11 @@ -package com.egm.stellio.search.web +package com.egm.stellio.search.discovery.web import arrow.core.left import arrow.core.right -import com.egm.stellio.search.config.SearchProperties -import com.egm.stellio.search.model.* -import com.egm.stellio.search.model.AttributeType -import com.egm.stellio.search.service.EntityTypeService +import com.egm.stellio.search.common.config.SearchProperties +import com.egm.stellio.search.discovery.model.* +import com.egm.stellio.search.discovery.model.AttributeType +import com.egm.stellio.search.discovery.service.EntityTypeService import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.model.ResourceNotFoundException import com.egm.stellio.shared.util.* diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/listener/ObservationEventListenerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/entity/listener/ObservationEventListenerTests.kt similarity index 86% rename from search-service/src/test/kotlin/com/egm/stellio/search/listener/ObservationEventListenerTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/entity/listener/ObservationEventListenerTests.kt index ff5c18f1b..6472bdf98 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/listener/ObservationEventListenerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/entity/listener/ObservationEventListenerTests.kt @@ -1,12 +1,12 @@ -package com.egm.stellio.search.listener +package com.egm.stellio.search.entity.listener import arrow.core.right -import com.egm.stellio.search.model.NotUpdatedDetails -import com.egm.stellio.search.model.UpdateOperationResult -import com.egm.stellio.search.model.UpdateResult -import com.egm.stellio.search.model.UpdatedDetails -import com.egm.stellio.search.service.EntityEventService -import com.egm.stellio.search.service.EntityPayloadService +import com.egm.stellio.search.entity.model.NotUpdatedDetails +import com.egm.stellio.search.entity.model.UpdateOperationResult +import com.egm.stellio.search.entity.model.UpdateResult +import com.egm.stellio.search.entity.model.UpdatedDetails +import com.egm.stellio.search.entity.service.EntityEventService +import com.egm.stellio.search.entity.service.EntityService import com.egm.stellio.shared.model.ExpandedEntity import com.egm.stellio.shared.model.NgsiLdEntity import com.egm.stellio.shared.util.BEEHIVE_TYPE @@ -31,7 +31,7 @@ class ObservationEventListenerTests { private lateinit var observationEventListener: ObservationEventListener @MockkBean(relaxed = true) - private lateinit var entityPayloadService: EntityPayloadService + private lateinit var entityService: EntityService @MockkBean(relaxed = true) private lateinit var entityEventService: EntityEventService @@ -44,14 +44,14 @@ class ObservationEventListenerTests { val observationEvent = loadSampleData("events/entity/entityCreateEvent.json") coEvery { - entityPayloadService.createEntity(any(), any(), any()) + entityService.createEntity(any(), any(), any()) } returns Unit.right() coEvery { entityEventService.publishEntityCreateEvent(any(), any(), any()) } returns Job() observationEventListener.dispatchObservationMessage(observationEvent) coVerify { - entityPayloadService.createEntity( + entityService.createEntity( any(), any(), eq("0123456789-1234-5678-987654321") @@ -72,7 +72,7 @@ class ObservationEventListenerTests { val observationEvent = loadSampleData("events/entity/attributeUpdateNumericPropDatasetIdEvent.json") coEvery { - entityPayloadService.partialUpdateAttribute(any(), any(), any()) + entityService.partialUpdateAttribute(any(), any(), any()) } returns UpdateResult( updated = arrayListOf( UpdatedDetails( @@ -91,7 +91,7 @@ class ObservationEventListenerTests { observationEventListener.dispatchObservationMessage(observationEvent) coVerify { - entityPayloadService.partialUpdateAttribute( + entityService.partialUpdateAttribute( expectedEntityId, match { it.first == TEMPERATURE_PROPERTY }, null @@ -118,7 +118,7 @@ class ObservationEventListenerTests { val observationEvent = loadSampleData("events/entity/attributeUpdateNumericPropDatasetIdEvent.json") coEvery { - entityPayloadService.partialUpdateAttribute(any(), any(), any()) + entityService.partialUpdateAttribute(any(), any(), any()) } returns UpdateResult( emptyList(), listOf(NotUpdatedDetails(TEMPERATURE_PROPERTY, "Property does not exist")) @@ -134,7 +134,7 @@ class ObservationEventListenerTests { val observationEvent = loadSampleData("events/entity/attributeAppendNumericPropDatasetIdEvent.json") coEvery { - entityPayloadService.appendAttributes(any(), any(), any(), any()) + entityService.appendAttributes(any(), any(), any(), any()) } returns UpdateResult( listOf( UpdatedDetails( @@ -154,7 +154,7 @@ class ObservationEventListenerTests { observationEventListener.dispatchObservationMessage(observationEvent) coVerify { - entityPayloadService.appendAttributes( + entityService.appendAttributes( expectedEntityId, any(), false, @@ -184,7 +184,7 @@ class ObservationEventListenerTests { val observationEvent = loadSampleData("events/entity/attributeAppendNumericPropDatasetIdEvent.json") coEvery { - entityPayloadService.appendAttributes(any(), any(), any(), any()) + entityService.appendAttributes(any(), any(), any(), any()) } returns UpdateResult( emptyList(), listOf(NotUpdatedDetails(TEMPERATURE_PROPERTY, "Property could not be appended")) @@ -208,7 +208,7 @@ class ObservationEventListenerTests { observationEventListener.dispatchObservationMessage(invalidObservationEvent) } - verify { entityPayloadService wasNot called } + verify { entityService wasNot called } verify { entityEventService wasNot called } } @@ -225,7 +225,7 @@ class ObservationEventListenerTests { observationEventListener.dispatchObservationMessage(invalidObservationEvent) } - verify { entityPayloadService wasNot called } + verify { entityService wasNot called } verify { entityEventService wasNot called } } } diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/model/EntityModelTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/entity/model/EntityModelTests.kt similarity index 83% rename from search-service/src/test/kotlin/com/egm/stellio/search/model/EntityModelTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/entity/model/EntityModelTests.kt index 89d30032a..b6b635300 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/model/EntityModelTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/entity/model/EntityModelTests.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.model +package com.egm.stellio.search.entity.model import com.egm.stellio.search.support.EMPTY_JSON_PAYLOAD import com.egm.stellio.shared.util.AuthContextModel @@ -15,7 +15,7 @@ class EntityModelTests { private val now = Instant.now().atZone(ZoneOffset.UTC) - private val entityPayload = EntityPayload( + private val entity = Entity( entityId = "urn:ngsi-ld:beehive:01".toUri(), types = listOf(BEEHIVE_TYPE), createdAt = now, @@ -25,7 +25,7 @@ class EntityModelTests { @Test fun `it should serialize entityPayload with createdAt and modifiedAt`() { - val serializedEntity = entityPayload.serializeProperties() + val serializedEntity = entity.serializeProperties() assertTrue(serializedEntity.contains(JsonLdUtils.NGSILD_CREATED_AT_PROPERTY)) assertTrue(serializedEntity.contains(JsonLdUtils.NGSILD_MODIFIED_AT_PROPERTY)) assertFalse(serializedEntity.contains(AuthContextModel.AUTH_PROP_SAP)) @@ -34,7 +34,7 @@ class EntityModelTests { @Test fun `it should serialize entityPayload with SAP if present`() { val entityPayloadWithSAP = - entityPayload.copy(specificAccessPolicy = AuthContextModel.SpecificAccessPolicy.AUTH_WRITE) + entity.copy(specificAccessPolicy = AuthContextModel.SpecificAccessPolicy.AUTH_WRITE) val serializedEntity = entityPayloadWithSAP.serializeProperties() assertTrue(serializedEntity.contains(AuthContextModel.AUTH_PROP_SAP)) } diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/model/UpdateResultTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/entity/model/UpdateResultTests.kt similarity index 98% rename from search-service/src/test/kotlin/com/egm/stellio/search/model/UpdateResultTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/entity/model/UpdateResultTests.kt index 69cc52ba3..d46951752 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/model/UpdateResultTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/entity/model/UpdateResultTests.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.model +package com.egm.stellio.search.entity.model import com.egm.stellio.shared.util.toUri import org.junit.jupiter.api.Assertions.assertFalse diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalEntityAttributeServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityAttributeServiceTests.kt similarity index 76% rename from search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalEntityAttributeServiceTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityAttributeServiceTests.kt index 511b12d5b..df19ae787 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalEntityAttributeServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityAttributeServiceTests.kt @@ -1,10 +1,15 @@ -package com.egm.stellio.search.service +package com.egm.stellio.search.entity.service import arrow.core.right -import com.egm.stellio.search.model.* +import com.egm.stellio.search.entity.model.Attribute +import com.egm.stellio.search.entity.model.AttributeMetadata +import com.egm.stellio.search.entity.model.Entity +import com.egm.stellio.search.entity.model.UpdateOperationResult import com.egm.stellio.search.support.EMPTY_JSON_PAYLOAD import com.egm.stellio.search.support.WithKafkaContainer import com.egm.stellio.search.support.WithTimescaleContainer +import com.egm.stellio.search.temporal.model.AttributeInstance +import com.egm.stellio.search.temporal.service.AttributeInstanceService import com.egm.stellio.shared.model.ResourceNotFoundException import com.egm.stellio.shared.model.toNgsiLdAttribute import com.egm.stellio.shared.model.toNgsiLdAttributes @@ -26,6 +31,7 @@ import org.junit.jupiter.api.fail import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.data.r2dbc.core.R2dbcEntityTemplate +import org.springframework.data.r2dbc.core.delete import org.springframework.data.r2dbc.core.insert import org.springframework.test.context.ActiveProfiles import java.time.Instant @@ -34,11 +40,11 @@ import java.time.ZonedDateTime @SpringBootTest @ActiveProfiles("test") -class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaContainer { +class EntityAttributeServiceTests : WithTimescaleContainer, WithKafkaContainer { @Autowired @SpykBean - private lateinit var temporalEntityAttributeService: TemporalEntityAttributeService + private lateinit var entityAttributeService: EntityAttributeService @MockkBean private lateinit var attributeInstanceService: AttributeInstanceService @@ -51,8 +57,8 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon @BeforeEach fun bootstrapEntities() { - r2dbcEntityTemplate.insert().into("entity_payload").using( - EntityPayload( + r2dbcEntityTemplate.insert().into("entity_payload").using( + Entity( entityId = beehiveTestCId, types = listOf(BEEHIVE_TYPE), createdAt = Instant.now().atZone(UTC), @@ -60,8 +66,8 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon ) ).block() - r2dbcEntityTemplate.insert().into("entity_payload").using( - EntityPayload( + r2dbcEntityTemplate.insert().into("entity_payload").using( + Entity( entityId = beehiveTestDId, types = listOf(BEEHIVE_TYPE), createdAt = Instant.now().atZone(UTC), @@ -71,18 +77,14 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon } @AfterEach - fun clearPreviousTemporalEntityAttributesAndObservations() { - r2dbcEntityTemplate.delete(EntityPayload::class.java) - .all() - .block() + fun clearPreviousAttributesAndObservations() { + r2dbcEntityTemplate.delete().from("entity_payload").all().block() r2dbcEntityTemplate.delete(AttributeInstance::class.java) .all() .block() - r2dbcEntityTemplate.delete(TemporalEntityAttribute::class.java) - .all() - .block() + r2dbcEntityTemplate.delete().from("temporal_entity_attribute").all().block() } @Test @@ -91,10 +93,10 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon coEvery { attributeInstanceService.create(any()) } returns Unit.right() - temporalEntityAttributeService.createEntityTemporalReferences(rawEntity, APIC_COMPOUND_CONTEXTS) + entityAttributeService.createAttributes(rawEntity, APIC_COMPOUND_CONTEXTS) - val temporalEntityAttributes = - temporalEntityAttributeService.getForEntity( + val attributes = + entityAttributeService.getForEntity( beehiveTestDId, setOf( INCOMING_PROPERTY, @@ -103,8 +105,8 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon emptySet() ) - assertEquals(2, temporalEntityAttributes.size) - assertTrue(listOf(INCOMING_PROPERTY, OUTGOING_PROPERTY).contains(temporalEntityAttributes[0].attributeName)) + assertEquals(2, attributes.size) + assertTrue(listOf(INCOMING_PROPERTY, OUTGOING_PROPERTY).contains(attributes[0].attributeName)) coVerify(exactly = 6) { attributeInstanceService.create(any()) } } @@ -115,14 +117,14 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon coEvery { attributeInstanceService.create(any()) } returns Unit.right() - temporalEntityAttributeService.createEntityTemporalReferences( + entityAttributeService.createAttributes( rawEntity, APIC_COMPOUND_CONTEXTS, "0123456789-1234-5678-987654321" ).shouldSucceed() - val teas = temporalEntityAttributeService.getForEntity(beehiveTestCId, emptySet(), emptySet()) - assertEquals(4, teas.size) + val attributes = entityAttributeService.getForEntity(beehiveTestCId, emptySet(), emptySet()) + assertEquals(4, attributes.size) coVerify { attributeInstanceService.create( @@ -167,13 +169,13 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon coEvery { attributeInstanceService.create(any()) } returns Unit.right() - temporalEntityAttributeService.createEntityTemporalReferences( + entityAttributeService.createAttributes( rawEntity, APIC_COMPOUND_CONTEXTS ).shouldSucceed() - val teas = temporalEntityAttributeService.getForEntity(beehiveTestCId, emptySet(), emptySet()) - assertEquals(2, teas.size) + val attributes = entityAttributeService.getForEntity(beehiveTestCId, emptySet(), emptySet()) + assertEquals(2, attributes.size) coVerify { attributeInstanceService.create( @@ -223,18 +225,18 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon } throws RuntimeException("Unexpected DB error!") assertThrows("it should have thrown a RuntimeException") { - temporalEntityAttributeService.createEntityTemporalReferences( + entityAttributeService.createAttributes( rawEntity, APIC_COMPOUND_CONTEXTS ).shouldSucceed() } - val teas = temporalEntityAttributeService.getForEntity( + val attributes = entityAttributeService.getForEntity( "urn:ngsi-ld:BeeHive:TESTC".toUri(), emptySet(), emptySet() ) - assertTrue(teas.isEmpty()) + assertTrue(attributes.isEmpty()) } @Test @@ -243,10 +245,10 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon coEvery { attributeInstanceService.create(any()) } returns Unit.right() - temporalEntityAttributeService.createEntityTemporalReferences(rawEntity, APIC_COMPOUND_CONTEXTS) + entityAttributeService.createAttributes(rawEntity, APIC_COMPOUND_CONTEXTS) .shouldSucceed() - val temporalEntityAttribute = temporalEntityAttributeService.getForEntityAndAttribute( + val attribute = entityAttributeService.getForEntityAndAttribute( beehiveTestCId, INCOMING_PROPERTY ).shouldSucceedAndResult() @@ -255,16 +257,16 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon val newProperty = loadSampleData("fragments/beehive_new_incoming_property.json") val expandedAttribute = expandAttribute(newProperty, APIC_COMPOUND_CONTEXTS) val newNgsiLdProperty = expandedAttribute.toNgsiLdAttribute().shouldSucceedAndResult() - temporalEntityAttributeService.replaceAttribute( - temporalEntityAttribute, + entityAttributeService.replaceAttribute( + attribute, newNgsiLdProperty, AttributeMetadata( null, "It's a string now", null, - TemporalEntityAttribute.AttributeValueType.STRING, + Attribute.AttributeValueType.STRING, null, - TemporalEntityAttribute.AttributeType.Property, + Attribute.AttributeType.Property, ZonedDateTime.parse("2022-12-24T14:01:22.066Z") ), createdAt, @@ -272,12 +274,12 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon null ).shouldSucceed() - temporalEntityAttributeService.getForEntityAndAttribute( + entityAttributeService.getForEntityAndAttribute( beehiveTestCId, INCOMING_PROPERTY ).shouldSucceedWith { - assertEquals(TemporalEntityAttribute.AttributeType.Property, it.attributeType) - assertEquals(TemporalEntityAttribute.AttributeValueType.STRING, it.attributeValueType) + assertEquals(Attribute.AttributeType.Property, it.attributeType) + assertEquals(Attribute.AttributeValueType.STRING, it.attributeValueType) assertEquals(beehiveTestCId, it.entityId) assertJsonPayloadsAreEqual(serializeObject(expandedAttribute.second[0]), it.payload.asString()) assertTrue(it.createdAt.isBefore(createdAt)) @@ -291,10 +293,10 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon coEvery { attributeInstanceService.create(any()) } returns Unit.right() - temporalEntityAttributeService.createEntityTemporalReferences(rawEntity, APIC_COMPOUND_CONTEXTS) + entityAttributeService.createAttributes(rawEntity, APIC_COMPOUND_CONTEXTS) .shouldSucceed() - val temporalEntityAttribute = temporalEntityAttributeService.getForEntityAndAttribute( + val attribute = entityAttributeService.getForEntityAndAttribute( beehiveTestCId, INCOMING_PROPERTY ).shouldSucceedAndResult() @@ -302,16 +304,16 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon val mergedAt = ngsiLdDateTime() val propertyToMerge = loadSampleData("fragments/beehive_mergeAttribute.json") val expandedAttribute = expandAttribute(propertyToMerge, APIC_COMPOUND_CONTEXTS) - temporalEntityAttributeService.mergeAttribute( - temporalEntityAttribute, + entityAttributeService.mergeAttribute( + attribute, INCOMING_PROPERTY, AttributeMetadata( null, "It's a string now", null, - TemporalEntityAttribute.AttributeValueType.STRING, + Attribute.AttributeValueType.STRING, null, - TemporalEntityAttribute.AttributeType.Property, + Attribute.AttributeType.Property, ZonedDateTime.parse("2022-12-24T14:01:22.066Z") ), mergedAt, @@ -337,12 +339,12 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon APIC_COMPOUND_CONTEXTS ) - temporalEntityAttributeService.getForEntityAndAttribute( + entityAttributeService.getForEntityAndAttribute( beehiveTestCId, INCOMING_PROPERTY ).shouldSucceedWith { - assertEquals(TemporalEntityAttribute.AttributeType.Property, it.attributeType) - assertEquals(TemporalEntityAttribute.AttributeValueType.STRING, it.attributeValueType) + assertEquals(Attribute.AttributeType.Property, it.attributeType) + assertEquals(Attribute.AttributeValueType.STRING, it.attributeValueType) assertJsonPayloadsAreEqual(serializeObject(expectedMergedPayload.second[0]), it.payload.asString()) assertTrue(it.createdAt.isBefore(mergedAt)) assertEquals(mergedAt, it.modifiedAt) @@ -355,14 +357,14 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon coEvery { attributeInstanceService.create(any()) } returns Unit.right() - temporalEntityAttributeService.createEntityTemporalReferences(rawEntity, APIC_COMPOUND_CONTEXTS) + entityAttributeService.createAttributes(rawEntity, APIC_COMPOUND_CONTEXTS) .shouldSucceed() val createdAt = ngsiLdDateTime() val attributesToMerge = loadSampleData("fragments/beehive_mergeAttributes.json") val expandedAttributes = JsonLdUtils.expandAttributes(attributesToMerge, APIC_COMPOUND_CONTEXTS) val ngsiLdAttributes = expandedAttributes.toMap().toNgsiLdAttributes().shouldSucceedAndResult() - temporalEntityAttributeService.mergeEntityAttributes( + entityAttributeService.mergeAttributes( beehiveTestCId, ngsiLdAttributes, expandedAttributes, @@ -379,12 +381,12 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon assertTrue(newAttributes.containsAll(listOf(OUTGOING_PROPERTY, TEMPERATURE_PROPERTY))) } - val teas = temporalEntityAttributeService.getForEntity( + val attributes = entityAttributeService.getForEntity( beehiveTestCId, emptySet(), emptySet() ) - assertEquals(6, teas.size) + assertEquals(6, attributes.size) coVerify { attributeInstanceService.create( match { @@ -421,7 +423,7 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon coEvery { attributeInstanceService.create(any()) } returns Unit.right() - temporalEntityAttributeService.createEntityTemporalReferences(rawEntity, APIC_COMPOUND_CONTEXTS) + entityAttributeService.createAttributes(rawEntity, APIC_COMPOUND_CONTEXTS) .shouldSucceed() val createdAt = ngsiLdDateTime() @@ -429,7 +431,7 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon val propertyToMerge = loadSampleData("fragments/beehive_mergeAttribute_without_observedAt.json") val expandedAttributes = JsonLdUtils.expandAttributes(propertyToMerge, APIC_COMPOUND_CONTEXTS) val ngsiLdAttributes = expandedAttributes.toMap().toNgsiLdAttributes().shouldSucceedAndResult() - temporalEntityAttributeService.mergeEntityAttributes( + entityAttributeService.mergeAttributes( beehiveTestCId, ngsiLdAttributes, expandedAttributes, @@ -460,7 +462,7 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon coEvery { attributeInstanceService.create(any()) } returns Unit.right() - temporalEntityAttributeService.createEntityTemporalReferences(rawEntity, APIC_COMPOUND_CONTEXTS) + entityAttributeService.createAttributes(rawEntity, APIC_COMPOUND_CONTEXTS) .shouldSucceed() val replacedAt = ngsiLdDateTime() @@ -468,7 +470,7 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon val expandedAttribute = expandAttribute(propertyToReplace, APIC_COMPOUND_CONTEXTS) val ngsiLdAttribute = expandedAttribute.toNgsiLdAttribute().shouldSucceedAndResult() - temporalEntityAttributeService.replaceEntityAttribute( + entityAttributeService.replaceAttribute( beehiveTestCId, ngsiLdAttribute, expandedAttribute, @@ -476,12 +478,12 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon null ).shouldSucceed() - temporalEntityAttributeService.getForEntityAndAttribute( + entityAttributeService.getForEntityAndAttribute( beehiveTestCId, INCOMING_PROPERTY ).shouldSucceedWith { - assertEquals(TemporalEntityAttribute.AttributeType.Property, it.attributeType) - assertEquals(TemporalEntityAttribute.AttributeValueType.STRING, it.attributeValueType) + assertEquals(Attribute.AttributeType.Property, it.attributeType) + assertEquals(Attribute.AttributeValueType.STRING, it.attributeValueType) assertJsonPayloadsAreEqual(serializeObject(expandedAttribute.second[0]), it.payload.asString()) assertTrue(it.createdAt.isBefore(replacedAt)) assertEquals(replacedAt, it.modifiedAt) @@ -494,7 +496,7 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon coEvery { attributeInstanceService.create(any()) } returns Unit.right() - temporalEntityAttributeService.createEntityTemporalReferences(rawEntity, APIC_COMPOUND_CONTEXTS) + entityAttributeService.createAttributes(rawEntity, APIC_COMPOUND_CONTEXTS) .shouldSucceed() val replacedAt = ngsiLdDateTime() @@ -502,7 +504,7 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon val expandedAttribute = expandAttribute(propertyToReplace, APIC_COMPOUND_CONTEXTS) val ngsiLdAttribute = expandedAttribute.toNgsiLdAttribute().shouldSucceedAndResult() - temporalEntityAttributeService.replaceEntityAttribute( + entityAttributeService.replaceAttribute( beehiveTestCId, ngsiLdAttribute, expandedAttribute, @@ -517,28 +519,28 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon } @Test - fun `it should return the temporalEntityAttributeId of a given entityId and attributeName`() = runTest { + fun `it should return the attributeId of a given entityId and attributeName`() = runTest { val rawEntity = loadSampleData() coEvery { attributeInstanceService.create(any()) } returns Unit.right() - temporalEntityAttributeService.createEntityTemporalReferences(rawEntity, APIC_COMPOUND_CONTEXTS) + entityAttributeService.createAttributes(rawEntity, APIC_COMPOUND_CONTEXTS) - temporalEntityAttributeService.getForEntityAndAttribute( + entityAttributeService.getForEntityAndAttribute( beehiveTestCId, INCOMING_PROPERTY ).shouldSucceedWith { assertNotNull(it) } } @Test - fun `it should return the temporalEntityAttributeId of a given entityId attributeName and datasetId`() = runTest { + fun `it should return the attributeId of a given entityId attributeName and datasetId`() = runTest { val rawEntity = loadSampleData("beehive_multi_instance_property.jsonld") coEvery { attributeInstanceService.create(any()) } returns Unit.right() - temporalEntityAttributeService.createEntityTemporalReferences(rawEntity, APIC_COMPOUND_CONTEXTS) + entityAttributeService.createAttributes(rawEntity, APIC_COMPOUND_CONTEXTS) - temporalEntityAttributeService.getForEntityAndAttribute( + entityAttributeService.getForEntityAndAttribute( beehiveTestCId, INCOMING_PROPERTY, "urn:ngsi-ld:Dataset:01234".toUri() @@ -546,14 +548,14 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon } @Test - fun `it should not return a temporalEntityAttributeId if the datasetId is unknown`() = runTest { + fun `it should not return a attributeId if the datasetId is unknown`() = runTest { val rawEntity = loadSampleData("beehive_multi_instance_property.jsonld") coEvery { attributeInstanceService.create(any()) } returns Unit.right() - temporalEntityAttributeService.createEntityTemporalReferences(rawEntity, APIC_COMPOUND_CONTEXTS) + entityAttributeService.createAttributes(rawEntity, APIC_COMPOUND_CONTEXTS) - temporalEntityAttributeService.getForEntityAndAttribute( + entityAttributeService.getForEntityAndAttribute( beehiveTestCId, INCOMING_PROPERTY, "urn:ngsi-ld:Dataset:Unknown".toUri() @@ -569,9 +571,9 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon coEvery { attributeInstanceService.create(any()) } returns Unit.right() coEvery { attributeInstanceService.deleteInstancesOfAttribute(any(), any(), any()) } returns Unit.right() - temporalEntityAttributeService.createEntityTemporalReferences(rawEntity, APIC_COMPOUND_CONTEXTS) + entityAttributeService.createAttributes(rawEntity, APIC_COMPOUND_CONTEXTS) - temporalEntityAttributeService.deleteTemporalAttribute( + entityAttributeService.deleteAttribute( beehiveTestDId, INCOMING_PROPERTY, null @@ -581,7 +583,7 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon attributeInstanceService.deleteInstancesOfAttribute(eq(beehiveTestDId), eq(INCOMING_PROPERTY), null) } - temporalEntityAttributeService.getForEntityAndAttribute(beehiveTestDId, INCOMING_PROPERTY) + entityAttributeService.getForEntityAndAttribute(beehiveTestDId, INCOMING_PROPERTY) .shouldFail { assertInstanceOf(ResourceNotFoundException::class.java, it) } } @@ -592,9 +594,9 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon coEvery { attributeInstanceService.create(any()) } returns Unit.right() coEvery { attributeInstanceService.deleteAllInstancesOfAttribute(any(), any()) } returns Unit.right() - temporalEntityAttributeService.createEntityTemporalReferences(rawEntity, APIC_COMPOUND_CONTEXTS) + entityAttributeService.createAttributes(rawEntity, APIC_COMPOUND_CONTEXTS) - temporalEntityAttributeService.deleteTemporalAttribute( + entityAttributeService.deleteAttribute( beehiveTestCId, INCOMING_PROPERTY, null, @@ -605,7 +607,7 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon attributeInstanceService.deleteAllInstancesOfAttribute(eq(beehiveTestCId), eq(INCOMING_PROPERTY)) } - temporalEntityAttributeService.getForEntityAndAttribute(beehiveTestCId, INCOMING_PROPERTY) + entityAttributeService.getForEntityAndAttribute(beehiveTestCId, INCOMING_PROPERTY) .shouldFail { assertInstanceOf(ResourceNotFoundException::class.java, it) } } @@ -615,9 +617,9 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon coEvery { attributeInstanceService.create(any()) } returns Unit.right() - temporalEntityAttributeService.createEntityTemporalReferences(rawEntity, APIC_COMPOUND_CONTEXTS) + entityAttributeService.createAttributes(rawEntity, APIC_COMPOUND_CONTEXTS) - temporalEntityAttributeService.checkEntityAndAttributeExistence(beehiveTestCId, INCOMING_PROPERTY) + entityAttributeService.checkEntityAndAttributeExistence(beehiveTestCId, INCOMING_PROPERTY) .shouldSucceed() } @@ -627,9 +629,9 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon coEvery { attributeInstanceService.create(any()) } returns Unit.right() - temporalEntityAttributeService.createEntityTemporalReferences(rawEntity, APIC_COMPOUND_CONTEXTS) + entityAttributeService.createAttributes(rawEntity, APIC_COMPOUND_CONTEXTS) - val result = temporalEntityAttributeService.checkEntityAndAttributeExistence(beehiveTestCId, "speed") + val result = entityAttributeService.checkEntityAndAttributeExistence(beehiveTestCId, "speed") result.fold( { assertEquals("Attribute speed (default datasetId) was not found", it.message) }, @@ -639,7 +641,7 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon @Test fun `it should return a left entity not found if entity does not exist`() = runTest { - temporalEntityAttributeService.checkEntityAndAttributeExistence( + entityAttributeService.checkEntityAndAttributeExistence( "urn:ngsi-ld:Entity:01".toUri(), "speed" ).fold( @@ -654,18 +656,18 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon coEvery { attributeInstanceService.create(any()) } returns Unit.right() - temporalEntityAttributeService.createEntityTemporalReferences(rawEntity, APIC_COMPOUND_CONTEXTS) + entityAttributeService.createAttributes(rawEntity, APIC_COMPOUND_CONTEXTS) .shouldSucceed() - val temporalEntityAttributes = - temporalEntityAttributeService.getForEntity( + val attributes = + entityAttributeService.getForEntity( beehiveTestCId, emptySet(), setOf("urn:ngsi-ld:Dataset:01234") ) - assertEquals(1, temporalEntityAttributes.size) - assertEquals(INCOMING_PROPERTY, temporalEntityAttributes[0].attributeName) - assertEquals("urn:ngsi-ld:Dataset:01234", temporalEntityAttributes[0].datasetId.toString()) + assertEquals(1, attributes.size) + assertEquals(INCOMING_PROPERTY, attributes[0].attributeName) + assertEquals("urn:ngsi-ld:Dataset:01234", attributes[0].datasetId.toString()) } } diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityEventServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityEventServiceTests.kt similarity index 88% rename from search-service/src/test/kotlin/com/egm/stellio/search/service/EntityEventServiceTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityEventServiceTests.kt index 851f242ef..5294a7f1d 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityEventServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityEventServiceTests.kt @@ -1,10 +1,10 @@ -package com.egm.stellio.search.service +package com.egm.stellio.search.entity.service import arrow.core.right -import com.egm.stellio.search.model.EntityPayload -import com.egm.stellio.search.model.UpdateOperationResult -import com.egm.stellio.search.model.UpdateResult -import com.egm.stellio.search.model.UpdatedDetails +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.support.EMPTY_PAYLOAD import com.egm.stellio.shared.model.* import com.egm.stellio.shared.util.AQUAC_COMPOUND_CONTEXT @@ -35,7 +35,7 @@ class EntityEventServiceTests { private lateinit var kafkaTemplate: KafkaTemplate @MockkBean(relaxed = true) - private lateinit var entityPayloadService: EntityPayloadService + private lateinit var entityQueryService: EntityQueryService private val breedingServiceUri = "urn:ngsi-ld:BreedingService:0214".toUri() private val breedingServiceType = "https://ontology.eglobalmark.com/aquac#BreedingService" @@ -115,21 +115,21 @@ class EntityEventServiceTests { @Test fun `it should publish an ENTITY_DELETE event`() = runTest { - val entityPayload = mockk(relaxed = true) { + val entity = mockk(relaxed = true) { every { entityId } returns breedingServiceUri } every { kafkaTemplate.send(any(), any(), any()) } returns CompletableFuture() - entityEventService.publishEntityDeleteEvent(null, entityPayload).join() + entityEventService.publishEntityDeleteEvent(null, entity).join() verify { kafkaTemplate.send("cim.entity._CatchAll", breedingServiceUri.toString(), any()) } } @Test fun `it should publish a single ATTRIBUTE_APPEND event if an attribute was appended`() = runTest { - val entityPayload = mockk(relaxed = true) - coEvery { entityPayloadService.retrieve(breedingServiceUri) } returns entityPayload.right() - every { entityPayload.types } returns listOf(breedingServiceType) + val entity = mockk(relaxed = true) + coEvery { entityQueryService.retrieve(breedingServiceUri) } returns entity.right() + every { entity.types } returns listOf(breedingServiceType) val expandedAttribute = expandAttribute(fishNumberTerm, fishNumberAttributeFragment, listOf(AQUAC_COMPOUND_CONTEXT)) @@ -162,9 +162,9 @@ class EntityEventServiceTests { @Test fun `it should publish a single ATTRIBUTE_REPLACE event if an attribute was replaced`() = runTest { - val entityPayload = mockk(relaxed = true) - coEvery { entityPayloadService.retrieve(breedingServiceUri) } returns entityPayload.right() - every { entityPayload.types } returns listOf(breedingServiceType) + val entity = mockk(relaxed = true) + coEvery { entityQueryService.retrieve(breedingServiceUri) } returns entity.right() + every { entity.types } returns listOf(breedingServiceType) val expandedAttribute = expandAttribute(fishNumberTerm, fishNumberAttributeFragment, listOf(AQUAC_COMPOUND_CONTEXT)) @@ -198,7 +198,7 @@ class EntityEventServiceTests { @Test fun `it should publish ATTRIBUTE_APPEND and ATTRIBUTE_REPLACE events if attributes were appended and replaced`() = runTest { - val entityPayload = mockk(relaxed = true) + val entity = mockk(relaxed = true) val attributesPayload = """ { @@ -215,8 +215,8 @@ class EntityEventServiceTests { emptyList() ) - coEvery { entityPayloadService.retrieve(breedingServiceUri) } returns entityPayload.right() - every { entityPayload.types } returns listOf(breedingServiceType) + coEvery { entityQueryService.retrieve(breedingServiceUri) } returns entity.right() + every { entity.types } returns listOf(breedingServiceType) entityEventService.publishAttributeChangeEvents( null, @@ -263,7 +263,7 @@ class EntityEventServiceTests { @Test fun `it should publish ATTRIBUTE_REPLACE events if two attributes are replaced`() = runTest { - val entityPayload = mockk(relaxed = true) + val entity = mockk(relaxed = true) val attributesPayload = """ { @@ -280,8 +280,8 @@ class EntityEventServiceTests { notUpdated = arrayListOf() ) - coEvery { entityPayloadService.retrieve(breedingServiceUri) } returns entityPayload.right() - every { entityPayload.types } returns listOf(breedingServiceType) + coEvery { entityQueryService.retrieve(breedingServiceUri) } returns entity.right() + every { entity.types } returns listOf(breedingServiceType) entityEventService.publishAttributeChangeEvents( null, @@ -315,7 +315,7 @@ class EntityEventServiceTests { @Test fun `it should publish ATTRIBUTE_REPLACE events if a multi-attribute is replaced`() = runTest { - val entityPayload = mockk(relaxed = true) + val entity = mockk(relaxed = true) val fishNameAttributeFragment2 = """ { @@ -339,8 +339,8 @@ class EntityEventServiceTests { notUpdated = arrayListOf() ) - coEvery { entityPayloadService.retrieve(breedingServiceUri) } returns entityPayload.right() - every { entityPayload.types } returns listOf(breedingServiceType) + coEvery { entityQueryService.retrieve(breedingServiceUri) } returns entity.right() + every { entity.types } returns listOf(breedingServiceType) entityEventService.publishAttributeChangeEvents( null, @@ -376,7 +376,7 @@ class EntityEventServiceTests { @Test fun `it should publish ATTRIBUTE_UPDATE event if an attribute is updated`() = runTest { - val entityPayload = mockk(relaxed = true) + val entity = mockk(relaxed = true) val expandedAttribute = expandAttribute( fishNameTerm, @@ -387,8 +387,8 @@ class EntityEventServiceTests { UpdatedDetails(fishNameProperty, fishName1DatasetUri, UpdateOperationResult.UPDATED) ) - coEvery { entityPayloadService.retrieve(breedingServiceUri) } returns entityPayload.right() - every { entityPayload.types } returns listOf(breedingServiceType) + coEvery { entityQueryService.retrieve(breedingServiceUri) } returns entity.right() + every { entity.types } returns listOf(breedingServiceType) entityEventService.publishAttributeChangeEvents( null, @@ -416,10 +416,10 @@ class EntityEventServiceTests { @Test fun `it should publish ATTRIBUTE_DELETE_ALL_INSTANCE event if all instances of an attribute are deleted`() = runTest { - val entityPayload = mockk(relaxed = true) + val entity = mockk(relaxed = true) - coEvery { entityPayloadService.retrieve(breedingServiceUri) } returns entityPayload.right() - every { entityPayload.types } returns listOf(breedingServiceType) + coEvery { entityQueryService.retrieve(breedingServiceUri) } returns entity.right() + every { entity.types } returns listOf(breedingServiceType) entityEventService.publishAttributeDeleteEvent( null, @@ -446,10 +446,10 @@ class EntityEventServiceTests { @Test fun `it should publish ATTRIBUTE_DELETE event if an instance of an attribute is deleted`() = runTest { - val entityPayload = mockk(relaxed = true) + val entity = mockk(relaxed = true) - coEvery { entityPayloadService.retrieve(breedingServiceUri) } returns entityPayload.right() - every { entityPayload.types } returns listOf(breedingServiceType) + coEvery { entityQueryService.retrieve(breedingServiceUri) } returns entity.right() + every { entity.types } returns listOf(breedingServiceType) entityEventService.publishAttributeDeleteEvent( null, diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityOperationServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityOperationServiceTests.kt similarity index 55% rename from search-service/src/test/kotlin/com/egm/stellio/search/service/EntityOperationServiceTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityOperationServiceTests.kt index 27bbc5cbd..f33b46d59 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityOperationServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityOperationServiceTests.kt @@ -1,29 +1,30 @@ -package com.egm.stellio.search.service +package com.egm.stellio.search.entity.service import arrow.core.left import arrow.core.right -import com.egm.stellio.search.authorization.AuthorizationService -import com.egm.stellio.search.model.EMPTY_UPDATE_RESULT -import com.egm.stellio.search.model.EntityPayload -import com.egm.stellio.search.model.NotUpdatedDetails -import com.egm.stellio.search.model.UpdateResult -import com.egm.stellio.search.web.BatchEntityError -import com.egm.stellio.search.web.BatchEntitySuccess +import com.egm.stellio.search.entity.model.EMPTY_UPDATE_RESULT +import com.egm.stellio.search.entity.model.NotUpdatedDetails +import com.egm.stellio.search.entity.model.UpdateResult +import com.egm.stellio.search.entity.web.BatchEntityError +import com.egm.stellio.search.entity.web.BatchEntitySuccess +import com.egm.stellio.search.entity.web.BatchOperationResult +import com.egm.stellio.search.entity.web.JsonLdNgsiLdEntity import com.egm.stellio.shared.model.BadRequestDataException import com.egm.stellio.shared.model.ExpandedEntity import com.egm.stellio.shared.model.InternalErrorException import com.egm.stellio.shared.model.NgsiLdEntity +import com.egm.stellio.shared.util.ENTITIY_CREATION_FORBIDDEN_MESSAGE +import com.egm.stellio.shared.util.ENTITY_ADMIN_FORBIDDEN_MESSAGE import com.egm.stellio.shared.util.Sub import com.egm.stellio.shared.util.toUri import com.ninjasquad.springmockk.MockkBean +import com.ninjasquad.springmockk.SpykBean import io.mockk.* -import kotlinx.coroutines.Job import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.test.context.ActiveProfiles @@ -32,18 +33,15 @@ import org.springframework.test.context.ActiveProfiles class EntityOperationServiceTests { @MockkBean - private lateinit var entityPayloadService: EntityPayloadService + private lateinit var entityService: EntityService @MockkBean(relaxed = true) - private lateinit var temporalEntityAttributeService: TemporalEntityAttributeService + private lateinit var entityAttributeService: EntityAttributeService @MockkBean - private lateinit var authorizationService: AuthorizationService + private lateinit var entityQueryService: EntityQueryService - @MockkBean - private lateinit var entityEventService: EntityEventService - - @Autowired + @SpykBean private lateinit var entityOperationService: EntityOperationService val firstEntityURI = "urn:ngsi-ld:Device:HCMR-AQUABOX1".toUri() @@ -74,7 +72,7 @@ class EntityOperationServiceTests { @Test fun `splitEntitiesByExistence should split entities per existence`() = runTest { coEvery { - entityPayloadService.filterExistingEntitiesAsIds(listOf(firstEntityURI, secondEntityURI)) + entityQueryService.filterExistingEntitiesAsIds(listOf(firstEntityURI, secondEntityURI)) } returns listOf(firstEntityURI) val (exist, doNotExist) = entityOperationService.splitEntitiesByExistence( @@ -91,7 +89,7 @@ class EntityOperationServiceTests { @Test fun `splitEntitiesByExistence should split entities per existence with ids`() = runTest { coEvery { - entityPayloadService.filterExistingEntitiesAsIds(listOf(firstEntityURI, secondEntityURI)) + entityQueryService.filterExistingEntitiesAsIds(listOf(firstEntityURI, secondEntityURI)) } returns listOf(firstEntityURI) val (exist, doNotExist) = @@ -127,12 +125,11 @@ class EntityOperationServiceTests { @Test fun `processEntities should count as error a process which raises a BadRequestDataException`() = runTest { coEvery { - entityPayloadService.appendAttributes(firstEntityURI, any(), any(), any()) + entityService.appendAttributes(firstEntityURI, any(), any(), any()) } returns EMPTY_UPDATE_RESULT.right() coEvery { - entityPayloadService.appendAttributes(secondEntityURI, any(), any(), any()) + entityService.appendAttributes(secondEntityURI, any(), any(), any()) } returns BadRequestDataException("error").left() - coEvery { entityEventService.publishAttributeChangeEvents(any(), any(), any(), any(), any()) } returns Job() val batchOperationResult = entityOperationService.processEntities( @@ -153,24 +150,6 @@ class EntityOperationServiceTests { listOf(BatchEntityError(secondEntityURI, arrayListOf("error"))), batchOperationResult.errors ) - coVerify { - entityEventService.publishAttributeChangeEvents( - sub, - eq(firstEntityURI), - any(), - match { it.isSuccessful() }, - any() - ) - } - coVerify { - entityEventService.publishAttributeChangeEvents( - sub, - eq(secondEntityURI), - any(), - any(), - any() - ) wasNot Called - } } @Test @@ -183,12 +162,11 @@ class EntityOperationServiceTests { ) ) coEvery { - entityPayloadService.appendAttributes(firstEntityURI, any(), any(), any()) + entityService.appendAttributes(firstEntityURI, any(), any(), any()) } returns EMPTY_UPDATE_RESULT.right() coEvery { - entityPayloadService.appendAttributes(secondEntityURI, any(), any(), any()) + entityService.appendAttributes(secondEntityURI, any(), any(), any()) } returns updateResult.right() - coEvery { entityEventService.publishAttributeChangeEvents(any(), any(), any(), any(), any()) } returns Job() val batchOperationResult = entityOperationService.processEntities( listOf( @@ -217,8 +195,7 @@ class EntityOperationServiceTests { @Test fun `batch create should ask to create all provided entities`() = runTest { - coEvery { entityPayloadService.createEntity(any(), any(), any()) } returns Unit.right() - coEvery { entityEventService.publishEntityCreateEvent(any(), any(), any()) } returns Job() + coEvery { entityService.createEntity(any(), any(), any()) } returns Unit.right() val batchOperationResult = entityOperationService.create( listOf( @@ -235,23 +212,19 @@ class EntityOperationServiceTests { assertTrue(batchOperationResult.errors.isEmpty()) coVerify { - entityPayloadService.createEntity(firstEntity, firstExpandedEntity, sub) + entityService.createEntity(firstEntity, firstExpandedEntity, sub) } coVerify { - entityPayloadService.createEntity(secondEntity, secondExpandedEntity, sub) - } - coVerify(exactly = 2) { - entityEventService.publishEntityCreateEvent(any(), any(), any()) + entityService.createEntity(secondEntity, secondExpandedEntity, sub) } } @Test fun `batch create should ask to create entities and transmit back any error`() = runTest { - coEvery { entityPayloadService.createEntity(firstEntity, any(), any()) } returns Unit.right() + coEvery { entityService.createEntity(firstEntity, any(), any()) } returns Unit.right() coEvery { - entityPayloadService.createEntity(secondEntity, any(), any()) + entityService.createEntity(secondEntity, any(), any()) } returns BadRequestDataException("Invalid entity").left() - coEvery { entityEventService.publishEntityCreateEvent(any(), any(), any()) } returns Job() val batchOperationResult = entityOperationService.create( listOf( @@ -269,16 +242,15 @@ class EntityOperationServiceTests { batchOperationResult.errors ) coVerify(exactly = 1) { - entityEventService.publishEntityCreateEvent(any(), any(), any()) + entityService.createEntity(secondEntity, any(), any()) } } @Test fun `batch update should ask to update attributes of entities`() = runTest { coEvery { - entityPayloadService.appendAttributes(any(), any(), any(), any()) + entityService.appendAttributes(any(), any(), any(), any()) } returns EMPTY_UPDATE_RESULT.right() - coEvery { entityEventService.publishAttributeChangeEvents(any(), any(), any(), any(), any()) } returns Job() val batchOperationResult = entityOperationService.update( listOf( @@ -295,25 +267,21 @@ class EntityOperationServiceTests { ) coVerify { - entityPayloadService.appendAttributes(eq(firstEntityURI), any(), false, sub) + entityService.appendAttributes(eq(firstEntityURI), any(), false, sub) } coVerify { - entityPayloadService.appendAttributes(eq(secondEntityURI), any(), false, sub) - } - coVerify(exactly = 2) { - entityEventService.publishAttributeChangeEvents(any(), any(), any(), any(), any()) + entityService.appendAttributes(eq(secondEntityURI), any(), false, sub) } } @Test fun `batch replace should ask to replace entities`() = runTest { coEvery { - temporalEntityAttributeService.deleteTemporalAttributesOfEntity(any()) + entityAttributeService.deleteAttributes(any()) } returns Unit.right() coEvery { - entityPayloadService.appendAttributes(any(), any(), any(), any()) + entityService.appendAttributes(any(), any(), any(), any()) } returns EMPTY_UPDATE_RESULT.right() - coEvery { entityEventService.publishEntityReplaceEvent(any(), any(), any()) } returns Job() val batchOperationResult = entityOperationService.replace( listOf( @@ -329,29 +297,24 @@ class EntityOperationServiceTests { ) assertTrue(batchOperationResult.errors.isEmpty()) - coVerify { temporalEntityAttributeService.deleteTemporalAttributesOfEntity(firstEntityURI) } - coVerify { temporalEntityAttributeService.deleteTemporalAttributesOfEntity(secondEntityURI) } + coVerify { entityAttributeService.deleteAttributes(firstEntityURI) } + coVerify { entityAttributeService.deleteAttributes(secondEntityURI) } coVerify { - entityPayloadService.appendAttributes(eq(firstEntityURI), any(), false, sub) + entityService.appendAttributes(eq(firstEntityURI), any(), false, sub) } coVerify { - entityPayloadService.appendAttributes(eq(secondEntityURI), any(), false, sub) - } - coVerify(exactly = 2) { - entityEventService.publishEntityReplaceEvent(any(), any(), any()) + entityService.appendAttributes(eq(secondEntityURI), any(), false, sub) } } @Test fun `batch delete should return the list of deleted entity ids when deletion is successful`() = runTest { - coEvery { entityPayloadService.deleteEntity(any()) } returns mockkClass(EntityPayload::class).right() - coEvery { authorizationService.removeRightsOnEntity(any()) } returns Unit.right() - coEvery { entityEventService.publishEntityDeleteEvent(any(), any()) } returns Job() + coEvery { entityService.deleteEntity(any(), any()) } returns Unit.right() val batchOperationResult = entityOperationService.delete( - setOf( - mockkClass(EntityPayload::class) { every { entityId } returns firstEntityURI }, - mockkClass(EntityPayload::class) { every { entityId } returns secondEntityURI }, + listOf( + firstEntityURI, + secondEntityURI, ), sub ) @@ -363,32 +326,23 @@ class EntityOperationServiceTests { assertEquals(emptyList(), batchOperationResult.errors) coVerify { - entityPayloadService.deleteEntity(firstEntityURI) - entityPayloadService.deleteEntity(secondEntityURI) - authorizationService.removeRightsOnEntity(firstEntityURI) - authorizationService.removeRightsOnEntity(secondEntityURI) - } - coVerify(exactly = 2) { - entityEventService.publishEntityDeleteEvent(sub, any()) + entityService.deleteEntity(firstEntityURI, sub) + entityService.deleteEntity(secondEntityURI, sub) } } @Test fun `batch delete should return deleted entity ids and in errors when deletion is partially successful`() = runTest { + coEvery { entityService.deleteEntity(firstEntityURI, sub) } returns Unit.right() coEvery { - entityPayloadService.deleteEntity(firstEntityURI) - } returns mockkClass(EntityPayload::class).right() - coEvery { - entityPayloadService.deleteEntity(secondEntityURI) + entityService.deleteEntity(secondEntityURI, sub) } returns InternalErrorException("Something went wrong during deletion").left() - coEvery { authorizationService.removeRightsOnEntity(any()) } returns Unit.right() - coEvery { entityEventService.publishEntityDeleteEvent(any(), any()) } returns Job() val batchOperationResult = entityOperationService.delete( - setOf( - mockkClass(EntityPayload::class) { every { entityId } returns firstEntityURI }, - mockkClass(EntityPayload::class) { every { entityId } returns secondEntityURI }, + listOf( + firstEntityURI, + secondEntityURI, ), sub ) @@ -406,10 +360,6 @@ class EntityOperationServiceTests { ), batchOperationResult.errors ) - coVerify(exactly = 1) { - authorizationService.removeRightsOnEntity(any()) - entityEventService.publishEntityDeleteEvent(any(), any()) - } } @Test @@ -417,13 +367,13 @@ class EntityOperationServiceTests { val deleteEntityErrorMessage = "Something went wrong with deletion request" coEvery { - entityPayloadService.deleteEntity(any()) + entityService.deleteEntity(any(), any()) } returns InternalErrorException(deleteEntityErrorMessage).left() val batchOperationResult = entityOperationService.delete( - setOf( - mockkClass(EntityPayload::class) { every { entityId } returns firstEntityURI }, - mockkClass(EntityPayload::class) { every { entityId } returns secondEntityURI }, + listOf( + firstEntityURI, + secondEntityURI, ), sub ) @@ -443,17 +393,15 @@ class EntityOperationServiceTests { batchOperationResult.errors ) - coVerify { entityPayloadService.deleteEntity(firstEntityURI) } - coVerify { entityPayloadService.deleteEntity(secondEntityURI) } - coVerify { entityEventService.publishEntityDeleteEvent(any(), any()) wasNot Called } + coVerify { entityService.deleteEntity(firstEntityURI, sub) } + coVerify { entityService.deleteEntity(secondEntityURI, sub) } } @Test fun `batch merge should ask to merge attributes of entities`() = runTest { coEvery { - entityPayloadService.mergeEntity(any(), any(), any(), any()) + entityService.mergeEntity(any(), any(), any(), any()) } returns EMPTY_UPDATE_RESULT.right() - coEvery { entityEventService.publishAttributeChangeEvents(any(), any(), any(), any(), any()) } returns Job() val batchOperationResult = entityOperationService.merge( listOf( @@ -469,11 +417,160 @@ class EntityOperationServiceTests { ) coVerify { - entityPayloadService.mergeEntity(eq(firstEntityURI), any(), null, sub) - entityPayloadService.mergeEntity(eq(secondEntityURI), any(), null, sub) + entityService.mergeEntity(eq(firstEntityURI), any(), null, sub) + entityService.mergeEntity(eq(secondEntityURI), any(), null, sub) + } + } + + fun upsertUpdateSetup() { + val capturedExpandedEntities = slot>() + + coEvery { entityOperationService.splitEntitiesByUniqueness(capture(capturedExpandedEntities)) } answers { + capturedExpandedEntities.captured to emptyList() + } + + coEvery { entityOperationService.splitEntitiesByExistence(any()) } returns Pair( + listOf( + firstExpandedEntity to firstEntity, + secondExpandedEntity to secondEntity + ), + emptyList() + ) + } + + @Test + fun `upsert batch entity without option should replace existing entities`() = runTest { + upsertUpdateSetup() + + coEvery { entityOperationService.replace(any(), any()) } returns BatchOperationResult( + mutableListOf(BatchEntitySuccess(firstEntity.id), BatchEntitySuccess(secondEntity.id)), + arrayListOf() + ) + + val (batchOperationResult, createdIds) = entityOperationService.upsert( + listOf( + firstExpandedEntity to firstEntity, + secondExpandedEntity to secondEntity + ), + null, + sub + ) + + assertEquals(0, createdIds.size) + assertEquals(2, batchOperationResult.success.size) + assertEquals(0, batchOperationResult.errors.size) + + coVerify { entityOperationService.create(any(), any()) wasNot Called } + coVerify { entityOperationService.replace(any(), com.egm.stellio.shared.util.sub.getOrNull()) } + coVerify { entityOperationService.update(any(), any(), any()) wasNot Called } + } + + @Test + fun `upsert batch entity with update should update existing entities`() = runTest { + upsertUpdateSetup() + + coEvery { entityOperationService.update(any(), any(), any()) } returns BatchOperationResult( + mutableListOf(BatchEntitySuccess(firstEntity.id), BatchEntitySuccess(secondEntity.id)), + arrayListOf() + ) + + val (batchOperationResult, createdIds) = entityOperationService.upsert( + listOf( + firstExpandedEntity to firstEntity, + secondExpandedEntity to secondEntity + ), + "update", + sub + ) + + assertEquals(0, createdIds.size) + assertEquals(2, batchOperationResult.success.size) + assertEquals(0, batchOperationResult.errors.size) + + coVerify { entityOperationService.create(any(), any()) wasNot Called } + coVerify { entityOperationService.update(any(), false, com.egm.stellio.shared.util.sub.getOrNull()) } + coVerify { entityOperationService.replace(any(), any()) wasNot Called } + } + + @Test + fun `upsert batch entity with non existing entities should create them`() = runTest { + val capturedExpandedEntities = slot>() + + coEvery { entityOperationService.splitEntitiesByUniqueness(capture(capturedExpandedEntities)) } answers { + capturedExpandedEntities.captured to emptyList() } - coVerify(exactly = 2) { - entityEventService.publishAttributeChangeEvents(any(), any(), any(), any(), any()) + + coEvery { entityOperationService.splitEntitiesByExistence(any()) } returns Pair( + emptyList(), + listOf( + firstExpandedEntity to firstEntity, + secondExpandedEntity to secondEntity + ) + ) + + coEvery { entityOperationService.create(any(), any()) } returns BatchOperationResult( + mutableListOf(BatchEntitySuccess(firstEntity.id), BatchEntitySuccess(secondEntity.id)), + arrayListOf() + ) + + val (batchOperationResult, createdIds) = entityOperationService.upsert( + listOf( + firstExpandedEntity to firstEntity, + secondExpandedEntity to secondEntity + ), + "update", + sub + ) + + assertEquals(2, createdIds.size) + assertEquals(2, batchOperationResult.success.size) + assertEquals(0, batchOperationResult.errors.size) + + coVerify { entityOperationService.create(any(), any()) } + coVerify { entityOperationService.update(any(), any(), any()) wasNot Called } + coVerify { entityOperationService.replace(any(), any()) wasNot Called } + } + + @Test + fun `upsert batch entity should return errors`() = runTest { + val capturedExpandedEntities = slot>() + + coEvery { entityOperationService.splitEntitiesByUniqueness(capture(capturedExpandedEntities)) } answers { + capturedExpandedEntities.captured to emptyList() } + + coEvery { entityOperationService.splitEntitiesByExistence(any()) } returns Pair( + listOf( + firstExpandedEntity to firstEntity, + ), + listOf( + secondExpandedEntity to secondEntity + ) + ) + + coEvery { entityOperationService.create(any(), any()) } returns BatchOperationResult( + emptyList().toMutableList(), + arrayListOf(BatchEntityError(firstEntity.id, mutableListOf(ENTITIY_CREATION_FORBIDDEN_MESSAGE))) + ) + coEvery { entityOperationService.replace(any(), any()) } returns BatchOperationResult( + emptyList().toMutableList(), + arrayListOf(BatchEntityError(secondEntity.id, mutableListOf(ENTITY_ADMIN_FORBIDDEN_MESSAGE))) + ) + + val (batchOperationResult, createdIds) = entityOperationService.upsert( + listOf( + firstExpandedEntity to firstEntity, + secondExpandedEntity to secondEntity + ), + null, + sub + ) + + assertEquals(0, createdIds.size) + assertEquals(0, batchOperationResult.success.size) + assertEquals(2, batchOperationResult.errors.size) + + coVerify { entityOperationService.create(any(), any()) } + coVerify { entityOperationService.replace(any(), any()) } } } diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityQueryServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityQueryServiceTests.kt new file mode 100644 index 000000000..61b949fe6 --- /dev/null +++ b/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityQueryServiceTests.kt @@ -0,0 +1,219 @@ +package com.egm.stellio.search.entity.service + +import arrow.core.right +import com.egm.stellio.search.authorization.service.AuthorizationService +import com.egm.stellio.search.entity.model.Attribute +import com.egm.stellio.search.entity.model.Entity +import com.egm.stellio.search.support.WithKafkaContainer +import com.egm.stellio.search.support.WithTimescaleContainer +import com.egm.stellio.search.support.buildDefaultQueryParams +import com.egm.stellio.shared.model.AlreadyExistsException +import com.egm.stellio.shared.model.ResourceNotFoundException +import com.egm.stellio.shared.util.* +import com.egm.stellio.shared.util.AuthContextModel.SpecificAccessPolicy.AUTH_READ +import com.ninjasquad.springmockk.MockkBean +import io.mockk.coEvery +import kotlinx.coroutines.test.runTest +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.data.r2dbc.core.R2dbcEntityTemplate +import org.springframework.data.r2dbc.core.delete +import org.springframework.test.context.ActiveProfiles +import java.time.ZonedDateTime + +@SpringBootTest +@ActiveProfiles("test") +class EntityQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { + + @Autowired + private lateinit var entityQueryService: EntityQueryService + + @Autowired + private lateinit var entityService: EntityService + + @MockkBean + private lateinit var authorizationService: AuthorizationService + + @Autowired + private lateinit var r2dbcEntityTemplate: R2dbcEntityTemplate + + private val beehiveTestCId = "urn:ngsi-ld:BeeHive:TESTC".toUri() + private val entity01Uri = "urn:ngsi-ld:Entity:01".toUri() + private val entity02Uri = "urn:ngsi-ld:Entity:02".toUri() + private val now = ngsiLdDateTime() + + @AfterEach + fun deleteEntities() { + r2dbcEntityTemplate.delete().from("entity_payload").all().block() + r2dbcEntityTemplate.delete().from("temporal_entity_attribute").all().block() + } + + @Test + fun `it should return a JSON-LD entity when querying by id`() = runTest { + coEvery { authorizationService.userCanReadEntity(any(), any()) } returns Unit.right() + + loadAndPrepareSampleData("beehive.jsonld") + .map { + entityService.createEntityPayload( + it.second, + it.first, + now, + ).shouldSucceed() + } + + entityQueryService.queryEntity(beehiveTestCId) + .shouldSucceedWith { + assertEquals(beehiveTestCId.toString(), it.id) + assertEquals(listOf(BEEHIVE_TYPE), it.types) + assertEquals(7, it.members.size) + } + } + + @Test + fun `it should return an API exception if no entity exists with the given id`() = runTest { + entityQueryService.queryEntity(entity01Uri) + .shouldFail { + assertTrue(it is ResourceNotFoundException) + } + } + + @Test + fun `it should return a list of JSON-LD entities when querying entities`() = runTest { + coEvery { authorizationService.computeAccessRightFilter(any()) } returns { null } + + loadAndPrepareSampleData("beehive.jsonld") + .map { + entityService.createEntityPayload( + it.second, + it.first, + now, + ).shouldSucceed() + } + + entityQueryService.queryEntities(buildDefaultQueryParams().copy(ids = setOf(beehiveTestCId))) + .shouldSucceedWith { + assertEquals(1, it.second) + assertEquals(beehiveTestCId.toString(), it.first[0].id) + assertEquals(listOf(BEEHIVE_TYPE), it.first[0].types) + assertEquals(7, it.first[0].members.size) + } + } + + @Test + fun `it should return an empty list if no entity matched the query`() = runTest { + coEvery { authorizationService.computeAccessRightFilter(any()) } returns { null } + + entityQueryService.queryEntities(buildDefaultQueryParams().copy(ids = setOf(entity01Uri))) + .shouldSucceedWith { + assertEquals(0, it.second) + assertTrue(it.first.isEmpty()) + } + } + + @Test + fun `it should retrieve an entity payload`() = runTest { + loadMinimalEntity(entity01Uri, setOf(BEEHIVE_TYPE)) + .sampleDataToNgsiLdEntity() + .map { + entityService.createEntityPayload( + it.second, + it.first, + now, + ).shouldSucceed() + } + + entityQueryService.retrieve(entity01Uri) + .shouldSucceedWith { + assertThat(it) + .hasFieldOrPropertyWithValue("entityId", entity01Uri) + .hasFieldOrPropertyWithValue("types", listOf(BEEHIVE_TYPE)) + .hasFieldOrPropertyWithValue("createdAt", now) + .hasFieldOrPropertyWithValue("modifiedAt", null) + .hasFieldOrPropertyWithValue("specificAccessPolicy", null) + } + } + + @Test + fun `it should retrieve an entity payload with specificAccesPolicy`() = runTest { + loadMinimalEntityWithSap(entity01Uri, setOf(BEEHIVE_TYPE), AUTH_READ, AUTHZ_TEST_COMPOUND_CONTEXTS) + .sampleDataToNgsiLdEntity() + .map { + entityService.createEntityPayload( + it.second, + it.first, + now + ) + } + + entityQueryService.retrieve(entity01Uri) + .shouldSucceedWith { + assertThat(it) + .hasFieldOrPropertyWithValue("specificAccessPolicy", AUTH_READ) + } + } + + @Test + fun `it should retrieve a list of entity payloads`() = runTest { + val expandedPayload = loadSampleData("beehive_expanded.jsonld") + expandedPayload.sampleDataToNgsiLdEntity().map { + entityService.createEntityPayload( + it.second, + it.first, + ZonedDateTime.parse("2023-08-20T15:44:10.381090Z") + ) + } + + val entityPayloads = entityQueryService.retrieve(listOf(beehiveTestCId)) + assertEquals(1, entityPayloads.size) + assertEquals(beehiveTestCId, entityPayloads[0].entityId) + assertJsonPayloadsAreEqual(expandedPayload, entityPayloads[0].payload.asString()) + } + + @Test + fun `it should filter existing entities from a list of ids`() = runTest { + loadMinimalEntity(entity01Uri, setOf(BEEHIVE_TYPE)) + .sampleDataToNgsiLdEntity() + .map { + entityService.createEntityPayload( + it.second, + it.first, + now + ) + } + + val existingEntities = entityQueryService.filterExistingEntitiesAsIds(listOf(entity01Uri, entity02Uri)) + assertEquals(1, existingEntities.size) + assertEquals(entity01Uri, existingEntities[0]) + } + + @Test + fun `it should return an empty list if no ids are provided to the filter on existence`() = runTest { + val existingEntities = entityQueryService.filterExistingEntitiesAsIds(emptyList()) + assertTrue(existingEntities.isEmpty()) + } + + @Test + fun `it should check the existence or non-existence of an entity`() = runTest { + loadMinimalEntity(entity01Uri, setOf(BEEHIVE_TYPE)) + .sampleDataToNgsiLdEntity() + .map { + entityService.createEntityPayload( + it.second, + it.first, + now + ) + } + + entityQueryService.checkEntityExistence(entity01Uri).shouldSucceed() + entityQueryService.checkEntityExistence(entity02Uri) + .shouldFail { assert(it is ResourceNotFoundException) } + entityQueryService.checkEntityExistence(entity01Uri, true) + .shouldFail { assert(it is AlreadyExistsException) } + entityQueryService.checkEntityExistence(entity02Uri, true).shouldSucceed() + } +} diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityQueryServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityServiceQueriesTests.kt similarity index 87% rename from search-service/src/test/kotlin/com/egm/stellio/search/service/EntityQueryServiceTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityServiceQueriesTests.kt index 8afce0713..c39a4f4ff 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityQueryServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityServiceQueriesTests.kt @@ -1,10 +1,12 @@ -package com.egm.stellio.search.service +package com.egm.stellio.search.entity.service import arrow.core.right -import com.egm.stellio.search.model.EntitiesQuery -import com.egm.stellio.search.model.EntityPayload +import com.egm.stellio.search.authorization.service.AuthorizationService +import com.egm.stellio.search.entity.model.EntitiesQuery +import com.egm.stellio.search.entity.model.Entity import com.egm.stellio.search.support.WithKafkaContainer import com.egm.stellio.search.support.WithTimescaleContainer +import com.egm.stellio.search.temporal.service.AttributeInstanceService import com.egm.stellio.shared.model.GeoQuery import com.egm.stellio.shared.model.PaginationQuery import com.egm.stellio.shared.util.* @@ -30,10 +32,16 @@ import java.net.URI @SpringBootTest @ActiveProfiles("test") -class EntityQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { +class EntityServiceQueriesTests : WithTimescaleContainer, WithKafkaContainer { @Autowired - private lateinit var entityPayloadService: EntityPayloadService + private lateinit var entityQueryService: EntityQueryService + + @Autowired + private lateinit var entityService: EntityService + + @MockkBean + private lateinit var authorizationService: AuthorizationService @MockkBean(relaxed = true) private lateinit var attributeInstanceService: AttributeInstanceService @@ -55,24 +63,28 @@ class EntityQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { val fourthRawEntity = loadSampleData("beekeeper.jsonld") val fifthRawEntity = loadSampleData("apiary.jsonld") + coEvery { authorizationService.userCanCreateEntities(any()) } returns Unit.right() coEvery { attributeInstanceService.create(any()) } returns Unit.right() + coEvery { authorizationService.createOwnerRight(any(), any()) } returns Unit.right() runBlocking { listOf(firstRawEntity, secondRawEntity, thirdRawEntity, fourthRawEntity, fifthRawEntity).forEach { val (expandedEntity, ngsiLdEntity) = it.sampleDataToNgsiLdEntity().shouldSucceedAndResult() - entityPayloadService.createEntity(ngsiLdEntity, expandedEntity) + entityService.createEntity(ngsiLdEntity, expandedEntity, MOCK_USER_SUB).shouldSucceed() } } } @AfterAll fun deleteEntities() { + coEvery { authorizationService.userCanAdminEntity(any(), any()) } returns Unit.right() + coEvery { authorizationService.removeRightsOnEntity(any()) } returns Unit.right() runBlocking { - entityPayloadService.deleteEntity(entity01Uri) - entityPayloadService.deleteEntity(entity02Uri) - entityPayloadService.deleteEntity(entity03Uri) - entityPayloadService.deleteEntity(entity04Uri) - entityPayloadService.deleteEntity(entity05Uri) + entityService.deleteEntity(entity01Uri) + entityService.deleteEntity(entity02Uri) + entityService.deleteEntity(entity03Uri) + entityService.deleteEntity(entity04Uri) + entityService.deleteEntity(entity05Uri) } } @@ -94,7 +106,7 @@ class EntityQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { expectedListOfEntities: String? ) = runTest { val entitiesIds = - entityPayloadService.queryEntities( + entityQueryService.queryEntities( EntitiesQuery( typeSelection = types, paginationQuery = PaginationQuery(limit = 30, offset = 0), @@ -122,7 +134,7 @@ class EntityQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { expectedListOfEntities: String? ) = runTest { val entitiesIds = - entityPayloadService.queryEntities( + entityQueryService.queryEntities( EntitiesQuery( typeSelection = BEEHIVE_TYPE, scopeQ = scopeQ, @@ -139,7 +151,7 @@ class EntityQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { @Test fun `it should retrieve entities according to ids and a type`() = runTest { val entitiesIds = - entityPayloadService.queryEntities( + entityQueryService.queryEntities( EntitiesQuery( ids = setOf(entity02Uri), typeSelection = BEEHIVE_TYPE, @@ -155,7 +167,7 @@ class EntityQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { @Test fun `it should retrieve entities according to ids and a selection of types`() = runTest { val entitiesIds = - entityPayloadService.queryEntities( + entityQueryService.queryEntities( EntitiesQuery( ids = setOf(entity02Uri, entity05Uri), typeSelection = "$APIARY_TYPE|$BEEHIVE_TYPE", @@ -171,7 +183,7 @@ class EntityQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { @Test fun `it should retrieve entities according to attrs and types`() = runTest { val entitiesIds = - entityPayloadService.queryEntities( + entityQueryService.queryEntities( EntitiesQuery( typeSelection = BEEHIVE_TYPE, paginationQuery = PaginationQuery(limit = 2, offset = 0), @@ -187,7 +199,7 @@ class EntityQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { @Test fun `it should retrieve entities by a list of ids`() = runTest { val entitiesIds = - entityPayloadService.queryEntities( + entityQueryService.queryEntities( EntitiesQuery( ids = setOf(entity02Uri), typeSelection = BEEHIVE_TYPE, @@ -203,7 +215,7 @@ class EntityQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { @Test fun `it should retrieve entities with respect to limit and offset`() = runTest { val entitiesIds = - entityPayloadService.queryEntities( + entityQueryService.queryEntities( EntitiesQuery( typeSelection = BEEHIVE_TYPE, paginationQuery = PaginationQuery(limit = 1, offset = 0), @@ -217,7 +229,7 @@ class EntityQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { @Test fun `it should retrieve entities with respect to idPattern`() = runTest { val entitiesIds = - entityPayloadService.queryEntities( + entityQueryService.queryEntities( EntitiesQuery( typeSelection = BEEHIVE_TYPE, idPattern = ".*urn:ngsi-ld:BeeHive:01.*", @@ -276,7 +288,7 @@ class EntityQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { expectedListOfEntities: String? ) = runTest { val entitiesIds = - entityPayloadService.queryEntities( + entityQueryService.queryEntities( EntitiesQuery( q = q, paginationQuery = PaginationQuery(limit = 2, offset = 0), @@ -309,7 +321,7 @@ class EntityQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { expectedCount: Int ) = runTest { val entitiesIds = - entityPayloadService.queryEntities( + entityQueryService.queryEntities( EntitiesQuery( paginationQuery = PaginationQuery(limit = 2, offset = 0), geoQuery = GeoQuery( @@ -331,7 +343,7 @@ class EntityQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { @Test fun `it should retrieve entities according to access rights`() = runTest { val entitiesIds = - entityPayloadService.queryEntities( + entityQueryService.queryEntities( EntitiesQuery( typeSelection = BEEHIVE_TYPE, paginationQuery = PaginationQuery(limit = 30, offset = 0), @@ -356,7 +368,7 @@ class EntityQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { updateSpecificAccessPolicy(entity01Uri, AuthContextModel.SpecificAccessPolicy.AUTH_READ) val entitiesIds = - entityPayloadService.queryEntities( + entityQueryService.queryEntities( EntitiesQuery( typeSelection = BEEHIVE_TYPE, paginationQuery = PaginationQuery(limit = 30, offset = 0), @@ -383,7 +395,7 @@ class EntityQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { updateSpecificAccessPolicy(entity01Uri, AuthContextModel.SpecificAccessPolicy.AUTH_READ) val entitiesIds = - entityPayloadService.queryEntities( + entityQueryService.queryEntities( EntitiesQuery( typeSelection = BEEHIVE_TYPE, paginationQuery = PaginationQuery(limit = 30, offset = 0), @@ -407,7 +419,7 @@ class EntityQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { @Test fun `it should retrieve the count of entities`() = runTest { - entityPayloadService.queryEntitiesCount( + entityQueryService.queryEntitiesCount( EntitiesQuery( typeSelection = BEEHIVE_TYPE, paginationQuery = PaginationQuery(limit = 30, offset = 0), @@ -418,7 +430,7 @@ class EntityQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { @Test fun `it should retrieve the count of entities according to access rights`() = runTest { - entityPayloadService.queryEntitiesCount( + entityQueryService.queryEntitiesCount( EntitiesQuery( ids = setOf(entity02Uri, entity01Uri), typeSelection = BEEHIVE_TYPE, @@ -432,7 +444,7 @@ class EntityQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { @Test fun `it should return an empty list if no entity matches the requested type`() = runTest { val entitiesIds = - entityPayloadService.queryEntities( + entityQueryService.queryEntities( EntitiesQuery( ids = setOf(entity02Uri, entity01Uri), typeSelection = "https://ontology.eglobalmark.com/apic#UnknownType", @@ -445,9 +457,9 @@ class EntityQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { } @Test - fun `it should return an empty list if no entitiy matched the requested attributes`() = runTest { + fun `it should return an empty list if no entity matches the requested attributes`() = runTest { val entitiesIds = - entityPayloadService.queryEntities( + entityQueryService.queryEntities( EntitiesQuery( typeSelection = BEEHIVE_TYPE, paginationQuery = PaginationQuery(limit = 2, offset = 10), @@ -462,17 +474,13 @@ class EntityQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { private fun updateSpecificAccessPolicy( entityId: URI, specificAccessPolicy: AuthContextModel.SpecificAccessPolicy - ) = r2dbcEntityTemplate.update( - Query.query(Criteria.where("entity_id").`is`(entityId)), - Update.update("specific_access_policy", specificAccessPolicy.toString()), - EntityPayload::class.java - ).block() + ) = r2dbcEntityTemplate.update(Entity::class.java).inTable("entity_payload") + .matching(Query.query(Criteria.where("entity_id").`is`(entityId))) + .apply(Update.update("specific_access_policy", specificAccessPolicy.toString())).block() private fun resetSpecificAccessPolicy( entityId: URI - ) = r2dbcEntityTemplate.update( - Query.query(Criteria.where("entity_id").`is`(entityId)), - Update.update("specific_access_policy", null), - EntityPayload::class.java - ).block() + ) = r2dbcEntityTemplate.update(Entity::class.java).inTable("entity_payload") + .matching(Query.query(Criteria.where("entity_id").`is`(entityId))) + .apply(Update.update("specific_access_policy", null)).block() } diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityServiceTests.kt new file mode 100644 index 000000000..95e012419 --- /dev/null +++ b/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityServiceTests.kt @@ -0,0 +1,518 @@ +package com.egm.stellio.search.entity.service + +import arrow.core.right +import arrow.core.toOption +import com.egm.stellio.search.authorization.service.AuthorizationService +import com.egm.stellio.search.common.util.deserializeAsMap +import com.egm.stellio.search.entity.model.* +import com.egm.stellio.search.support.EMPTY_PAYLOAD +import com.egm.stellio.search.support.WithKafkaContainer +import com.egm.stellio.search.support.WithTimescaleContainer +import com.egm.stellio.shared.util.* +import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_TYPE +import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_DEFAULT_VOCAB +import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_SCOPE_PROPERTY +import com.egm.stellio.shared.util.JsonLdUtils.expandAttribute +import com.egm.stellio.shared.util.JsonLdUtils.expandAttributes +import com.egm.stellio.shared.util.JsonUtils.deserializeExpandedPayload +import com.ninjasquad.springmockk.MockkBean +import io.mockk.coEvery +import io.mockk.coVerify +import kotlinx.coroutines.Job +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.dao.DataIntegrityViolationException +import org.springframework.data.r2dbc.core.R2dbcEntityTemplate +import org.springframework.data.r2dbc.core.delete +import org.springframework.test.context.ActiveProfiles + +@SpringBootTest +@ActiveProfiles("test") +class EntityServiceTests : WithTimescaleContainer, WithKafkaContainer { + + @Autowired + private lateinit var entityService: EntityService + + @Autowired + private lateinit var entityQueryService: EntityQueryService + + @MockkBean + private lateinit var entityAttributeService: EntityAttributeService + + @MockkBean(relaxed = true) + private lateinit var entityEventService: EntityEventService + + @MockkBean + private lateinit var authorizationService: AuthorizationService + + @Autowired + private lateinit var r2dbcEntityTemplate: R2dbcEntityTemplate + + private val entity01Uri = "urn:ngsi-ld:Entity:01".toUri() + private val beehiveTestCId = "urn:ngsi-ld:BeeHive:TESTC".toUri() + private val sub = "0123456789-1234-5678-987654321" + private val now = ngsiLdDateTime() + + @AfterEach + fun clearEntityPayloadTable() { + r2dbcEntityTemplate.delete().from("entity_payload").all().block() + } + + @Test + fun `it should create an entity payload from string if none existed yet`() = runTest { + loadMinimalEntity(entity01Uri, setOf(BEEHIVE_TYPE)) + .sampleDataToNgsiLdEntity() + .map { + entityService.createEntityPayload( + it.second, + it.first, + now + ).shouldSucceed() + } + } + + @Test + fun `it should create an entity payload from an NGSI-LD Entity if none existed yet`() = runTest { + val (jsonLdEntity, ngsiLdEntity) = loadSampleData().sampleDataToNgsiLdEntity().shouldSucceedAndResult() + entityService.createEntityPayload(ngsiLdEntity, jsonLdEntity, now) + .shouldSucceed() + } + + @Test + fun `it should only create an entity payload for a minimal entity`() = runTest { + coEvery { authorizationService.userCanCreateEntities(any()) } returns Unit.right() + coEvery { entityEventService.publishEntityCreateEvent(any(), any(), any()) } returns Job() + coEvery { + entityAttributeService.createAttributes(any(), any(), any(), any(), any()) + } returns Unit.right() + coEvery { authorizationService.createOwnerRight(any(), any()) } returns Unit.right() + + val (expandedEntity, ngsiLdEntity) = + loadAndPrepareSampleData("beehive_minimal.jsonld").shouldSucceedAndResult() + + entityService.createEntity( + ngsiLdEntity, + expandedEntity, + sub + ).shouldSucceed() + + entityQueryService.retrieve(beehiveTestCId) + .shouldSucceedWith { + assertEquals(beehiveTestCId, it.entityId) + assertEquals(listOf(BEEHIVE_TYPE), it.types) + } + + coVerify { + authorizationService.userCanCreateEntities(eq(sub.toOption())) + entityAttributeService.createAttributes( + any(), + any(), + emptyList(), + any(), + eq(sub) + ) + entityEventService.publishEntityCreateEvent( + eq(sub), + eq(beehiveTestCId), + any() + ) + authorizationService.createOwnerRight(beehiveTestCId, sub.toOption()) + } + } + + @Test + fun `it should not create an entity payload if one already existed`() = runTest { + val (jsonLdEntity, ngsiLdEntity) = + loadMinimalEntity(entity01Uri, setOf(BEEHIVE_TYPE)).sampleDataToNgsiLdEntity().shouldSucceedAndResult() + entityService.createEntityPayload( + ngsiLdEntity, + jsonLdEntity, + now, + ) + + assertThrows { + entityService.createEntityPayload( + ngsiLdEntity, + jsonLdEntity, + now, + ) + } + } + + @Test + fun `it should merge an entity`() = runTest { + coEvery { authorizationService.userCanCreateEntities(any()) } returns Unit.right() + coEvery { authorizationService.userCanUpdateEntity(any(), any()) } returns Unit.right() + coEvery { + entityAttributeService.createAttributes(any(), any(), any(), any(), any()) + } returns Unit.right() + coEvery { + entityAttributeService.mergeAttributes(any(), any(), any(), any(), any(), any()) + } returns UpdateResult( + listOf(UpdatedDetails(INCOMING_PROPERTY, null, UpdateOperationResult.APPENDED)), + emptyList() + ).right() + coEvery { entityAttributeService.getForEntity(any(), any(), any()) } returns emptyList() + coEvery { authorizationService.createOwnerRight(any(), any()) } returns Unit.right() + + val (expandedEntity, ngsiLdEntity) = + loadAndPrepareSampleData("beehive_minimal.jsonld").shouldSucceedAndResult() + + entityService.createEntity( + ngsiLdEntity, + expandedEntity, + sub + ).shouldSucceed() + + val (jsonLdEntity, _) = loadSampleData().sampleDataToNgsiLdEntity().shouldSucceedAndResult() + + entityService.mergeEntity( + beehiveTestCId, + jsonLdEntity.getModifiableMembers(), + now, + sub + ).shouldSucceed() + + entityQueryService.retrieve(beehiveTestCId) + .shouldSucceedWith { + assertTrue(it.modifiedAt != null) + } + + coVerify { + authorizationService.userCanCreateEntities(sub.toOption()) + authorizationService.userCanUpdateEntity(beehiveTestCId, sub.toOption()) + entityAttributeService.createAttributes( + any(), + any(), + emptyList(), + any(), + eq(sub) + ) + entityAttributeService.mergeAttributes( + eq(beehiveTestCId), + any(), + any(), + any(), + any(), + eq(sub) + ) + entityAttributeService.getForEntity( + eq(beehiveTestCId), + emptySet(), + emptySet() + ) + authorizationService.createOwnerRight(beehiveTestCId, sub.toOption()) + } + } + + @Test + fun `it should merge an entity with new types`() = runTest { + coEvery { authorizationService.userCanCreateEntities(any()) } returns Unit.right() + coEvery { authorizationService.userCanUpdateEntity(any(), any()) } returns Unit.right() + coEvery { + entityAttributeService.createAttributes(any(), any(), any(), any(), any()) + } returns Unit.right() + coEvery { + entityAttributeService.mergeAttributes(any(), any(), any(), any(), any(), any()) + } returns UpdateResult( + listOf(UpdatedDetails(INCOMING_PROPERTY, null, UpdateOperationResult.APPENDED)), + emptyList() + ).right() + coEvery { entityAttributeService.getForEntity(any(), any(), any()) } returns emptyList() + coEvery { authorizationService.createOwnerRight(any(), any()) } returns Unit.right() + + val (expandedEntity, ngsiLdEntity) = + loadAndPrepareSampleData("beehive_minimal.jsonld").shouldSucceedAndResult() + + entityService.createEntity( + ngsiLdEntity, + expandedEntity, + sub + ).shouldSucceed() + + val expandedAttributes = expandAttributes( + loadSampleData("fragments/beehive_merge_entity_multiple_types.jsonld"), + APIC_COMPOUND_CONTEXTS + ) + + entityService.mergeEntity( + beehiveTestCId, + expandedAttributes, + now, + sub + ).shouldSucceed() + + entityQueryService.retrieve(beehiveTestCId) + .shouldSucceedWith { + assertTrue(it.types.containsAll(setOf(BEEHIVE_TYPE, NGSILD_DEFAULT_VOCAB + "Distribution"))) + } + } + + @Test + fun `it should merge an entity with new types and scopes`() = runTest { + coEvery { authorizationService.userCanCreateEntities(any()) } returns Unit.right() + coEvery { authorizationService.userCanUpdateEntity(any(), any()) } returns Unit.right() + coEvery { + entityAttributeService.createAttributes(any(), any(), any(), any(), any()) + } returns Unit.right() + coEvery { + entityAttributeService.mergeAttributes(any(), any(), any(), any(), any(), any()) + } returns UpdateResult( + listOf(UpdatedDetails(INCOMING_PROPERTY, null, UpdateOperationResult.APPENDED)), + emptyList() + ).right() + coEvery { + entityAttributeService.partialUpdateAttribute(any(), any(), any(), any()) + } returns EMPTY_UPDATE_RESULT.right() + coEvery { entityAttributeService.getForEntity(any(), any(), any()) } returns emptyList() + coEvery { authorizationService.createOwnerRight(any(), any()) } returns Unit.right() + + val (expandedEntity, ngsiLdEntity) = + loadAndPrepareSampleData("beehive_minimal.jsonld").shouldSucceedAndResult() + + entityService.createEntity( + ngsiLdEntity, + expandedEntity, + sub + ).shouldSucceed() + + val expandedAttributes = expandAttributes( + loadSampleData("fragments/beehive_merge_entity_multiple_types_and_scopes.jsonld"), + APIC_COMPOUND_CONTEXTS + ) + + entityService.mergeEntity( + beehiveTestCId, + expandedAttributes, + now, + sub + ).shouldSucceed() + + entityQueryService.retrieve(beehiveTestCId) + .shouldSucceedWith { + assertTrue(it.types.containsAll(setOf(BEEHIVE_TYPE, NGSILD_DEFAULT_VOCAB + "Distribution"))) + assertTrue(it.scopes?.containsAll(setOf("/Nantes/BottiereChenaie", "/Agri/Beekeeping")) ?: false) + } + } + + @Test + fun `it should replace an entity payload if entity previously existed`() = runTest { + val (jsonLdEntity, ngsiLdEntity) = loadSampleData().sampleDataToNgsiLdEntity().shouldSucceedAndResult() + entityService.createEntityPayload(ngsiLdEntity, jsonLdEntity, now).shouldSucceed() + + entityService.replaceEntityPayload(ngsiLdEntity, jsonLdEntity, now).shouldSucceed() + } + + @Test + fun `it should replace an entity`() = runTest { + coEvery { authorizationService.userCanCreateEntities(any()) } returns Unit.right() + coEvery { authorizationService.userCanUpdateEntity(any(), any()) } returns Unit.right() + coEvery { + entityAttributeService.createAttributes(any(), any(), any(), any(), any()) + } returns Unit.right() + coEvery { entityAttributeService.deleteAttributes(any()) } returns Unit.right() + coEvery { authorizationService.createOwnerRight(any(), any()) } returns Unit.right() + + val (expandedEntity, ngsiLdEntity) = + loadAndPrepareSampleData("beehive_minimal.jsonld").shouldSucceedAndResult() + + entityService.createEntity( + ngsiLdEntity, + expandedEntity, + sub + ).shouldSucceed() + + val (newExpandedEntity, newNgsiLdEntity) = loadSampleData().sampleDataToNgsiLdEntity().shouldSucceedAndResult() + + entityService.replaceEntity( + beehiveTestCId, + newNgsiLdEntity, + newExpandedEntity, + sub + ).shouldSucceed() + + entityQueryService.retrieve(beehiveTestCId) + .shouldSucceedWith { + assertTrue(it.modifiedAt != null) + assertEquals(8, it.payload.deserializeAsMap().size) + } + + coVerify { + authorizationService.userCanCreateEntities(sub.toOption()) + authorizationService.userCanUpdateEntity(beehiveTestCId, sub.toOption()) + entityAttributeService.deleteAttributes(beehiveTestCId) + entityAttributeService.createAttributes( + any(), + any(), + emptyList(), + any(), + eq(sub) + ) + authorizationService.createOwnerRight(beehiveTestCId, sub.toOption()) + } + } + + @Test + fun `it should replace an attribute`() = runTest { + coEvery { authorizationService.userCanUpdateEntity(any(), any()) } returns Unit.right() + coEvery { entityAttributeService.getForEntity(any(), any(), any()) } returns emptyList() + coEvery { + entityAttributeService.replaceAttribute(any(), any(), any(), any(), any()) + } returns UpdateResult( + updated = listOf(UpdatedDetails(INCOMING_PROPERTY, null, UpdateOperationResult.REPLACED)), + notUpdated = emptyList() + ).right() + + val (jsonLdEntity, ngsiLdEntity) = loadSampleData().sampleDataToNgsiLdEntity().shouldSucceedAndResult() + entityService.createEntityPayload(ngsiLdEntity, jsonLdEntity, now).shouldSucceed() + + val expandedAttribute = expandAttribute( + loadSampleData("fragments/beehive_new_incoming_property.json"), + APIC_COMPOUND_CONTEXTS + ) + + entityService.replaceAttribute(beehiveTestCId, expandedAttribute, sub) + .shouldSucceedWith { + it.updated.size == 1 && + it.notUpdated.isEmpty() && + it.updated[0].attributeName == INCOMING_PROPERTY && + it.updated[0].updateOperationResult == UpdateOperationResult.REPLACED + } + } + + @Test + fun `it should add a type to an entity`() = runTest { + val expandedPayload = loadSampleData("beehive_expanded.jsonld") + expandedPayload.sampleDataToNgsiLdEntity().map { + entityService.createEntityPayload( + it.second, + it.first, + now + ) + } + + entityService.updateTypes(beehiveTestCId, listOf(BEEHIVE_TYPE, APIARY_TYPE), ngsiLdDateTime(), false) + .shouldSucceedWith { + assertTrue(it.isSuccessful()) + assertEquals(1, it.updated.size) + val updatedDetails = it.updated[0] + assertEquals(JSONLD_TYPE, updatedDetails.attributeName) + assertEquals(UpdateOperationResult.APPENDED, updatedDetails.updateOperationResult) + } + + entityQueryService.retrieve(beehiveTestCId) + .shouldSucceedWith { + assertEquals(listOf(BEEHIVE_TYPE, APIARY_TYPE), it.types) + assertEquals( + listOf(BEEHIVE_TYPE, APIARY_TYPE), + it.payload.asString().deserializeExpandedPayload()[JSONLD_TYPE] + ) + } + } + + @Test + fun `it should add a type to an entity even if existing types are not in the list of types to add`() = runTest { + loadMinimalEntity(entity01Uri, setOf(BEEHIVE_TYPE)) + .sampleDataToNgsiLdEntity() + .map { + entityService.createEntityPayload( + it.second, + it.first, + now + ) + } + entityService.updateTypes(entity01Uri, listOf(APIARY_TYPE), ngsiLdDateTime(), false) + .shouldSucceed() + + entityQueryService.retrieve(entity01Uri) + .shouldSucceedWith { + assertEquals(listOf(BEEHIVE_TYPE, APIARY_TYPE), it.types) + assertEquals( + listOf(BEEHIVE_TYPE, APIARY_TYPE), + it.payload.asString().deserializeExpandedPayload()[JSONLD_TYPE] + ) + } + } + + @Test + fun `it should upsert an entity payload if one already existed`() = runTest { + loadMinimalEntity(entity01Uri, setOf(BEEHIVE_TYPE)) + .sampleDataToNgsiLdEntity() + .map { + entityService.createEntityPayload( + it.second, + it.first, + now + ) + } + + entityService.upsertEntityPayload(entity01Uri, EMPTY_PAYLOAD) + .shouldSucceed() + } + + @Test + fun `it should delete an entity payload`() = runTest { + coEvery { entityAttributeService.deleteAttributes(any()) } returns Unit.right() + + loadMinimalEntity(entity01Uri, setOf(BEEHIVE_TYPE)) + .sampleDataToNgsiLdEntity() + .map { + entityService.createEntityPayload( + it.second, + it.first, + now + ) + } + + entityService.deleteEntityPayload(entity01Uri) + .shouldSucceedWith { + assertEquals(entity01Uri, it.entityId) + assertNotNull(it.payload) + } + + // if correctly deleted, we should be able to create a new one + loadMinimalEntity(entity01Uri, setOf(BEEHIVE_TYPE)) + .sampleDataToNgsiLdEntity() + .map { + entityService.createEntityPayload( + it.second, + it.first, + now + ).shouldSucceed() + } + } + + @Test + fun `it should remove the scopes from an entity`() = runTest { + coEvery { authorizationService.userCanUpdateEntity(any(), any()) } returns Unit.right() + coEvery { + entityAttributeService.addAttribute(any(), any(), any(), any(), any(), any()) + } returns Unit.right() + coEvery { + entityAttributeService.getForEntity(any(), any(), any()) + } returns emptyList() + + loadSampleData("beehive_with_scope.jsonld") + .sampleDataToNgsiLdEntity() + .map { + entityService.createEntityPayload( + it.second, + it.first, + now + ) + } + + entityService.deleteAttribute(beehiveTestCId, NGSILD_SCOPE_PROPERTY, null) + .shouldSucceed() + + entityQueryService.retrieve(beehiveTestCId) + .shouldSucceedWith { + assertNull(it.scopes) + } + } +} diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/util/AttributeInstanceUtilsTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/entity/util/AttributeUtilsTests.kt similarity index 67% rename from search-service/src/test/kotlin/com/egm/stellio/search/util/AttributeInstanceUtilsTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/entity/util/AttributeUtilsTests.kt index 8329bc3dc..b9d039538 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/util/AttributeInstanceUtilsTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/entity/util/AttributeUtilsTests.kt @@ -1,6 +1,6 @@ -package com.egm.stellio.search.util +package com.egm.stellio.search.entity.util -import com.egm.stellio.search.model.TemporalEntityAttribute +import com.egm.stellio.search.entity.model.Attribute import com.egm.stellio.shared.util.JsonLdUtils.expandAttribute import com.egm.stellio.shared.util.NGSILD_TEST_CORE_CONTEXTS import com.egm.stellio.shared.util.ngsiLdDateTime @@ -12,7 +12,7 @@ import java.net.URI import java.time.LocalTime @ActiveProfiles("test") -class AttributeInstanceUtilsTests { +class AttributeUtilsTests { @Test fun `it should guess the value type of a string property`() = runTest { @@ -22,8 +22,8 @@ class AttributeInstanceUtilsTests { NGSILD_TEST_CORE_CONTEXTS ) assertEquals( - TemporalEntityAttribute.AttributeValueType.STRING, - guessAttributeValueType(TemporalEntityAttribute.AttributeType.Property, expandedStringProperty.second[0]) + Attribute.AttributeValueType.STRING, + guessAttributeValueType(Attribute.AttributeType.Property, expandedStringProperty.second[0]) ) } @@ -35,8 +35,8 @@ class AttributeInstanceUtilsTests { NGSILD_TEST_CORE_CONTEXTS ) assertEquals( - TemporalEntityAttribute.AttributeValueType.NUMBER, - guessAttributeValueType(TemporalEntityAttribute.AttributeType.Property, expandedBooleanProperty.second[0]) + Attribute.AttributeValueType.NUMBER, + guessAttributeValueType(Attribute.AttributeType.Property, expandedBooleanProperty.second[0]) ) } @@ -48,8 +48,8 @@ class AttributeInstanceUtilsTests { NGSILD_TEST_CORE_CONTEXTS ) assertEquals( - TemporalEntityAttribute.AttributeValueType.NUMBER, - guessAttributeValueType(TemporalEntityAttribute.AttributeType.Property, expandedBooleanProperty.second[0]) + Attribute.AttributeValueType.NUMBER, + guessAttributeValueType(Attribute.AttributeType.Property, expandedBooleanProperty.second[0]) ) } @@ -61,8 +61,8 @@ class AttributeInstanceUtilsTests { NGSILD_TEST_CORE_CONTEXTS ) assertEquals( - TemporalEntityAttribute.AttributeValueType.BOOLEAN, - guessAttributeValueType(TemporalEntityAttribute.AttributeType.Property, expandedBooleanProperty.second[0]) + Attribute.AttributeValueType.BOOLEAN, + guessAttributeValueType(Attribute.AttributeType.Property, expandedBooleanProperty.second[0]) ) } @@ -74,8 +74,8 @@ class AttributeInstanceUtilsTests { NGSILD_TEST_CORE_CONTEXTS ) assertEquals( - TemporalEntityAttribute.AttributeValueType.OBJECT, - guessAttributeValueType(TemporalEntityAttribute.AttributeType.Property, expandedListProperty.second[0]) + Attribute.AttributeValueType.OBJECT, + guessAttributeValueType(Attribute.AttributeType.Property, expandedListProperty.second[0]) ) } @@ -87,8 +87,8 @@ class AttributeInstanceUtilsTests { NGSILD_TEST_CORE_CONTEXTS ) assertEquals( - TemporalEntityAttribute.AttributeValueType.ARRAY, - guessAttributeValueType(TemporalEntityAttribute.AttributeType.Property, expandedListProperty.second[0]) + Attribute.AttributeValueType.ARRAY, + guessAttributeValueType(Attribute.AttributeType.Property, expandedListProperty.second[0]) ) } @@ -100,8 +100,8 @@ class AttributeInstanceUtilsTests { NGSILD_TEST_CORE_CONTEXTS ) assertEquals( - TemporalEntityAttribute.AttributeValueType.TIME, - guessAttributeValueType(TemporalEntityAttribute.AttributeType.Property, expandedTimeProperty.second[0]) + Attribute.AttributeValueType.TIME, + guessAttributeValueType(Attribute.AttributeType.Property, expandedTimeProperty.second[0]) ) } @@ -113,8 +113,8 @@ class AttributeInstanceUtilsTests { NGSILD_TEST_CORE_CONTEXTS ) assertEquals( - TemporalEntityAttribute.AttributeValueType.DATETIME, - guessAttributeValueType(TemporalEntityAttribute.AttributeType.Property, expandedTimeProperty.second[0]) + Attribute.AttributeValueType.DATETIME, + guessAttributeValueType(Attribute.AttributeType.Property, expandedTimeProperty.second[0]) ) } @@ -129,8 +129,8 @@ class AttributeInstanceUtilsTests { NGSILD_TEST_CORE_CONTEXTS ) assertEquals( - TemporalEntityAttribute.AttributeValueType.GEOMETRY, - guessAttributeValueType(TemporalEntityAttribute.AttributeType.GeoProperty, expandedGeoProperty.second[0]) + Attribute.AttributeValueType.GEOMETRY, + guessAttributeValueType(Attribute.AttributeType.GeoProperty, expandedGeoProperty.second[0]) ) } @@ -145,8 +145,8 @@ class AttributeInstanceUtilsTests { NGSILD_TEST_CORE_CONTEXTS ) assertEquals( - TemporalEntityAttribute.AttributeValueType.JSON, - guessAttributeValueType(TemporalEntityAttribute.AttributeType.JsonProperty, expandedJsonProperty.second[0]) + Attribute.AttributeValueType.JSON, + guessAttributeValueType(Attribute.AttributeType.JsonProperty, expandedJsonProperty.second[0]) ) } @@ -158,9 +158,9 @@ class AttributeInstanceUtilsTests { NGSILD_TEST_CORE_CONTEXTS ) assertEquals( - TemporalEntityAttribute.AttributeValueType.URI, + Attribute.AttributeValueType.URI, guessAttributeValueType( - TemporalEntityAttribute.AttributeType.Relationship, + Attribute.AttributeType.Relationship, expandedGeoRelationship.second[0] ) ) diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/entity/util/EntitiesQueryUtilsTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/entity/util/EntitiesQueryUtilsTests.kt new file mode 100644 index 000000000..3e05edf96 --- /dev/null +++ b/search-service/src/test/kotlin/com/egm/stellio/search/entity/util/EntitiesQueryUtilsTests.kt @@ -0,0 +1,257 @@ +package com.egm.stellio.search.entity.util + +import arrow.core.Either +import arrow.core.raise.either +import com.egm.stellio.search.common.model.Query +import com.egm.stellio.search.common.model.Query.Companion.invoke +import com.egm.stellio.search.entity.model.EntitiesQuery +import com.egm.stellio.search.support.buildDefaultPagination +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.GeoQuery +import com.egm.stellio.shared.util.APIARY_TYPE +import com.egm.stellio.shared.util.APIC_COMPOUND_CONTEXTS +import com.egm.stellio.shared.util.BEEHIVE_TYPE +import com.egm.stellio.shared.util.GEO_QUERY_GEOREL_EQUALS +import com.egm.stellio.shared.util.INCOMING_PROPERTY +import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_DEFAULT_VOCAB +import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_OBSERVATION_SPACE_PROPERTY +import com.egm.stellio.shared.util.NGSILD_TEST_CORE_CONTEXTS +import com.egm.stellio.shared.util.OUTGOING_PROPERTY +import com.egm.stellio.shared.util.shouldFailWith +import com.egm.stellio.shared.util.shouldSucceedAndResult +import com.egm.stellio.shared.util.shouldSucceedWith +import com.egm.stellio.shared.util.toUri +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.springframework.test.context.ActiveProfiles +import org.springframework.util.LinkedMultiValueMap +import org.springframework.util.MultiValueMap +import java.net.URI + +@ActiveProfiles("test") +class EntitiesQueryUtilsTests { + + @Test + fun `it should parse query parameters`() = runTest { + val requestParams = gimmeEntitiesQueryParams() + val entitiesQuery = composeEntitiesQuery( + buildDefaultPagination(1, 20), + requestParams, + APIC_COMPOUND_CONTEXTS + ).shouldSucceedAndResult() + + assertEquals("$BEEHIVE_TYPE,$APIARY_TYPE", entitiesQuery.typeSelection) + assertEquals(setOf(INCOMING_PROPERTY, OUTGOING_PROPERTY), entitiesQuery.attrs) + assertEquals( + setOf("urn:ngsi-ld:BeeHive:TESTC".toUri(), "urn:ngsi-ld:BeeHive:TESTB".toUri()), + entitiesQuery.ids + ) + assertEquals(".*BeeHive.*", entitiesQuery.idPattern) + assertEquals("brandName!=Mercedes", entitiesQuery.q) + assertEquals(setOf("urn:ngsi-ld:Dataset:Test1", "urn:ngsi-ld:Dataset:Test2"), entitiesQuery.datasetId) + assertEquals(true, entitiesQuery.paginationQuery.count) + assertEquals(1, entitiesQuery.paginationQuery.offset) + assertEquals(10, entitiesQuery.paginationQuery.limit) + } + + @Test + fun `it should decode q in query parameters`() = runTest { + val requestParams = LinkedMultiValueMap() + requestParams.add("q", "speed%3E50%3BfoodName%3D%3Ddietary+fibres") + val entitiesQuery = composeEntitiesQuery( + buildDefaultPagination(30, 100), + requestParams, + NGSILD_TEST_CORE_CONTEXTS + ).shouldSucceedAndResult() + + assertEquals("speed>50;foodName==dietary fibres", entitiesQuery.q) + } + + @Test + fun `it should set default values in query parameters`() = runTest { + val requestParams = LinkedMultiValueMap() + val entitiesQuery = composeEntitiesQuery( + buildDefaultPagination(30, 100), + requestParams, + NGSILD_TEST_CORE_CONTEXTS + ).shouldSucceedAndResult() + + assertEquals(null, entitiesQuery.typeSelection) + assertEquals(emptySet(), entitiesQuery.attrs) + assertEquals(emptySet(), entitiesQuery.ids) + assertEquals(null, entitiesQuery.idPattern) + assertEquals(null, entitiesQuery.q) + assertEquals(emptySet(), entitiesQuery.datasetId) + assertEquals(false, entitiesQuery.paginationQuery.count) + assertEquals(0, entitiesQuery.paginationQuery.offset) + assertEquals(30, entitiesQuery.paginationQuery.limit) + } + + private fun gimmeEntitiesQueryParams(): LinkedMultiValueMap { + val requestParams = LinkedMultiValueMap() + requestParams.add("type", "BeeHive,Apiary") + requestParams.add("attrs", "incoming,outgoing") + requestParams.add("id", "urn:ngsi-ld:BeeHive:TESTC,urn:ngsi-ld:BeeHive:TESTB") + requestParams.add("idPattern", ".*BeeHive.*") + requestParams.add("q", "brandName!=Mercedes") + requestParams.add("datasetId", "urn:ngsi-ld:Dataset:Test1,urn:ngsi-ld:Dataset:Test2") + requestParams.add("count", "true") + requestParams.add("offset", "1") + requestParams.add("limit", "10") + requestParams.add("options", "keyValues") + return requestParams + } + + @Test + fun `it should parse a valid complete query`() = runTest { + val query = """ + { + "type": "Query", + "entities": [{ + "id": "urn:ngsi-ld:BeeHive:TESTC", + "idPattern": "urn:ngsi-ld:BeeHive:*", + "type": "BeeHive" + }], + "attrs": ["attr1", "attr2"], + "q": "temperature>32", + "geoQ": { + "geometry": "Point", + "coordinates": [1.0, 1.0], + "georel": "equals", + "geoproperty": "observationSpace" + }, + "temporalQ": { + "timerel": "between", + "timeAt": "2023-10-01T12:34:56Z", + "endTimeAt": "2023-10-02T12:34:56Z", + "lastN": 10, + "timeproperty": "observedAt" + }, + "scopeQ": "/Nantes", + "datasetId": ["urn:ngsi-ld:Dataset:Test1", "urn:ngsi-ld:Dataset:Test2"] + } + """.trimIndent() + + composeEntitiesQueryFromPostRequest( + buildDefaultPagination(30, 100), + query, + LinkedMultiValueMap(), + APIC_COMPOUND_CONTEXTS + ).shouldSucceedWith { + assertEquals(setOf("urn:ngsi-ld:BeeHive:TESTC".toUri()), it.ids) + assertEquals("urn:ngsi-ld:BeeHive:*", it.idPattern) + assertEquals(BEEHIVE_TYPE, it.typeSelection) + assertEquals(setOf("${NGSILD_DEFAULT_VOCAB}attr1", "${NGSILD_DEFAULT_VOCAB}attr2"), it.attrs) + assertEquals("temperature>32", it.q) + assertEquals(GeoQuery.GeometryType.POINT, it.geoQuery?.geometry) + assertEquals("[1.0, 1.0]", it.geoQuery?.coordinates) + assertEquals(GEO_QUERY_GEOREL_EQUALS, it.geoQuery?.georel) + assertEquals(NGSILD_OBSERVATION_SPACE_PROPERTY, it.geoQuery?.geoproperty) + assertEquals("/Nantes", it.scopeQ) + assertEquals(setOf("urn:ngsi-ld:Dataset:Test1", "urn:ngsi-ld:Dataset:Test2"), it.datasetId) + } + } + + @Test + fun `it should parse a valid simple query`() = runTest { + val query = """ + { + "type": "Query", + "entities": [{ + "type": "BeeHive" + }], + "attrs": ["attr1"], + "q": "temperature>32" + } + """.trimIndent() + + composeEntitiesQueryFromPostRequest( + buildDefaultPagination(30, 100), + query, + LinkedMultiValueMap(), + APIC_COMPOUND_CONTEXTS + ).shouldSucceedWith { + assertEquals(BEEHIVE_TYPE, it.typeSelection) + assertEquals(setOf("${NGSILD_DEFAULT_VOCAB}attr1"), it.attrs) + assertEquals("temperature>32", it.q) + } + } + + @Test + fun `it should not validate a query if the type is not correct`() { + val query = """ + { + "type": "NotAQuery", + "attrs": ["attr1", "attr2"] + } + """.trimIndent() + + composeEntitiesQueryFromPostRequest( + buildDefaultPagination(30, 100), + query, + LinkedMultiValueMap(), + APIC_COMPOUND_CONTEXTS + ).shouldFailWith { + it is BadRequestDataException && + it.message == "The type parameter should be equals to 'Query'" + } + } + + @Test + fun `it should not validate a query if the payload could not be parsed because the JSON is invalid`() { + val query = """ + { + "type": "Query",, + "attrs": ["attr1", "attr2"] + } + """.trimIndent() + + composeEntitiesQueryFromPostRequest( + buildDefaultPagination(30, 100), + query, + LinkedMultiValueMap(), + APIC_COMPOUND_CONTEXTS + ).shouldFailWith { + it is BadRequestDataException && + it.message.startsWith("The supplied query could not be parsed") + } + } + + @Test + fun `it should not validate a query if the payload contains unexpected parameters`() { + val query = """ + { + "type": "Query", + "property": "anUnexpectedProperty" + } + """.trimIndent() + + composeEntitiesQueryFromPostRequest( + buildDefaultPagination(30, 100), + query, + LinkedMultiValueMap(), + APIC_COMPOUND_CONTEXTS + ).shouldFailWith { + it is BadRequestDataException && + it.message.startsWith("The supplied query could not be parsed") + } + } + + private fun composeEntitiesQueryFromPostRequest( + defaultPagination: ApplicationProperties.Pagination, + requestBody: String, + requestParams: MultiValueMap, + contexts: List + ): Either = either { + val query = Query(requestBody).bind() + composeEntitiesQueryFromPostRequest( + defaultPagination, + query, + requestParams, + contexts + ).bind() + } +} diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/util/PatchAttributeTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/entity/util/PatchAttributeTests.kt similarity index 98% rename from search-service/src/test/kotlin/com/egm/stellio/search/util/PatchAttributeTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/entity/util/PatchAttributeTests.kt index acb08d941..df243327c 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/util/PatchAttributeTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/entity/util/PatchAttributeTests.kt @@ -1,4 +1,4 @@ -package com.egm.stellio.search.util +package com.egm.stellio.search.entity.util import com.egm.stellio.shared.util.JsonLdUtils.expandAttribute import com.egm.stellio.shared.util.JsonUtils.serializeObject @@ -319,7 +319,7 @@ class PatchAttributeTests { } @ParameterizedTest - @MethodSource("com.egm.stellio.search.util.PatchAttributeTests#partialUpdatePatchProvider") + @MethodSource("com.egm.stellio.search.entity.util.PatchAttributeTests#partialUpdatePatchProvider") fun `it should apply a partial update patch behavior to attribute instance`( source: String, target: String, @@ -337,7 +337,7 @@ class PatchAttributeTests { } @ParameterizedTest - @MethodSource("com.egm.stellio.search.util.PatchAttributeTests#mergePatchProvider") + @MethodSource("com.egm.stellio.search.entity.util.PatchAttributeTests#mergePatchProvider") fun `it should apply a merge patch behavior to attribute instance`( source: String, target: String, diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityHandlerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/entity/web/EntityHandlerTests.kt similarity index 78% rename from search-service/src/test/kotlin/com/egm/stellio/search/web/EntityHandlerTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/entity/web/EntityHandlerTests.kt index 3f555a4c4..2eddfb4bc 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityHandlerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/entity/web/EntityHandlerTests.kt @@ -1,13 +1,11 @@ -package com.egm.stellio.search.web +package com.egm.stellio.search.entity.web import arrow.core.left import arrow.core.right -import com.egm.stellio.search.authorization.AuthorizationService -import com.egm.stellio.search.config.SearchProperties -import com.egm.stellio.search.model.* -import com.egm.stellio.search.service.EntityEventService -import com.egm.stellio.search.service.EntityPayloadService -import com.egm.stellio.search.service.QueryService +import com.egm.stellio.search.common.config.SearchProperties +import com.egm.stellio.search.entity.model.* +import com.egm.stellio.search.entity.service.EntityQueryService +import com.egm.stellio.search.entity.service.EntityService import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.model.* import com.egm.stellio.shared.util.* @@ -26,7 +24,6 @@ import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_RELATIONSHIP_TYPE import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_TIME_TYPE import com.ninjasquad.springmockk.MockkBean import io.mockk.* -import kotlinx.coroutines.Job import kotlinx.coroutines.test.runTest import org.hamcrest.core.Is import org.junit.jupiter.api.BeforeAll @@ -44,7 +41,6 @@ import org.springframework.security.test.web.reactive.server.SecurityMockServerC import org.springframework.test.context.ActiveProfiles import org.springframework.test.web.reactive.server.WebTestClient import java.lang.reflect.UndeclaredThrowableException -import java.net.URI import java.time.* @ActiveProfiles("test") @@ -59,16 +55,10 @@ class EntityHandlerTests { private lateinit var applicationProperties: ApplicationProperties @MockkBean - private lateinit var entityPayloadService: EntityPayloadService + private lateinit var entityService: EntityService @MockkBean - private lateinit var queryService: QueryService - - @MockkBean(relaxed = true) - private lateinit var authorizationService: AuthorizationService - - @MockkBean - private lateinit var entityEventService: EntityEventService + private lateinit var entityQueryService: EntityQueryService @BeforeAll fun configureWebClientDefaults() { @@ -83,8 +73,6 @@ class EntityHandlerTests { } private val beehiveId = "urn:ngsi-ld:BeeHive:TESTC".toUri() - private val breedingServiceType = "https://ontology.eglobalmark.com/aquac#BreedingService" - private val deadFishesType = "https://ontology.eglobalmark.com/aquac#DeadFishes" private val fishNumberAttribute = "https://ontology.eglobalmark.com/aquac#fishNumber" private val fishSizeAttribute = "https://ontology.eglobalmark.com/aquac#fishSize" @@ -93,13 +81,9 @@ class EntityHandlerTests { val jsonLdFile = ClassPathResource("/ngsild/aquac/breedingService.jsonld") val breedingServiceId = "urn:ngsi-ld:BreedingService:0214".toUri() - coEvery { authorizationService.userCanCreateEntities(sub) } returns Unit.right() - coEvery { entityPayloadService.checkEntityExistence(any(), any()) } returns Unit.right() coEvery { - entityPayloadService.createEntity(any(), any(), any()) + entityService.createEntity(any(), any(), any()) } returns Unit.right() - coEvery { authorizationService.createOwnerRight(any(), any()) } returns Unit.right() - coEvery { entityEventService.publishEntityCreateEvent(any(), any(), any()) } returns Job() webClient.post() .uri("/ngsi-ld/v1/entities") @@ -109,23 +93,13 @@ class EntityHandlerTests { .expectHeader().value("Location", Is.`is`("/ngsi-ld/v1/entities/$breedingServiceId")) coVerify { - authorizationService.userCanCreateEntities(sub) - entityPayloadService.checkEntityExistence(any(), true) - entityPayloadService.createEntity( + entityService.createEntity( match { it.id == breedingServiceId }, any(), any() ) - authorizationService.createOwnerRight(eq(breedingServiceId), sub) - } - coVerify { - entityEventService.publishEntityCreateEvent( - eq("60AAEBA3-C0C7-42B6-8CB0-0D30857F210E"), - eq(breedingServiceId), - eq(listOf(breedingServiceType)) - ) } } @@ -133,9 +107,8 @@ class EntityHandlerTests { fun `create entity should return a 409 if the entity already exists`() { val jsonLdFile = ClassPathResource("/ngsild/aquac/breedingService.jsonld") - coEvery { authorizationService.userCanCreateEntities(sub) } returns Unit.right() coEvery { - entityPayloadService.checkEntityExistence(any(), any()) + entityService.createEntity(any(), any(), MOCK_USER_SUB) } returns AlreadyExistsException("Already Exists").left() webClient.post() @@ -148,18 +121,14 @@ class EntityHandlerTests { "\"title\":\"The referred element already exists\"," + "\"detail\":\"Already Exists\"}" ) - - verify { entityEventService wasNot called } } @Test fun `create entity should return a 500 error if there is an internal server error`() { val jsonLdFile = ClassPathResource("/ngsild/aquac/breedingService.jsonld") - coEvery { authorizationService.userCanCreateEntities(sub) } returns Unit.right() - coEvery { entityPayloadService.checkEntityExistence(any(), any()) } returns Unit.right() coEvery { - entityPayloadService.createEntity(any(), any(), any()) + entityService.createEntity(any(), any(), any()) } throws InternalErrorException("Internal Server Exception") webClient.post() @@ -228,11 +197,9 @@ class EntityHandlerTests { fun `create entity should return a 400 if creation unexpectedly fails`() { val jsonLdFile = ClassPathResource("/ngsild/aquac/breedingService.jsonld") - coEvery { authorizationService.userCanCreateEntities(sub) } returns Unit.right() - coEvery { entityPayloadService.checkEntityExistence(any(), any()) } returns Unit.right() // reproduce the runtime behavior where the raised exception is wrapped in an UndeclaredThrowableException coEvery { - entityPayloadService.createEntity(any(), any(), any()) + entityService.createEntity(any(), any(), any()) } throws UndeclaredThrowableException(BadRequestDataException("Target entity does not exist")) webClient.post() @@ -256,7 +223,7 @@ class EntityHandlerTests { val jsonLdFile = ClassPathResource("/ngsild/aquac/breedingService.jsonld") coEvery { - authorizationService.userCanCreateEntities(sub) + entityService.createEntity(any(), any(), sub.getOrNull()) } returns AccessDeniedException("User forbidden to create entities").left() webClient.post() @@ -275,18 +242,10 @@ class EntityHandlerTests { ) } - private fun mockkDefaultBehaviorForGetEntityById() { - coEvery { entityPayloadService.checkEntityExistence(any()) } returns Unit.right() - coEvery { entityPayloadService.getTypes(any()) } returns listOf(BEEHIVE_TYPE).right() - coEvery { authorizationService.userCanReadEntity(beehiveId, sub) } returns Unit.right() - } - @Test fun `get entity by id should return 200 when entity exists`() { - mockkDefaultBehaviorForGetEntityById() - val returnedExpandedEntity = mockkClass(ExpandedEntity::class, relaxed = true) - coEvery { queryService.queryEntity(any()) } returns returnedExpandedEntity.right() + coEvery { entityQueryService.queryEntity(any(), MOCK_USER_SUB) } returns returnedExpandedEntity.right() every { returnedExpandedEntity.checkContainsAnyOf(any()) } returns Unit.right() webClient.get() @@ -298,9 +257,7 @@ class EntityHandlerTests { @Test fun `get entity by id should correctly serialize temporal properties`() { - mockkDefaultBehaviorForGetEntityById() - - coEvery { queryService.queryEntity(any()) } returns ExpandedEntity( + coEvery { entityQueryService.queryEntity(any(), MOCK_USER_SUB) } returns ExpandedEntity( mapOf( NGSILD_CREATED_AT_PROPERTY to mapOf( @@ -329,8 +286,6 @@ class EntityHandlerTests { @Test fun `get entity by id should correctly filter the asked attributes`() = runTest { - mockkDefaultBehaviorForGetEntityById() - val entity = """ { "id": "$beehiveId", @@ -350,7 +305,7 @@ class EntityHandlerTests { """.trimIndent() val expandedEntity = expandJsonLdEntity(entity) - coEvery { queryService.queryEntity(any()) } returns expandedEntity.right() + coEvery { entityQueryService.queryEntity(any(), MOCK_USER_SUB) } returns expandedEntity.right() webClient.get() .uri("/ngsi-ld/v1/entities/$beehiveId?attrs=attr2") @@ -364,9 +319,7 @@ class EntityHandlerTests { @Test fun `get entity by id should correctly return the simplified representation of an entity`() { - mockkDefaultBehaviorForGetEntityById() - - coEvery { queryService.queryEntity(any()) } returns ExpandedEntity( + coEvery { entityQueryService.queryEntity(any(), MOCK_USER_SUB) } returns ExpandedEntity( mapOf( "@id" to beehiveId.toString(), "@type" to listOf("Beehive"), @@ -406,9 +359,7 @@ class EntityHandlerTests { @Test fun `get entity by id should return 404 if the entity has none of the requested attributes`() { - mockkDefaultBehaviorForGetEntityById() - - coEvery { queryService.queryEntity(any()) } returns ExpandedEntity( + coEvery { entityQueryService.queryEntity(any(), MOCK_USER_SUB) } returns ExpandedEntity( mapOf( "@id" to beehiveId.toString(), "@type" to listOf(BEEHIVE_TYPE) @@ -437,9 +388,7 @@ class EntityHandlerTests { @Test fun `get entity by id should not include temporal properties if optional query param sysAttrs is not present`() { - mockkDefaultBehaviorForGetEntityById() - - coEvery { queryService.queryEntity(any()) } returns ExpandedEntity( + coEvery { entityQueryService.queryEntity(any(), MOCK_USER_SUB) } returns ExpandedEntity( mapOf( "@id" to beehiveId.toString(), "@type" to listOf("Beehive") @@ -458,8 +407,7 @@ class EntityHandlerTests { @Test fun `get entity by id should correctly serialize properties of type DateTime and display sysAttrs asked`() { - mockkDefaultBehaviorForGetEntityById() - coEvery { queryService.queryEntity(any()) } returns ExpandedEntity( + coEvery { entityQueryService.queryEntity(any(), MOCK_USER_SUB) } returns ExpandedEntity( mapOf( NGSILD_CREATED_AT_PROPERTY to mapOf( @@ -514,9 +462,7 @@ class EntityHandlerTests { @Test fun `get entity by id should correctly serialize properties of type Date`() { - mockkDefaultBehaviorForGetEntityById() - - coEvery { queryService.queryEntity(any()) } returns ExpandedEntity( + coEvery { entityQueryService.queryEntity(any(), MOCK_USER_SUB) } returns ExpandedEntity( mapOf( "https://uri.etsi.org/ngsi-ld/default-context/testedAt" to mapOf( "@type" to "https://uri.etsi.org/ngsi-ld/Property", @@ -553,9 +499,7 @@ class EntityHandlerTests { @Test fun `get entity by id should correctly serialize properties of type Time`() { - mockkDefaultBehaviorForGetEntityById() - - coEvery { queryService.queryEntity(any()) } returns ExpandedEntity( + coEvery { entityQueryService.queryEntity(any(), MOCK_USER_SUB) } returns ExpandedEntity( mapOf( "https://uri.etsi.org/ngsi-ld/default-context/testedAt" to mapOf( "@type" to "https://uri.etsi.org/ngsi-ld/Property", @@ -592,9 +536,7 @@ class EntityHandlerTests { @Test fun `get entity by id should correctly serialize multi-attribute property having one instance`() { - mockkDefaultBehaviorForGetEntityById() - - coEvery { queryService.queryEntity(any()) } returns ExpandedEntity( + coEvery { entityQueryService.queryEntity(any(), MOCK_USER_SUB) } returns ExpandedEntity( mapOf( "https://uri.etsi.org/ngsi-ld/default-context/name" to mapOf( @@ -628,9 +570,7 @@ class EntityHandlerTests { @Test fun `get entity by id should correctly serialize multi-attribute property having more than one instance`() { - mockkDefaultBehaviorForGetEntityById() - - coEvery { queryService.queryEntity(any()) } returns ExpandedEntity( + coEvery { entityQueryService.queryEntity(any(), MOCK_USER_SUB) } returns ExpandedEntity( mapOf( "https://uri.etsi.org/ngsi-ld/default-context/name" to listOf( @@ -680,9 +620,7 @@ class EntityHandlerTests { @Test fun `get entity by id should correctly serialize multi-attribute relationship having one instance`() { - mockkDefaultBehaviorForGetEntityById() - - coEvery { queryService.queryEntity(any()) } returns ExpandedEntity( + coEvery { entityQueryService.queryEntity(any(), MOCK_USER_SUB) } returns ExpandedEntity( mapOf( "https://uri.etsi.org/ngsi-ld/default-context/managedBy" to mapOf( @@ -722,9 +660,7 @@ class EntityHandlerTests { @Test fun `get entity by id should include createdAt & modifiedAt if query param sysAttrs is present`() { - mockkDefaultBehaviorForGetEntityById() - - coEvery { queryService.queryEntity(any()) } returns ExpandedEntity( + coEvery { entityQueryService.queryEntity(any(), MOCK_USER_SUB) } returns ExpandedEntity( mapOf( "https://uri.etsi.org/ngsi-ld/default-context/managedBy" to mapOf( @@ -763,9 +699,7 @@ class EntityHandlerTests { @Test fun `get entity by id should correctly serialize multi-attribute relationship having more than one instance`() { - mockkDefaultBehaviorForGetEntityById() - - coEvery { queryService.queryEntity(any()) } returns ExpandedEntity( + coEvery { entityQueryService.queryEntity(any(), MOCK_USER_SUB) } returns ExpandedEntity( mapOf( "https://uri.etsi.org/ngsi-ld/default-context/managedBy" to listOf( @@ -820,7 +754,7 @@ class EntityHandlerTests { @Test fun `get entity by id should return 404 when entity does not exist`() { coEvery { - entityPayloadService.checkEntityExistence(any()) + entityQueryService.queryEntity(any(), MOCK_USER_SUB) } returns ResourceNotFoundException(entityNotFoundMessage("urn:ngsi-ld:BeeHive:TEST")).left() webClient.get() @@ -837,10 +771,8 @@ class EntityHandlerTests { @Test fun `get entity by id should return 403 if user is not authorized to read an entity`() { - coEvery { entityPayloadService.checkEntityExistence(any()) } returns Unit.right() - coEvery { entityPayloadService.getTypes(any()) } returns listOf(BEEHIVE_TYPE).right() coEvery { - authorizationService.userCanReadEntity("urn:ngsi-ld:BeeHive:TEST".toUri(), sub) + entityQueryService.queryEntity("urn:ngsi-ld:BeeHive:TEST".toUri(), sub.getOrNull()) } returns AccessDeniedException("User forbidden read access to entity urn:ngsi-ld:BeeHive:TEST").left() webClient.get() @@ -861,7 +793,7 @@ class EntityHandlerTests { @Test fun `get entities by type should not include temporal properties if query param sysAttrs is not present`() { - coEvery { queryService.queryEntities(any(), any()) } returns Pair( + coEvery { entityQueryService.queryEntities(any(), any()) } returns Pair( listOf( ExpandedEntity( mapOf( @@ -896,13 +828,13 @@ class EntityHandlerTests { @Test fun `get entities by type should include temporal properties if optional query param sysAttrs is present`() { coEvery { - queryService.queryEntities( + entityQueryService.queryEntities( EntitiesQuery( typeSelection = "https://uri.etsi.org/ngsi-ld/default-context/Beehive", paginationQuery = PaginationQuery(offset = 0, limit = 30), contexts = listOf(applicationProperties.contexts.core) ), - any() + any() ) } returns Pair( listOf( @@ -942,7 +874,7 @@ class EntityHandlerTests { @Test fun `get entities should return 200 with prev and next link header if exists`() { - coEvery { queryService.queryEntities(any(), any()) } returns Pair( + coEvery { entityQueryService.queryEntities(any(), any()) } returns Pair( listOf( ExpandedEntity( mapOf("@id" to "urn:ngsi-ld:Beehive:TESTC", "@type" to listOf("Beehive")) @@ -981,7 +913,7 @@ class EntityHandlerTests { @Test fun `get entities should return 200 and empty response if requested offset does not exists`() { coEvery { - queryService.queryEntities(any(), any()) + entityQueryService.queryEntities(any(), any()) } returns Pair(emptyList(), 0).right() webClient.get() @@ -1028,14 +960,14 @@ class EntityHandlerTests { @Test fun `get entities with id and type should return 200`() { coEvery { - queryService.queryEntities( + entityQueryService.queryEntities( EntitiesQuery( ids = setOf(beehiveId), typeSelection = BEEHIVE_TYPE, paginationQuery = PaginationQuery(offset = 0, limit = 30), contexts = APIC_COMPOUND_CONTEXTS ), - any() + any() ) } returns Pair( listOf( @@ -1070,7 +1002,9 @@ class EntityHandlerTests { @Test fun `get entities should return 200 and the number of results`() { - coEvery { queryService.queryEntities(any(), any()) } returns Pair(emptyList(), 3).right() + coEvery { + entityQueryService.queryEntities(any(), any()) + } returns Pair(emptyList(), 3).right() webClient.get() .uri("/ngsi-ld/v1/entities/?type=Beehive&limit=0&offset=1&count=true") @@ -1099,7 +1033,9 @@ class EntityHandlerTests { @Test fun `get entities should allow a query not including a type request parameter`() { - coEvery { queryService.queryEntities(any(), any()) } returns Pair(emptyList(), 0).right() + coEvery { + entityQueryService.queryEntities(any(), any()) + } returns Pair(emptyList(), 0).right() webClient.get() .uri("/ngsi-ld/v1/entities/?attrs=myProp") @@ -1131,12 +1067,9 @@ class EntityHandlerTests { val jsonLdFile = ClassPathResource("/ngsild/aquac/breedingService.jsonld") val breedingServiceId = "urn:ngsi-ld:BreedingService:0214".toUri() - coEvery { entityPayloadService.checkEntityExistence(breedingServiceId) } returns Unit.right() - coEvery { authorizationService.userCanUpdateEntity(breedingServiceId, sub) } returns Unit.right() coEvery { - entityPayloadService.replaceEntity(any(), any(), any(), any()) + entityService.replaceEntity(any(), any(), any(), any()) } returns Unit.right() - coEvery { entityEventService.publishEntityReplaceEvent(any(), any(), any()) } returns Job() webClient.put() .uri("/ngsi-ld/v1/entities/$breedingServiceId") @@ -1145,9 +1078,7 @@ class EntityHandlerTests { .expectStatus().isNoContent coVerify { - authorizationService.userCanUpdateEntity(breedingServiceId, sub) - entityPayloadService.checkEntityExistence(breedingServiceId) - entityPayloadService.replaceEntity( + entityService.replaceEntity( eq(breedingServiceId), match { it.id == breedingServiceId @@ -1156,13 +1087,6 @@ class EntityHandlerTests { any() ) } - coVerify { - entityEventService.publishEntityReplaceEvent( - eq("60AAEBA3-C0C7-42B6-8CB0-0D30857F210E"), - eq(breedingServiceId), - eq(listOf(breedingServiceType)) - ) - } } @Test @@ -1170,9 +1094,8 @@ class EntityHandlerTests { val jsonLdFile = ClassPathResource("/ngsild/aquac/breedingService.jsonld") val breedingServiceId = "urn:ngsi-ld:BreedingService:0214".toUri() - coEvery { entityPayloadService.checkEntityExistence(breedingServiceId) } returns Unit.right() coEvery { - authorizationService.userCanUpdateEntity(breedingServiceId, sub) + entityService.replaceEntity(breedingServiceId, any(), any(), sub.getOrNull()) } returns AccessDeniedException("User forbidden to modify entity").left() webClient.put() @@ -1197,15 +1120,14 @@ class EntityHandlerTests { val breedingServiceId = "urn:ngsi-ld:BreedingService:0214".toUri() coEvery { - entityPayloadService.checkEntityExistence(eq(breedingServiceId)) + entityService.replaceEntity(breedingServiceId, any(), any(), sub.getOrNull()) } returns ResourceNotFoundException(entityNotFoundMessage(breedingServiceId.toString())).left() + webClient.put() .uri("/ngsi-ld/v1/entities/$breedingServiceId") .bodyValue(jsonLdFile) .exchange() .expectStatus().isNotFound - - coVerify { entityPayloadService.checkEntityExistence(eq(breedingServiceId)) } } @Test @@ -1213,9 +1135,6 @@ class EntityHandlerTests { val jsonLdFile = ClassPathResource("/ngsild/aquac/breedingService.jsonld") val breedingServiceId = "urn:ngsi-ld:BreedingService:0215".toUri() - coEvery { entityPayloadService.checkEntityExistence(breedingServiceId) } returns Unit.right() - coEvery { authorizationService.userCanUpdateEntity(breedingServiceId, sub) } returns Unit.right() - webClient.put() .uri("/ngsi-ld/v1/entities/$breedingServiceId") .bodyValue(jsonLdFile) @@ -1249,17 +1168,6 @@ class EntityHandlerTests { ) } - private fun mockkDefaultBehaviorForAppendAttribute() { - coEvery { entityPayloadService.checkEntityExistence(any()) } returns Unit.right() - coEvery { entityPayloadService.getTypes(any()) } returns listOf(breedingServiceType).right() - coEvery { - authorizationService.userCanUpdateEntity(any(), sub) - } returns Unit.right() - coEvery { - entityEventService.publishAttributeChangeEvents(any(), any(), any(), any(), true) - } returns Job() - } - @Test fun `append entity attribute should return a 204 if all attributes were appended`() { val jsonLdFile = ClassPathResource("/ngsild/aquac/fragments/BreedingService_newProperty.json") @@ -1275,9 +1183,8 @@ class EntityHandlerTests { emptyList() ) - mockkDefaultBehaviorForAppendAttribute() coEvery { - entityPayloadService.appendAttributes(any(), any(), any(), any()) + entityService.appendAttributes(any(), any(), any(), any()) } returns appendResult.right() webClient.post() @@ -1289,24 +1196,13 @@ class EntityHandlerTests { .expectStatus().isNoContent coVerify { - entityPayloadService.checkEntityExistence(eq(entityId)) - authorizationService.userCanUpdateEntity(eq(entityId), eq(sub)) - entityPayloadService.appendAttributes( + entityService.appendAttributes( eq(entityId), any(), eq(false), sub.getOrNull() ) } - coVerify { - entityEventService.publishAttributeChangeEvents( - eq("60AAEBA3-C0C7-42B6-8CB0-0D30857F210E"), - eq(entityId), - any(), - appendResult, - true - ) - } } @Test @@ -1324,9 +1220,8 @@ class EntityHandlerTests { listOf(NotUpdatedDetails(fishSizeAttribute, "overwrite disallowed")) ) - mockkDefaultBehaviorForAppendAttribute() coEvery { - entityPayloadService.appendAttributes(any(), any(), any(), any()) + entityService.appendAttributes(any(), any(), any(), any()) } returns appendResult.right() webClient.post() @@ -1346,23 +1241,13 @@ class EntityHandlerTests { ) coVerify { - entityPayloadService.checkEntityExistence(eq(entityId)) - entityPayloadService.appendAttributes( + entityService.appendAttributes( eq(entityId), any(), eq(false), sub.getOrNull() ) } - coVerify { - entityEventService.publishAttributeChangeEvents( - eq("60AAEBA3-C0C7-42B6-8CB0-0D30857F210E"), - eq(entityId), - any(), - appendResult, - true - ) - } } @Test @@ -1374,9 +1259,8 @@ class EntityHandlerTests { emptyList() ) - mockkDefaultBehaviorForAppendAttribute() coEvery { - entityPayloadService.appendAttributes(any(), any(), any(), any()) + entityService.appendAttributes(any(), any(), any(), any()) } returns appendTypeResult.right() webClient.post() @@ -1388,23 +1272,13 @@ class EntityHandlerTests { .expectStatus().isEqualTo(HttpStatus.NO_CONTENT) coVerify { - entityPayloadService.checkEntityExistence(eq(entityId)) - entityPayloadService.appendAttributes( + entityService.appendAttributes( eq(entityId), any(), eq(false), any() ) } - coVerify { - entityEventService.publishAttributeChangeEvents( - eq("60AAEBA3-C0C7-42B6-8CB0-0D30857F210E"), - eq(entityId), - any(), - appendTypeResult, - true - ) - } } @Test @@ -1420,9 +1294,8 @@ class EntityHandlerTests { listOf(NotUpdatedDetails(fishSizeAttribute, "overwrite disallowed")) ) - mockkDefaultBehaviorForAppendAttribute() coEvery { - entityPayloadService.appendAttributes(any(), any(), any(), any()) + entityService.appendAttributes(any(), any(), any(), any()) } returns appendTypeResult.mergeWith(appendResult).right() webClient.post() @@ -1443,16 +1316,6 @@ class EntityHandlerTests { } """.trimIndent() ) - - coVerify { - entityEventService.publishAttributeChangeEvents( - eq("60AAEBA3-C0C7-42B6-8CB0-0D30857F210E"), - eq(entityId), - any(), - appendTypeResult.mergeWith(appendResult), - true - ) - } } @Test @@ -1461,12 +1324,13 @@ class EntityHandlerTests { val entityId = "urn:ngsi-ld:BreedingService:0214".toUri() coEvery { - entityPayloadService.checkEntityExistence(eq(entityId)) + entityService.appendAttributes(eq(entityId), any(), any(), MOCK_USER_SUB) } returns ResourceNotFoundException(entityNotFoundMessage(entityId.toString())).left() webClient.post() .uri("/ngsi-ld/v1/entities/$entityId/attrs") .header(HttpHeaders.LINK, AQUAC_HEADER_LINK) + .contentType(MediaType.APPLICATION_JSON) .bodyValue(jsonLdFile) .exchange() .expectStatus().isNotFound @@ -1475,13 +1339,6 @@ class EntityHandlerTests { "\"title\":\"The referred resource has not been found\"," + "\"detail\":\"Entity urn:ngsi-ld:BreedingService:0214 was not found\"}" ) - - coVerify { - entityPayloadService.checkEntityExistence(eq(entityId)) - } - verify { - entityEventService wasNot called - } } @Test @@ -1497,10 +1354,8 @@ class EntityHandlerTests { } """.trimIndent() - coEvery { entityPayloadService.checkEntityExistence(any()) } returns Unit.right() - coEvery { authorizationService.userCanUpdateEntity(any(), any()) } returns Unit.right() coEvery { - entityPayloadService.appendAttributes(any(), any(), any(), any()) + entityService.appendAttributes(any(), any(), any(), any()) } returns BadRequestDataException( "Relationship https://ontology.eglobalmark.com/egm#connectsTo does not have an object field" ).left() @@ -1528,10 +1383,8 @@ class EntityHandlerTests { val jsonLdFile = ClassPathResource("/ngsild/aquac/fragments/BreedingService_newProperty.json") val entityId = "urn:ngsi-ld:BreedingService:0214".toUri() - coEvery { entityPayloadService.checkEntityExistence(any()) } returns Unit.right() - coEvery { entityPayloadService.getTypes(any()) } returns listOf(breedingServiceType).right() coEvery { - authorizationService.userCanUpdateEntity(entityId, sub) + entityService.appendAttributes(entityId, any(), any(), sub.getOrNull()) } returns AccessDeniedException("User forbidden write access to entity urn:ngsi-ld:BreedingService:0214").left() webClient.post() @@ -1550,14 +1403,6 @@ class EntityHandlerTests { } """.trimIndent() ) - - verify { entityEventService wasNot called } - } - - private fun mockkDefaultBehaviorForPartialUpdateAttribute() { - coEvery { entityPayloadService.checkEntityExistence(any()) } returns Unit.right() - coEvery { entityPayloadService.getTypes(any()) } returns listOf(deadFishesType).right() - coEvery { authorizationService.userCanUpdateEntity(any(), any()) } returns Unit.right() } @Test @@ -1572,13 +1417,9 @@ class EntityHandlerTests { notUpdated = arrayListOf() ) - mockkDefaultBehaviorForPartialUpdateAttribute() coEvery { - entityPayloadService.partialUpdateAttribute(any(), any(), any()) + entityService.partialUpdateAttribute(any(), any(), any()) } returns updateResult.right() - coEvery { - entityEventService.publishAttributeChangeEvents(any(), any(), any(), any(), any()) - } returns Job() webClient.patch() .uri("/ngsi-ld/v1/entities/$entityId/attrs/$attrId") @@ -1589,18 +1430,7 @@ class EntityHandlerTests { .expectStatus().isNoContent coVerify { - entityPayloadService.checkEntityExistence(eq(entityId)) - authorizationService.userCanUpdateEntity(eq(entityId), eq(sub)) - entityPayloadService.partialUpdateAttribute(eq(entityId), any(), sub.getOrNull()) - } - coVerify { - entityEventService.publishAttributeChangeEvents( - eq("60AAEBA3-C0C7-42B6-8CB0-0D30857F210E"), - eq(entityId), - any(), - eq(updateResult), - eq(false) - ) + entityService.partialUpdateAttribute(eq(entityId), any(), sub.getOrNull()) } } @@ -1611,7 +1441,7 @@ class EntityHandlerTests { val attrId = "fishNumber" coEvery { - entityPayloadService.checkEntityExistence(any()) + entityService.partialUpdateAttribute(any(), any(), MOCK_USER_SUB) } returns ResourceNotFoundException(entityNotFoundMessage(entityId.toString())).left() webClient.patch() @@ -1621,8 +1451,6 @@ class EntityHandlerTests { .bodyValue(jsonLdFile) .exchange() .expectStatus().isNotFound - - coVerify { entityPayloadService.checkEntityExistence(eq(entityId)) } } @Test @@ -1631,9 +1459,8 @@ class EntityHandlerTests { val entityId = "urn:ngsi-ld:DeadFishes:019BN".toUri() val attrId = "fishNumber" - mockkDefaultBehaviorForPartialUpdateAttribute() coEvery { - entityPayloadService.partialUpdateAttribute(any(), any(), any()) + entityService.partialUpdateAttribute(any(), any(), any()) } returns UpdateResult( updated = arrayListOf(), notUpdated = arrayListOf( @@ -1653,7 +1480,7 @@ class EntityHandlerTests { .expectStatus().isNotFound coVerify { - entityPayloadService.partialUpdateAttribute(eq(entityId), any(), sub.getOrNull()) + entityService.partialUpdateAttribute(eq(entityId), any(), sub.getOrNull()) } } @@ -1663,10 +1490,8 @@ class EntityHandlerTests { val entityId = "urn:ngsi-ld:DeadFishes:019BN".toUri() val attrId = "fishNumber" - coEvery { entityPayloadService.checkEntityExistence(any()) } returns Unit.right() - coEvery { entityPayloadService.getTypes(any()) } returns listOf(deadFishesType).right() coEvery { - authorizationService.userCanUpdateEntity(any(), sub) + entityService.partialUpdateAttribute(any(), any(), sub.getOrNull()) } returns AccessDeniedException("User forbidden write access to entity urn:ngsi-ld:DeadFishes:019BN").left() webClient.patch() @@ -1685,14 +1510,6 @@ class EntityHandlerTests { } """.trimIndent() ) - - coVerify { authorizationService.userCanUpdateEntity(eq(entityId), sub) } - } - - private fun mockkDefaultBehaviorForUpdateAttribute() { - coEvery { entityPayloadService.checkEntityExistence(any()) } returns Unit.right() - coEvery { entityPayloadService.getTypes(any()) } returns listOf(deadFishesType).right() - coEvery { authorizationService.userCanUpdateEntity(any(), any()) } returns Unit.right() } @Test @@ -1715,14 +1532,9 @@ class EntityHandlerTests { notUpdated = emptyList() ) - coEvery { entityPayloadService.checkEntityExistence(any()) } returns Unit.right() - coEvery { authorizationService.userCanUpdateEntity(any(), any()) } returns Unit.right() coEvery { - entityPayloadService.mergeEntity(any(), any(), any(), any()) + entityService.mergeEntity(any(), any(), any(), any()) } returns updateResult.right() - coEvery { - entityEventService.publishAttributeChangeEvents(any(), any(), any(), any(), true) - } returns Job() webClient.patch() .uri("/ngsi-ld/v1/entities/$entityId") @@ -1733,18 +1545,7 @@ class EntityHandlerTests { .expectStatus().isNoContent coVerify { - entityPayloadService.checkEntityExistence(eq(entityId)) - authorizationService.userCanUpdateEntity(eq(entityId), eq(sub)) - entityPayloadService.mergeEntity(eq(entityId), any(), any(), any()) - } - coVerify { - entityEventService.publishAttributeChangeEvents( - eq("60AAEBA3-C0C7-42B6-8CB0-0D30857F210E"), - eq(entityId), - any(), - eq(updateResult), - true - ) + entityService.mergeEntity(eq(entityId), any(), any(), any()) } } @@ -1768,14 +1569,9 @@ class EntityHandlerTests { notUpdated = emptyList() ) - coEvery { entityPayloadService.checkEntityExistence(any()) } returns Unit.right() - coEvery { authorizationService.userCanUpdateEntity(any(), any()) } returns Unit.right() coEvery { - entityPayloadService.mergeEntity(any(), any(), any(), any()) + entityService.mergeEntity(any(), any(), any(), any()) } returns updateResult.right() - coEvery { - entityEventService.publishAttributeChangeEvents(any(), any(), any(), any(), true) - } returns Job() webClient.patch() .uri("/ngsi-ld/v1/entities/$entityId?observedAt=2019-12-04T12:00:00.00Z") @@ -1786,24 +1582,13 @@ class EntityHandlerTests { .expectStatus().isNoContent coVerify { - entityPayloadService.checkEntityExistence(eq(entityId)) - authorizationService.userCanUpdateEntity(eq(entityId), eq(sub)) - entityPayloadService.mergeEntity( + entityService.mergeEntity( eq(entityId), any(), eq(ZonedDateTime.parse("2019-12-04T12:00:00.00Z")), any() ) } - coVerify { - entityEventService.publishAttributeChangeEvents( - eq("60AAEBA3-C0C7-42B6-8CB0-0D30857F210E"), - eq(entityId), - any(), - eq(updateResult), - true - ) - } } @Test @@ -1811,9 +1596,6 @@ class EntityHandlerTests { val jsonLdFile = ClassPathResource("/ngsild/aquac/fragments/DeadFishes_mergeEntity.json") val entityId = "urn:ngsi-ld:DeadFishes:019BN".toUri() - coEvery { entityPayloadService.checkEntityExistence(any()) } returns Unit.right() - coEvery { authorizationService.userCanUpdateEntity(any(), any()) } returns Unit.right() - webClient.patch() .uri("/ngsi-ld/v1/entities/$entityId?observedAt=notDateTime") .header(HttpHeaders.LINK, AQUAC_HEADER_LINK) @@ -1837,11 +1619,12 @@ class EntityHandlerTests { val entityId = "urn:ngsi-ld:DeadFishes:019BN".toUri() coEvery { - entityPayloadService.checkEntityExistence(any()) + entityService.mergeEntity(any(), any(), any(), MOCK_USER_SUB) } returns ResourceNotFoundException(entityNotFoundMessage(entityId.toString())).left() webClient.patch() .uri("/ngsi-ld/v1/entities/$entityId") + .contentType(MediaType.APPLICATION_JSON) .bodyValue(jsonLdFile) .exchange() .expectStatus().isNotFound @@ -1861,9 +1644,8 @@ class EntityHandlerTests { val jsonLdFile = ClassPathResource("/ngsild/aquac/fragments/DeadFishes_mergeEntity.json") val entityId = "urn:ngsi-ld:DeadFishes:019BN".toUri() - coEvery { entityPayloadService.checkEntityExistence(any()) } returns Unit.right() coEvery { - authorizationService.userCanUpdateEntity(any(), any()) + entityService.mergeEntity(any(), any(), any(), MOCK_USER_SUB) } returns AccessDeniedException("User forbidden write access to entity urn:ngsi-ld:DeadFishes:019BN").left() webClient.patch() @@ -1921,13 +1703,9 @@ class EntityHandlerTests { notUpdated = emptyList() ) - mockkDefaultBehaviorForUpdateAttribute() coEvery { - entityPayloadService.updateAttributes(any(), any(), any()) + entityService.updateAttributes(any(), any(), any()) } returns updateResult.right() - coEvery { - entityEventService.publishAttributeChangeEvents(any(), any(), any(), any(), true) - } returns Job() webClient.patch() .uri("/ngsi-ld/v1/entities/$entityId/attrs") @@ -1938,18 +1716,7 @@ class EntityHandlerTests { .expectStatus().isNoContent coVerify { - entityPayloadService.checkEntityExistence(eq(entityId)) - authorizationService.userCanUpdateEntity(eq(entityId), eq(sub)) - entityPayloadService.updateAttributes(eq(entityId), any(), any()) - } - coVerify { - entityEventService.publishAttributeChangeEvents( - eq("60AAEBA3-C0C7-42B6-8CB0-0D30857F210E"), - eq(entityId), - any(), - eq(updateResult), - true - ) + entityService.updateAttributes(eq(entityId), any(), any()) } } @@ -1961,18 +1728,14 @@ class EntityHandlerTests { val entityId = "urn:ngsi-ld:DeadFishes:019BN".toUri() val notUpdatedAttribute = NotUpdatedDetails("removedFrom", "Property is not valid") - mockkDefaultBehaviorForUpdateAttribute() coEvery { - entityPayloadService.updateAttributes(any(), any(), any()) + entityService.updateAttributes(any(), any(), any()) } returns UpdateResult( updated = arrayListOf( UpdatedDetails(fishNumberAttribute, null, UpdateOperationResult.REPLACED) ), notUpdated = arrayListOf(notUpdatedAttribute) ).right() - coEvery { - entityEventService.publishAttributeChangeEvents(any(), any(), any(), any(), true) - } returns Job() webClient.patch() .uri("/ngsi-ld/v1/entities/$entityId/attrs") @@ -1990,16 +1753,12 @@ class EntityHandlerTests { ) val entityId = "urn:ngsi-ld:DeadFishes:019BN".toUri() - mockkDefaultBehaviorForUpdateAttribute() coEvery { - entityPayloadService.updateAttributes(any(), any(), any()) + entityService.updateAttributes(any(), any(), any()) } returns UpdateResult( updated = emptyList(), notUpdated = listOf(NotUpdatedDetails("type", "A type cannot be removed")) ).right() - coEvery { - entityEventService.publishAttributeChangeEvents(any(), any(), any(), any(), true) - } returns Job() webClient.patch() .uri("/ngsi-ld/v1/entities/$entityId/attrs") @@ -2010,7 +1769,7 @@ class EntityHandlerTests { .expectStatus().isEqualTo(HttpStatus.MULTI_STATUS) coVerify { - entityPayloadService.updateAttributes(eq(entityId), any(), any()) + entityService.updateAttributes(eq(entityId), any(), any()) } } @@ -2044,7 +1803,7 @@ class EntityHandlerTests { """.trimIndent() ) - verify { entityEventService wasNot called } + verify { entityService wasNot called } } @Test @@ -2058,7 +1817,7 @@ class EntityHandlerTests { """.trimIndent() coEvery { - entityPayloadService.checkEntityExistence(any()) + entityService.updateAttributes(any(), any(), MOCK_USER_SUB) } returns ResourceNotFoundException(entityNotFoundMessage(beehiveId.toString())).left() webClient.patch() @@ -2078,10 +1837,8 @@ class EntityHandlerTests { val jsonLdFile = ClassPathResource("/ngsild/aquac/fragments/DeadFishes_updateEntityAttribute.json") val entityId = "urn:ngsi-ld:Sensor:0022CCC".toUri() - coEvery { entityPayloadService.checkEntityExistence(any()) } returns Unit.right() - coEvery { entityPayloadService.getTypes(any()) } returns listOf(deadFishesType).right() coEvery { - authorizationService.userCanUpdateEntity(any(), any()) + entityService.updateAttributes(any(), any(), MOCK_USER_SUB) } returns AccessDeniedException("User forbidden write access to entity urn:ngsi-ld:Sensor:0022CCC").left() webClient.patch() @@ -2104,15 +1861,7 @@ class EntityHandlerTests { @Test fun `delete entity should return a 204 if an entity has been successfully deleted`() { - val entity = mockkClass(EntityPayload::class, relaxed = true) - - coEvery { entityPayloadService.checkEntityExistence(beehiveId) } returns Unit.right() - coEvery { entityPayloadService.retrieve(any()) } returns entity.right() - every { entity.types } returns listOf(BEEHIVE_TYPE) - coEvery { authorizationService.userCanAdminEntity(beehiveId, sub) } returns Unit.right() - coEvery { entityPayloadService.deleteEntity(any()) } returns mockkClass(EntityPayload::class).right() - coEvery { authorizationService.removeRightsOnEntity(any()) } returns Unit.right() - coEvery { entityEventService.publishEntityDeleteEvent(any(), any()) } returns Job() + coEvery { entityService.deleteEntity(any(), MOCK_USER_SUB) } returns Unit.right() webClient.delete() .uri("/ngsi-ld/v1/entities/$beehiveId") @@ -2121,24 +1870,14 @@ class EntityHandlerTests { .expectBody().isEmpty coVerify { - entityPayloadService.checkEntityExistence(beehiveId) - entityPayloadService.retrieve(eq(beehiveId)) - authorizationService.userCanAdminEntity(eq(beehiveId), eq(sub)) - entityPayloadService.deleteEntity(eq(beehiveId)) - authorizationService.removeRightsOnEntity(eq(beehiveId)) - } - coVerify { - entityEventService.publishEntityDeleteEvent( - eq("60AAEBA3-C0C7-42B6-8CB0-0D30857F210E"), - eq(entity) - ) + entityService.deleteEntity(eq(beehiveId), eq(MOCK_USER_SUB)) } } @Test fun `delete entity should return a 404 if entity to be deleted has not been found`() { coEvery { - entityPayloadService.checkEntityExistence(beehiveId) + entityService.deleteEntity(beehiveId, MOCK_USER_SUB) } returns ResourceNotFoundException(entityNotFoundMessage(beehiveId.toString())).left() webClient.delete() @@ -2154,19 +1893,12 @@ class EntityHandlerTests { } """.trimIndent() ) - - verify { entityEventService wasNot called } } @Test fun `delete entity should return a 500 if entity could not be deleted`() { - val entity = mockkClass(EntityPayload::class, relaxed = true) - coEvery { entityPayloadService.checkEntityExistence(beehiveId) } returns Unit.right() - coEvery { entityPayloadService.retrieve(any()) } returns entity.right() - every { entity.types } returns listOf(BEEHIVE_TYPE) - coEvery { authorizationService.userCanAdminEntity(beehiveId, sub) } returns Unit.right() coEvery { - entityPayloadService.deleteEntity(any()) + entityService.deleteEntity(any(), MOCK_USER_SUB) } throws RuntimeException("Unexpected server error") webClient.delete() @@ -2186,12 +1918,8 @@ class EntityHandlerTests { @Test fun `delete entity should return a 403 is user is not authorized to delete an entity`() { - val entity = mockkClass(EntityPayload::class, relaxed = true) - coEvery { entityPayloadService.checkEntityExistence(beehiveId) } returns Unit.right() - coEvery { entityPayloadService.retrieve(beehiveId) } returns entity.right() - every { entity.types } returns listOf(BEEHIVE_TYPE) coEvery { - authorizationService.userCanAdminEntity(beehiveId, sub) + entityService.deleteEntity(beehiveId, sub.getOrNull()) } returns AccessDeniedException("User forbidden admin access to entity $beehiveId").left() webClient.delete() @@ -2210,19 +1938,10 @@ class EntityHandlerTests { ) } - private fun mockkDefaultBehaviorForDeleteAttribute() { - coEvery { entityPayloadService.getTypes(any()) } returns listOf(BEEHIVE_TYPE).right() - coEvery { authorizationService.userCanUpdateEntity(any(), sub) } returns Unit.right() - coEvery { - entityEventService.publishAttributeDeleteEvent(any(), any(), any(), any(), any()) - } returns Job() - } - @Test fun `delete entity attribute should return a 204 if the attribute has been successfully deleted`() { - mockkDefaultBehaviorForDeleteAttribute() coEvery { - entityPayloadService.deleteAttribute(any(), any(), any()) + entityService.deleteAttribute(any(), any(), any(), any(), MOCK_USER_SUB) } returns Unit.right() webClient.method(HttpMethod.DELETE) @@ -2234,29 +1953,20 @@ class EntityHandlerTests { .expectBody().isEmpty coVerify { - authorizationService.userCanUpdateEntity(eq(beehiveId), eq(sub)) - entityPayloadService.deleteAttribute( + entityService.deleteAttribute( eq(beehiveId), eq(TEMPERATURE_PROPERTY), - null - ) - } - coVerify { - entityEventService.publishAttributeDeleteEvent( - eq("60AAEBA3-C0C7-42B6-8CB0-0D30857F210E"), - eq(beehiveId), - eq(TEMPERATURE_PROPERTY), - isNull(), - eq(false) + null, + eq(false), + eq(MOCK_USER_SUB) ) } } @Test fun `delete entity attribute should delete all instances if deleteAll flag is true`() { - mockkDefaultBehaviorForDeleteAttribute() coEvery { - entityPayloadService.deleteAttribute(any(), any(), any(), any()) + entityService.deleteAttribute(any(), any(), any(), any(), MOCK_USER_SUB) } returns Unit.right() webClient.method(HttpMethod.DELETE) @@ -2268,20 +1978,12 @@ class EntityHandlerTests { .expectBody().isEmpty coVerify { - entityPayloadService.deleteAttribute( + entityService.deleteAttribute( eq(beehiveId), eq(TEMPERATURE_PROPERTY), null, - eq(true) - ) - } - coVerify { - entityEventService.publishAttributeDeleteEvent( - eq("60AAEBA3-C0C7-42B6-8CB0-0D30857F210E"), - eq(beehiveId), - eq(TEMPERATURE_PROPERTY), - isNull(), - eq(true) + eq(true), + eq(MOCK_USER_SUB) ) } } @@ -2289,9 +1991,8 @@ class EntityHandlerTests { @Test fun `delete entity attribute should delete instance with the provided datasetId`() { val datasetId = "urn:ngsi-ld:Dataset:temperature:1" - mockkDefaultBehaviorForDeleteAttribute() coEvery { - entityPayloadService.deleteAttribute(any(), any(), any()) + entityService.deleteAttribute(any(), any(), any(), any(), MOCK_USER_SUB) } returns Unit.right() webClient.method(HttpMethod.DELETE) @@ -2303,28 +2004,20 @@ class EntityHandlerTests { .expectBody().isEmpty coVerify { - entityPayloadService.deleteAttribute( - eq(beehiveId), - eq(TEMPERATURE_PROPERTY), - eq(datasetId.toUri()) - ) - } - coVerify { - entityEventService.publishAttributeDeleteEvent( - eq("60AAEBA3-C0C7-42B6-8CB0-0D30857F210E"), + entityService.deleteAttribute( eq(beehiveId), eq(TEMPERATURE_PROPERTY), eq(datasetId.toUri()), - eq(false) + eq(false), + eq(MOCK_USER_SUB) ) } } @Test fun `delete entity attribute should return a 404 if the entity is not found`() { - mockkDefaultBehaviorForDeleteAttribute() coEvery { - entityPayloadService.deleteAttribute(any(), any(), any(), any()) + entityService.deleteAttribute(any(), any(), any(), any(), MOCK_USER_SUB) } returns ResourceNotFoundException(entityNotFoundMessage(beehiveId.toString())).left() webClient.method(HttpMethod.DELETE) @@ -2338,15 +2031,12 @@ class EntityHandlerTests { "\"title\":\"The referred resource has not been found\"," + "\"detail\":\"Entity urn:ngsi-ld:BeeHive:TESTC was not found\"}" ) - - verify { entityEventService wasNot called } } @Test fun `delete entity attribute should return a 404 if the attribute is not found`() { - mockkDefaultBehaviorForDeleteAttribute() coEvery { - entityPayloadService.deleteAttribute(any(), any(), any(), any()) + entityService.deleteAttribute(any(), any(), any(), any(), MOCK_USER_SUB) } throws ResourceNotFoundException("Attribute Not Found") webClient.method(HttpMethod.DELETE) @@ -2360,15 +2050,12 @@ class EntityHandlerTests { "\"title\":\"The referred resource has not been found\"," + "\"detail\":\"Attribute Not Found\"}" ) - - verify { entityEventService wasNot called } } @Test fun `delete entity attribute should return a 400 if the request is not correct`() { - mockkDefaultBehaviorForDeleteAttribute() coEvery { - entityPayloadService.deleteAttribute(any(), any(), any()) + entityService.deleteAttribute(any(), any(), any(), any(), MOCK_USER_SUB) } returns BadRequestDataException("Something is wrong with the request").left() webClient.method(HttpMethod.DELETE) @@ -2386,15 +2073,12 @@ class EntityHandlerTests { } """.trimIndent() ) - - verify { entityEventService wasNot called } } @Test fun `delete entity attribute should return a 403 if user is not allowed to update entity`() { - coEvery { entityPayloadService.getTypes(any()) } returns listOf(BEEHIVE_TYPE).right() coEvery { - authorizationService.userCanUpdateEntity(any(), sub) + entityService.deleteAttribute(any(), any(), any(), any(), sub.getOrNull()) } returns AccessDeniedException("User forbidden write access to entity $beehiveId").left() webClient.method(HttpMethod.DELETE) @@ -2412,23 +2096,13 @@ class EntityHandlerTests { } """.trimIndent() ) - verify { entityEventService wasNot called } - } - - private fun mockkDefaultBehaviorForReplaceAttribute() { - coEvery { entityPayloadService.checkEntityExistence(any()) } returns Unit.right() - coEvery { authorizationService.userCanUpdateEntity(any(), sub) } returns Unit.right() - coEvery { - entityEventService.publishAttributeChangeEvents(any(), any(), any(), any(), any()) - } returns Job() } @Test fun `replace attribute should return a 204 if attribute has been successfully replaced`() { - mockkDefaultBehaviorForReplaceAttribute() val attributeFragment = ClassPathResource("/ngsild/fragments/beehive_new_incoming_property.json") coEvery { - entityPayloadService.replaceAttribute(any(), any(), any()) + entityService.replaceAttribute(any(), any(), any()) } returns UpdateResult( updated = arrayListOf( UpdatedDetails(INCOMING_PROPERTY, null, UpdateOperationResult.REPLACED) @@ -2445,7 +2119,7 @@ class EntityHandlerTests { .expectStatus().isNoContent coVerify { - entityPayloadService.replaceAttribute( + entityService.replaceAttribute( beehiveId, match { it.first == INCOMING_PROPERTY @@ -2457,10 +2131,9 @@ class EntityHandlerTests { @Test fun `replace attribute should return a 404 if the attribute does not exist`() { - mockkDefaultBehaviorForReplaceAttribute() val attributeFragment = ClassPathResource("/ngsild/fragments/beehive_new_incoming_property.json") coEvery { - entityPayloadService.replaceAttribute(any(), any(), any()) + entityService.replaceAttribute(any(), any(), any()) } returns UpdateResult( updated = emptyList(), notUpdated = arrayListOf( diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/entity/web/EntityOperationHandlerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/entity/web/EntityOperationHandlerTests.kt new file mode 100644 index 000000000..3f07bbd3b --- /dev/null +++ b/search-service/src/test/kotlin/com/egm/stellio/search/entity/web/EntityOperationHandlerTests.kt @@ -0,0 +1,608 @@ +package com.egm.stellio.search.entity.web + +import arrow.core.right +import com.egm.stellio.search.common.config.SearchProperties +import com.egm.stellio.search.entity.model.EMPTY_UPDATE_RESULT +import com.egm.stellio.search.entity.model.UpdateResult +import com.egm.stellio.search.entity.service.EntityOperationService +import com.egm.stellio.search.entity.service.EntityQueryService +import com.egm.stellio.shared.config.ApplicationProperties +import com.egm.stellio.shared.model.ExpandedEntity +import com.egm.stellio.shared.model.NgsiLdEntity +import com.egm.stellio.shared.util.* +import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_DEFAULT_VOCAB +import com.ninjasquad.springmockk.MockkBean +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient +import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest +import org.springframework.core.io.ClassPathResource +import org.springframework.http.HttpStatus +import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf +import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockJwt +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.web.reactive.server.WebTestClient + +@AutoConfigureWebTestClient +@ActiveProfiles("test") +@WebFluxTest(EntityOperationHandler::class) +@EnableConfigurationProperties(ApplicationProperties::class, SearchProperties::class) +class EntityOperationHandlerTests { + + @Autowired + private lateinit var webClient: WebTestClient + + @MockkBean + private lateinit var entityOperationService: EntityOperationService + + @MockkBean + private lateinit var entityQueryService: EntityQueryService + + private val logger = LoggerFactory.getLogger(javaClass) + + private lateinit var mockedTemperatureSensorEntity: NgsiLdEntity + private lateinit var mockedDissolvedOxygenSensorEntity: NgsiLdEntity + private lateinit var mockedDeviceEntity: NgsiLdEntity + private lateinit var mockedTemperatureSensorExpandedEntity: ExpandedEntity + private lateinit var mockedDissolvedOxygenSensorExpandedEntity: ExpandedEntity + private lateinit var mockedDeviceExpandedEntity: ExpandedEntity + + @BeforeAll + fun configureWebClientDefaults() { + webClient = webClient.mutate() + .apply(mockJwt().jwt { it.subject(MOCK_USER_SUB) }) + .apply(csrf()) + .defaultHeaders { + it.accept = listOf(JSON_LD_MEDIA_TYPE) + it.contentType = JSON_LD_MEDIA_TYPE + } + .entityExchangeResultConsumer { + logger.warn(String(it.responseBody as? ByteArray ?: "Empty response body".toByteArray())) + } + .build() + + mockedTemperatureSensorEntity = mockkClass(NgsiLdEntity::class) { + every { id } returns temperatureSensorUri + every { types } returns listOf(SENSOR_TYPE) + } + mockedTemperatureSensorExpandedEntity = mockkClass(ExpandedEntity::class) { + every { id } returns temperatureSensorUri.toString() + every { members } returns emptyMap() + } + mockedDissolvedOxygenSensorEntity = mockkClass(NgsiLdEntity::class) { + every { id } returns dissolvedOxygenSensorUri + every { types } returns listOf(SENSOR_TYPE) + } + mockedDissolvedOxygenSensorExpandedEntity = mockkClass(ExpandedEntity::class) { + every { id } returns dissolvedOxygenSensorUri.toString() + every { members } returns emptyMap() + } + mockedDeviceEntity = mockkClass(NgsiLdEntity::class) { + every { id } returns deviceUri + every { types } returns listOf(DEVICE_TYPE) + } + mockedDeviceExpandedEntity = mockkClass(ExpandedEntity::class) { + every { id } returns deviceUri.toString() + every { members } returns emptyMap() + } + } + + private val temperatureSensorUri = "urn:ngsi-ld:Sensor:HCMR-AQUABOX1temperature".toUri() + private val dissolvedOxygenSensorUri = "urn:ngsi-ld:Sensor:HCMR-AQUABOX1dissolvedOxygen".toUri() + private val deviceUri = "urn:ngsi-ld:Device:HCMR-AQUABOX1".toUri() + private val allEntitiesUris = listOf(temperatureSensorUri, dissolvedOxygenSensorUri, deviceUri) + + private val batchCreateEndpoint = "/ngsi-ld/v1/entityOperations/create" + private val batchUpsertWithUpdateEndpoint = "/ngsi-ld/v1/entityOperations/upsert?options=update" + private val batchUpdateEndpoint = "/ngsi-ld/v1/entityOperations/update" + private val batchUpdateEndpointWithNoOverwriteOption = "/ngsi-ld/v1/entityOperations/update?options=noOverwrite" + private val batchDeleteEndpoint = "/ngsi-ld/v1/entityOperations/delete" + private val queryEntitiesEndpoint = "/ngsi-ld/v1/entityOperations/query" + private val batchMergeEndpoint = "/ngsi-ld/v1/entityOperations/merge" + + private val validJsonFile = ClassPathResource("/ngsild/hcmr/HCMR_test_file.json") + private val twoEntityOneInvalidJsonLDFile = ClassPathResource("/ngsild/two_sensors_one_invalid.jsonld") + private val missingContextJsonFile = ClassPathResource("/ngsild/hcmr/HCMR_test_file_missing_context.json") + private val oneEntityMissingContextJsonFile = + ClassPathResource("/ngsild/hcmr/HCMR_test_file_one_entity_missing_context.json") + private val deleteAllJsonFile = ClassPathResource("/ngsild/hcmr/HCMR_test_delete_all_entities.json") + + @Test + fun `update batch entity should return a 204 if JSON-LD payload is correct`() = runTest { + val jsonLdFile = validJsonFile + + coEvery { + entityOperationService.update(any(), any(), any()) + } returns BatchOperationResult(success = mutableListOf(), errors = mutableListOf()) + + webClient.post() + .uri(batchUpdateEndpoint) + .bodyValue(jsonLdFile) + .exchange() + .expectStatus().isNoContent + .expectBody().isEmpty + + coVerify { + entityOperationService.update(any(), false, eq("60AAEBA3-C0C7-42B6-8CB0-0D30857F210E")) + } + } + + @Test + fun `update batch entity should return a 207 if JSON-LD payload contains update errors`() = runTest { + val jsonLdFile = validJsonFile + val errors = arrayListOf( + BatchEntityError(temperatureSensorUri, arrayListOf("Update unexpectedly failed.")), + BatchEntityError(dissolvedOxygenSensorUri, arrayListOf("Update unexpectedly failed.")) + ) + + coEvery { entityOperationService.update(any(), any(), any()) } returns BatchOperationResult( + mutableListOf(), + errors + ) + + webClient.post() + .uri(batchUpdateEndpoint) + .bodyValue(jsonLdFile) + .exchange() + .expectStatus().isEqualTo(HttpStatus.MULTI_STATUS) + .expectBody().json( + """ + { + "errors": [ + { + "entityId": "urn:ngsi-ld:Sensor:HCMR-AQUABOX1temperature", + "error": [ "Update unexpectedly failed." ] + }, + { + "entityId": "urn:ngsi-ld:Sensor:HCMR-AQUABOX1dissolvedOxygen", + "error": [ "Update unexpectedly failed." ] + } + ], + "success": [] + } + """.trimIndent() + ) + } + + @Test + fun `update batch entity should return a 207 if one entity is an invalid NGSI-LD payload`() = runTest { + val jsonLdFile = twoEntityOneInvalidJsonLDFile + + coEvery { entityOperationService.update(any(), any(), any()) } returns BatchOperationResult( + mutableListOf(BatchEntitySuccess(temperatureSensorUri, EMPTY_UPDATE_RESULT)), + mutableListOf() + ) + + webClient.post() + .uri(batchUpdateEndpoint) + .bodyValue(jsonLdFile) + .exchange() + .expectStatus().isEqualTo(HttpStatus.MULTI_STATUS) + .expectBody().json( + """ + { + "errors": [ + { + "entityId": "urn:ngsi-ld:Sensor:HCMR-AQUABOX2temperature", + "error": [ "Unable to expand input payload" ] + } + ], + "success": [ + "urn:ngsi-ld:Sensor:HCMR-AQUABOX1temperature" + ] + } + """.trimIndent() + ) + } + + @Test + fun `update batch entity should return a 400 if JSON-LD payload is not correct`() { + shouldReturn400WithBadPayload("update") + } + + @Test + fun `update batch entity should return 204 if JSON-LD payload is correct and noOverwrite is asked`() = runTest { + val jsonLdFile = validJsonFile + coEvery { + entityOperationService.update(any(), any(), any()) + } returns BatchOperationResult(success = mutableListOf(), errors = mutableListOf()) + + webClient.post() + .uri(batchUpdateEndpointWithNoOverwriteOption) + .bodyValue(jsonLdFile) + .exchange() + .expectStatus().isNoContent + .expectBody().isEmpty + + coVerify { + entityOperationService.update(any(), true, eq("60AAEBA3-C0C7-42B6-8CB0-0D30857F210E")) + } + } + + @Test + fun `create batch entity should return a 201 if JSON-LD payload is correct`() = runTest { + val jsonLdFile = validJsonFile + + coEvery { entityOperationService.create(any(), any()) } returns BatchOperationResult( + allEntitiesUris.map { BatchEntitySuccess(it) }.toMutableList(), + arrayListOf() + ) + + webClient.post() + .uri(batchCreateEndpoint) + .bodyValue(jsonLdFile) + .exchange() + .expectStatus().isCreated + .expectBody() + .jsonPath("$").isArray + .jsonPath("$[*]").isEqualTo(allEntitiesUris.map { it.toString() }) + } + + @Test + fun `create batch entity should return a 207 when some creation are in errors`() = runTest { + val jsonLdFile = validJsonFile + val createdEntitiesIds = arrayListOf(dissolvedOxygenSensorUri, deviceUri) + + coEvery { entityOperationService.create(any(), any()) } returns BatchOperationResult( + createdEntitiesIds.map { BatchEntitySuccess(it) }.toMutableList(), + mutableListOf(BatchEntityError(temperatureSensorUri, mutableListOf(ENTITY_ALREADY_EXISTS_MESSAGE))) + ) + + webClient.post() + .uri(batchCreateEndpoint) + .bodyValue(jsonLdFile) + .exchange() + .expectStatus().isEqualTo(HttpStatus.MULTI_STATUS) + .expectBody().json( + """ + { + "errors": [ + { + "entityId": "urn:ngsi-ld:Sensor:HCMR-AQUABOX1temperature", + "error": [ "Entity already exists" ] + } + ], + "success": [ + "urn:ngsi-ld:Sensor:HCMR-AQUABOX1dissolvedOxygen", + "urn:ngsi-ld:Device:HCMR-AQUABOX1" + ] + } + """.trimIndent() + ) + } + + @Test + fun `create batch entity should return a 400 if JSON-LD payload is not correct`() = runTest { + shouldReturn400WithBadPayload("create") + } + + @Test + fun `create batch entity should return a 400 if one JSON-LD entity misses a context`() = runTest { + val jsonLdFile = oneEntityMissingContextJsonFile + webClient.post() + .uri(batchCreateEndpoint) + .bodyValue(jsonLdFile) + .exchange() + .expectStatus().isBadRequest + .expectBody().json( + """ + { + "type":"https://uri.etsi.org/ngsi-ld/errors/BadRequestData", + "title":"The request includes input data which does not meet the requirements of the operation", + "detail": + "Request payload must contain @context term for a request having an application/ld+json content type" + } + """.trimIndent() + ) + } + + @Test + fun `upsert batch entity should return a 201 if JSON-LD payload is correct`() = runTest { + val jsonLdFile = validJsonFile + val createdEntitiesIds = arrayListOf(temperatureSensorUri) + val updatedEntitiesIds = arrayListOf(dissolvedOxygenSensorUri, deviceUri) + + val updatedBatchResult = BatchOperationResult( + updatedEntitiesIds.map { BatchEntitySuccess(it, mockkClass(UpdateResult::class)) }.toMutableList() + ) + + coEvery { entityOperationService.upsert(any(), any(), any()) } returns ( + updatedBatchResult to createdEntitiesIds + ) + + webClient.post() + .uri(batchUpsertWithUpdateEndpoint) + .bodyValue(jsonLdFile).exchange() + .expectStatus().isCreated + .expectBody() + .jsonPath("$").isArray + .jsonPath("$[*]").isEqualTo(createdEntitiesIds.map { it.toString() }) + } + + @Test + fun `upsert batch entity should return a 204 if it has only updated existing entities`() = runTest { + val jsonLdFile = validJsonFile + coEvery { + entityOperationService.upsert(any(), any(), any()) + } returns (BatchOperationResult(success = mutableListOf(), errors = mutableListOf()) to emptyList()) + + webClient.post() + .uri(batchUpsertWithUpdateEndpoint) + .bodyValue(jsonLdFile) + .exchange() + .expectStatus().isNoContent + .expectBody().isEmpty + } + + @Test + fun `upsert batch entity should return a 207 if JSON-LD payload contains update errors`() = runTest { + val jsonLdFile = validJsonFile + val errors = arrayListOf( + BatchEntityError(temperatureSensorUri, arrayListOf("Update unexpectedly failed.")), + BatchEntityError(dissolvedOxygenSensorUri, arrayListOf("Update unexpectedly failed.")) + ) + + coEvery { entityOperationService.upsert(any(), any(), any()) } returns ( + BatchOperationResult( + arrayListOf(BatchEntitySuccess(deviceUri, mockkClass(UpdateResult::class))), + errors + ) to emptyList() + ) + + webClient.post() + .uri(batchUpsertWithUpdateEndpoint) + .bodyValue(jsonLdFile) + .exchange() + .expectStatus().isEqualTo(HttpStatus.MULTI_STATUS) + .expectBody().json( + """ + { + "errors": [ + { + "entityId": "urn:ngsi-ld:Sensor:HCMR-AQUABOX1temperature", + "error": [ "Update unexpectedly failed." ] + }, + { + "entityId": "urn:ngsi-ld:Sensor:HCMR-AQUABOX1dissolvedOxygen", + "error": [ "Update unexpectedly failed." ] + } + ], + "success": [ "urn:ngsi-ld:Device:HCMR-AQUABOX1" ] + } + """.trimIndent() + ) + } + + @Test + fun `upsert batch entity should return a 400 if JSON-LD payload is not correct`() { + shouldReturn400WithBadPayload("upsert") + } + + private fun shouldReturn400WithBadPayload(method: String) { + val jsonLdFile = missingContextJsonFile + webClient.post() + .uri("/ngsi-ld/v1/entityOperations/$method") + .bodyValue(jsonLdFile) + .exchange() + .expectStatus().isBadRequest + .expectBody().json( + """ + { + "type":"https://uri.etsi.org/ngsi-ld/errors/BadRequestData", + "title":"The request includes input data which does not meet the requirements of the operation", + "detail": + "Request payload must contain @context term for a request having an application/ld+json content type" + } + """.trimIndent() + ) + } + + @Test + fun `delete batch for correct entities should return a 204`() = runTest { + val jsonLdFile = deleteAllJsonFile + + coEvery { entityOperationService.delete(any(), any()) } returns + BatchOperationResult( + allEntitiesUris.map { BatchEntitySuccess(it) }.toMutableList(), + mutableListOf() + ) + + webClient.post() + .uri(batchDeleteEndpoint) + .bodyValue(jsonLdFile) + .exchange() + .expectStatus().isNoContent + } + + @Test + fun `delete batch with errors should return a 207 with explicit error messages`() = runTest { + coEvery { entityOperationService.delete(any(), any()) } returns + BatchOperationResult( + mutableListOf(BatchEntitySuccess(temperatureSensorUri), BatchEntitySuccess(dissolvedOxygenSensorUri)), + mutableListOf( + BatchEntityError(deviceUri, mutableListOf(ENTITY_DOES_NOT_EXIST_MESSAGE)) + ) + ) + + val jsonLdFile = deleteAllJsonFile + + webClient.post() + .uri(batchDeleteEndpoint) + .bodyValue(jsonLdFile) + .exchange() + .expectStatus().isEqualTo(HttpStatus.MULTI_STATUS) + .expectBody().json( + """ + { + "success": ["urn:ngsi-ld:Sensor:HCMR-AQUABOX1temperature","urn:ngsi-ld:Sensor:HCMR-AQUABOX1dissolvedOxygen"], + "errors": [ + {"entityId":"urn:ngsi-ld:Device:HCMR-AQUABOX1", + "error":["$ENTITY_DOES_NOT_EXIST_MESSAGE"]} + ] + } + """.trimIndent() + ) + } + + @Test + fun `query entities should return a 200 if the query is correct`() = runTest { + coEvery { + entityQueryService.queryEntities(any(), any()) + } returns Pair(emptyList(), 0).right() + + val query = """ + { + "type": "Query", + "entities": [{ + "type": "$BEEHIVE_TYPE" + }], + "attrs": ["attr1", "attr2"] + } + """.trimIndent() + + webClient.post() + .uri("$queryEntitiesEndpoint?limit=10&offset=20") + .bodyValue(query) + .exchange() + .expectStatus().isOk + + coVerify { + entityQueryService.queryEntities( + match { + it.paginationQuery.limit == 10 && + it.paginationQuery.offset == 20 && + it.typeSelection == BEEHIVE_TYPE && + it.attrs == setOf("${NGSILD_DEFAULT_VOCAB}attr1", "${NGSILD_DEFAULT_VOCAB}attr2") + }, + any() + ) + } + } + + @Test + fun `merge batch entity should return a 204 if JSON-LD payload is correct`() = runTest { + val jsonLdFile = validJsonFile + + coEvery { + entityOperationService.merge(any(), any()) + } returns BatchOperationResult(success = mutableListOf(), errors = mutableListOf()) + + webClient.post() + .uri(batchMergeEndpoint) + .bodyValue(jsonLdFile) + .exchange() + .expectStatus().isNoContent + .expectBody().isEmpty + + coVerify { + entityOperationService.merge(any(), eq("60AAEBA3-C0C7-42B6-8CB0-0D30857F210E")) + } + } + + @Test + fun `merge batch entity should return a 207 if JSON-LD payload contains update errors`() = runTest { + val jsonLdFile = validJsonFile + val errors = arrayListOf( + BatchEntityError(temperatureSensorUri, arrayListOf("Update unexpectedly failed.")), + BatchEntityError(dissolvedOxygenSensorUri, arrayListOf("Update unexpectedly failed.")) + ) + + coEvery { entityOperationService.merge(any(), any()) } returns BatchOperationResult( + mutableListOf(), + errors + ) + + webClient.post() + .uri(batchMergeEndpoint) + .bodyValue(jsonLdFile) + .exchange() + .expectStatus().isEqualTo(HttpStatus.MULTI_STATUS) + .expectBody().json( + """ + { + "errors": [ + { + "entityId": "urn:ngsi-ld:Sensor:HCMR-AQUABOX1temperature", + "error": [ "Update unexpectedly failed." ] + }, + { + "entityId": "urn:ngsi-ld:Sensor:HCMR-AQUABOX1dissolvedOxygen", + "error": [ "Update unexpectedly failed." ] + } + ], + "success": [] + } + """.trimIndent() + ) + } + + @Test + fun `merge batch entity should return a 207 if one entity is an invalid NGSI-LD payload`() = runTest { + val jsonLdFile = twoEntityOneInvalidJsonLDFile + + coEvery { entityOperationService.merge(any(), any()) } returns BatchOperationResult( + mutableListOf(BatchEntitySuccess(temperatureSensorUri, EMPTY_UPDATE_RESULT)), + mutableListOf() + ) + + webClient.post() + .uri(batchMergeEndpoint) + .bodyValue(jsonLdFile) + .exchange() + .expectStatus().isEqualTo(HttpStatus.MULTI_STATUS) + .expectBody().json( + """ + { + "errors": [ + { + "entityId": "urn:ngsi-ld:Sensor:HCMR-AQUABOX2temperature", + "error": [ "Unable to expand input payload" ] + } + ], + "success": [ + "urn:ngsi-ld:Sensor:HCMR-AQUABOX1temperature" + ] + } + """.trimIndent() + ) + } + + @Test + fun `merge batch entity should return a 400 if JSON-LD payload is not correct`() { + shouldReturn400WithBadPayload("merge") + } + + @Test + fun `merge batch entity should return 207 if there is a non existing entity in the payload`() = runTest { + val jsonLdFile = validJsonFile + + coEvery { entityOperationService.merge(any(), any()) } returns BatchOperationResult( + success = mutableListOf(BatchEntitySuccess(temperatureSensorUri, mockkClass(UpdateResult::class))), + errors = mutableListOf(BatchEntityError(deviceUri, mutableListOf("Entity does not exist"))) + ) + + webClient.post() + .uri(batchMergeEndpoint) + .bodyValue(jsonLdFile) + .exchange() + .expectStatus().isEqualTo(HttpStatus.MULTI_STATUS) + .expectBody().json( + """ + { + "errors": [ + { + "entityId": "urn:ngsi-ld:Device:HCMR-AQUABOX1", + "error": [ "Entity does not exist" ] + } + ], + "success": [ "urn:ngsi-ld:Sensor:HCMR-AQUABOX1temperature" ] + } + """.trimIndent() + ) + } +} diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/scope/ScopeServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/scope/ScopeServiceTests.kt index f652bbb1c..0bd780c93 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/scope/ScopeServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/scope/ScopeServiceTests.kt @@ -1,12 +1,17 @@ package com.egm.stellio.search.scope -import com.egm.stellio.search.model.* -import com.egm.stellio.search.model.AttributeInstance.TemporalProperty -import com.egm.stellio.search.service.EntityPayloadService +import com.egm.stellio.search.entity.model.EntitiesQuery +import com.egm.stellio.search.entity.model.Entity +import com.egm.stellio.search.entity.model.OperationType +import com.egm.stellio.search.entity.service.EntityQueryService +import com.egm.stellio.search.entity.service.EntityService +import com.egm.stellio.search.entity.util.toExpandedAttributeInstance import com.egm.stellio.search.support.WithKafkaContainer import com.egm.stellio.search.support.WithTimescaleContainer import com.egm.stellio.search.support.buildDefaultTestTemporalQuery -import com.egm.stellio.search.util.toExpandedAttributeInstance +import com.egm.stellio.search.temporal.model.AttributeInstance.TemporalProperty +import com.egm.stellio.search.temporal.model.TemporalEntitiesQuery +import com.egm.stellio.search.temporal.model.TemporalQuery import com.egm.stellio.shared.model.PaginationQuery import com.egm.stellio.shared.model.getScopes import com.egm.stellio.shared.util.* @@ -22,6 +27,7 @@ import org.junit.jupiter.params.provider.MethodSource import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.data.r2dbc.core.R2dbcEntityTemplate +import org.springframework.data.r2dbc.core.delete import org.springframework.test.context.ActiveProfiles import java.time.ZonedDateTime import java.util.stream.Stream @@ -34,7 +40,10 @@ class ScopeServiceTests : WithTimescaleContainer, WithKafkaContainer { private lateinit var scopeService: ScopeService @Autowired - private lateinit var entityPayloadService: EntityPayloadService + private lateinit var entityService: EntityService + + @Autowired + private lateinit var entityQueryService: EntityQueryService @Autowired private lateinit var r2dbcEntityTemplate: R2dbcEntityTemplate @@ -43,9 +52,7 @@ class ScopeServiceTests : WithTimescaleContainer, WithKafkaContainer { @AfterEach fun clearEntityPayloadTable() { - r2dbcEntityTemplate.delete(EntityPayload::class.java) - .all() - .block() + r2dbcEntityTemplate.delete().from("entity_payload").all().block() runBlocking { scopeService.delete(beehiveTestCId) @@ -91,7 +98,7 @@ class ScopeServiceTests : WithTimescaleContainer, WithKafkaContainer { ) = runTest { loadSampleData(initialEntity) .sampleDataToNgsiLdEntity() - .map { entityPayloadService.createEntityPayload(it.second, it.first, ngsiLdDateTime()) } + .map { entityService.createEntityPayload(it.second, it.first, ngsiLdDateTime()) } val expandedAttributes = JsonLdUtils.expandAttributes( """ @@ -109,7 +116,7 @@ class ScopeServiceTests : WithTimescaleContainer, WithKafkaContainer { operationType ).shouldSucceed() - entityPayloadService.retrieve(beehiveTestCId) + entityQueryService.retrieve(beehiveTestCId) .shouldSucceedWith { assertEquals(expectedScopes, it.scopes) val scopesInEntity = it.payload.toExpandedAttributeInstance().getScopes() @@ -120,7 +127,7 @@ class ScopeServiceTests : WithTimescaleContainer, WithKafkaContainer { private suspend fun createScopeHistory() { loadSampleData("beehive_with_scope.jsonld") .sampleDataToNgsiLdEntity() - .map { entityPayloadService.createEntityPayload(it.second, it.first, ngsiLdDateTime()) } + .map { entityService.createEntityPayload(it.second, it.first, ngsiLdDateTime()) } scopeService.addHistoryEntry( beehiveTestCId, listOf("/A", "/B/C"), @@ -325,7 +332,7 @@ class ScopeServiceTests : WithTimescaleContainer, WithKafkaContainer { fun `it should include lower bound of interval with after timerel`() = runTest { loadSampleData("beehive_with_scope.jsonld") .sampleDataToNgsiLdEntity() - .map { entityPayloadService.createEntityPayload(it.second, it.first, ngsiLdDateTime()) } + .map { entityService.createEntityPayload(it.second, it.first, ngsiLdDateTime()) } scopeService.addHistoryEntry( beehiveTestCId, listOf("/A", "/B/C"), @@ -366,7 +373,7 @@ class ScopeServiceTests : WithTimescaleContainer, WithKafkaContainer { fun `it should exclude upper bound of interval with between timerel`() = runTest { loadSampleData("beehive_with_scope.jsonld") .sampleDataToNgsiLdEntity() - .map { entityPayloadService.createEntityPayload(it.second, it.first, ngsiLdDateTime()) } + .map { entityService.createEntityPayload(it.second, it.first, ngsiLdDateTime()) } scopeService.addHistoryEntry( beehiveTestCId, listOf("/A", "/B/C"), @@ -417,7 +424,7 @@ class ScopeServiceTests : WithTimescaleContainer, WithKafkaContainer { fun `it should delete scope and its history`() = runTest { loadSampleData("beehive_with_scope.jsonld") .sampleDataToNgsiLdEntity() - .map { entityPayloadService.createEntityPayload(it.second, it.first, ngsiLdDateTime()) } + .map { entityService.createEntityPayload(it.second, it.first, ngsiLdDateTime()) } scopeService.delete(beehiveTestCId).shouldSucceed() diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/scope/TemporalScopeBuilderTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/scope/TemporalScopeBuilderTests.kt index f736e3abf..97798d43b 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/scope/TemporalScopeBuilderTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/scope/TemporalScopeBuilderTests.kt @@ -1,11 +1,11 @@ package com.egm.stellio.search.scope -import com.egm.stellio.search.model.AttributeInstance.TemporalProperty -import com.egm.stellio.search.model.TemporalEntitiesQuery -import com.egm.stellio.search.model.TemporalQuery import com.egm.stellio.search.support.buildDefaultQueryParams import com.egm.stellio.search.support.buildDefaultTestTemporalQuery import com.egm.stellio.search.support.gimmeEntityPayload +import com.egm.stellio.search.temporal.model.AttributeInstance.TemporalProperty +import com.egm.stellio.search.temporal.model.TemporalEntitiesQuery +import com.egm.stellio.search.temporal.model.TemporalQuery import com.egm.stellio.shared.util.JsonUtils import com.egm.stellio.shared.util.assertJsonPayloadsAreEqual import com.egm.stellio.shared.util.loadSampleData diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityPayloadServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityPayloadServiceTests.kt deleted file mode 100644 index 6aa967ea6..000000000 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityPayloadServiceTests.kt +++ /dev/null @@ -1,747 +0,0 @@ -package com.egm.stellio.search.service - -import arrow.core.right -import com.egm.stellio.search.model.* -import com.egm.stellio.search.support.EMPTY_PAYLOAD -import com.egm.stellio.search.support.WithKafkaContainer -import com.egm.stellio.search.support.WithTimescaleContainer -import com.egm.stellio.search.support.buildSapAttribute -import com.egm.stellio.search.util.deserializeAsMap -import com.egm.stellio.shared.model.AlreadyExistsException -import com.egm.stellio.shared.model.ResourceNotFoundException -import com.egm.stellio.shared.util.* -import com.egm.stellio.shared.util.AuthContextModel.SpecificAccessPolicy.AUTH_READ -import com.egm.stellio.shared.util.AuthContextModel.SpecificAccessPolicy.AUTH_WRITE -import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_TYPE -import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_DEFAULT_VOCAB -import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_SCOPE_PROPERTY -import com.egm.stellio.shared.util.JsonLdUtils.expandAttribute -import com.egm.stellio.shared.util.JsonLdUtils.expandAttributes -import com.egm.stellio.shared.util.JsonUtils.deserializeExpandedPayload -import com.ninjasquad.springmockk.MockkBean -import io.mockk.coEvery -import io.mockk.coVerify -import kotlinx.coroutines.test.runTest -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.api.Assertions.assertThatList -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.dao.DataIntegrityViolationException -import org.springframework.data.r2dbc.core.R2dbcEntityTemplate -import org.springframework.test.context.ActiveProfiles -import java.time.ZonedDateTime - -@SpringBootTest -@ActiveProfiles("test") -class EntityPayloadServiceTests : WithTimescaleContainer, WithKafkaContainer { - - @Autowired - private lateinit var entityPayloadService: EntityPayloadService - - @MockkBean - private lateinit var temporalEntityAttributeService: TemporalEntityAttributeService - - @Autowired - private lateinit var r2dbcEntityTemplate: R2dbcEntityTemplate - - private val entity01Uri = "urn:ngsi-ld:Entity:01".toUri() - private val entity02Uri = "urn:ngsi-ld:Entity:02".toUri() - private val beehiveTestCId = "urn:ngsi-ld:BeeHive:TESTC".toUri() - private val now = ngsiLdDateTime() - - @AfterEach - fun clearEntityPayloadTable() { - r2dbcEntityTemplate.delete(EntityPayload::class.java) - .all() - .block() - } - - @Test - fun `it should create an entity payload from string if none existed yet`() = runTest { - loadMinimalEntity(entity01Uri, setOf(BEEHIVE_TYPE)) - .sampleDataToNgsiLdEntity() - .map { - entityPayloadService.createEntityPayload( - it.second, - it.first, - now - ).shouldSucceed() - } - } - - @Test - fun `it should create an entity payload from an NGSI-LD Entity if none existed yet`() = runTest { - val (jsonLdEntity, ngsiLdEntity) = loadSampleData().sampleDataToNgsiLdEntity().shouldSucceedAndResult() - entityPayloadService.createEntityPayload(ngsiLdEntity, jsonLdEntity, now) - .shouldSucceed() - } - - @Test - fun `it should create an entity payload from string with specificAccessPolicy`() = runTest { - loadMinimalEntityWithSap(entity01Uri, setOf(BEEHIVE_TYPE), AUTH_READ, AUTHZ_TEST_COMPOUND_CONTEXTS) - .sampleDataToNgsiLdEntity() - .map { - entityPayloadService.createEntityPayload( - it.second, - it.first, - now - ) - } - loadMinimalEntityWithSap(entity02Uri, setOf(BEEHIVE_TYPE), AUTH_WRITE, AUTHZ_TEST_COMPOUND_CONTEXTS) - .sampleDataToNgsiLdEntity() - .map { - entityPayloadService.createEntityPayload( - it.second, - it.first, - now - ) - } - entityPayloadService.hasSpecificAccessPolicies( - entity01Uri, - listOf(AUTH_READ) - ).shouldSucceedWith { assertTrue(it) } - entityPayloadService.hasSpecificAccessPolicies( - entity01Uri, - listOf(AUTH_WRITE) - ).shouldSucceedWith { assertFalse(it) } - - entityPayloadService.hasSpecificAccessPolicies( - entity02Uri, - listOf(AUTH_READ) - ).shouldSucceedWith { assertFalse(it) } - entityPayloadService.hasSpecificAccessPolicies( - entity02Uri, - listOf(AUTH_WRITE) - ).shouldSucceedWith { assertTrue(it) } - } - - @Test - fun `it should create an entity payload from an NGSI-LD Entity with specificAccessPolicy`() = runTest { - val (jsonLdEntity, ngsiLdEntity) = - loadSampleData("beehive_with_sap.jsonld").sampleDataToNgsiLdEntity().shouldSucceedAndResult() - - entityPayloadService.createEntityPayload( - ngsiLdEntity, - jsonLdEntity, - now - ) - - entityPayloadService.hasSpecificAccessPolicies( - beehiveTestCId, - listOf(AUTH_READ) - ).shouldSucceedWith { assertTrue(it) } - entityPayloadService.hasSpecificAccessPolicies( - beehiveTestCId, - listOf(AUTH_WRITE) - ).shouldSucceedWith { assertFalse(it) } - } - - @Test - fun `it should only create an entity payload for a minimal entity`() = runTest { - coEvery { - temporalEntityAttributeService.createEntityTemporalReferences(any(), any(), any(), any(), any()) - } returns Unit.right() - - val (expandedEntity, ngsiLdEntity) = - loadAndPrepareSampleData("beehive_minimal.jsonld").shouldSucceedAndResult() - - entityPayloadService.createEntity( - ngsiLdEntity, - expandedEntity, - "0123456789-1234-5678-987654321" - ).shouldSucceed() - - entityPayloadService.retrieve(beehiveTestCId) - .shouldSucceedWith { - assertEquals("urn:ngsi-ld:BeeHive:TESTC".toUri(), it.entityId) - assertEquals(listOf(BEEHIVE_TYPE), it.types) - } - - coVerify { - temporalEntityAttributeService.createEntityTemporalReferences( - any(), - any(), - emptyList(), - any(), - eq("0123456789-1234-5678-987654321") - ) - } - } - - @Test - fun `it should not create an entity payload if one already existed`() = runTest { - val (jsonLdEntity, ngsiLdEntity) = - loadMinimalEntity(entity01Uri, setOf(BEEHIVE_TYPE)).sampleDataToNgsiLdEntity().shouldSucceedAndResult() - entityPayloadService.createEntityPayload( - ngsiLdEntity, - jsonLdEntity, - now, - ) - - assertThrows { - entityPayloadService.createEntityPayload( - ngsiLdEntity, - jsonLdEntity, - now, - ) - } - } - - @Test - fun `it should merge an entity`() = runTest { - coEvery { - temporalEntityAttributeService.createEntityTemporalReferences(any(), any(), any(), any(), any()) - } returns Unit.right() - coEvery { - temporalEntityAttributeService.mergeEntityAttributes(any(), any(), any(), any(), any(), any()) - } returns UpdateResult( - listOf(UpdatedDetails(INCOMING_PROPERTY, null, UpdateOperationResult.APPENDED)), - emptyList() - ).right() - coEvery { - temporalEntityAttributeService.getForEntity(any(), any(), any()) - } returns emptyList() - - val (expandedEntity, ngsiLdEntity) = - loadAndPrepareSampleData("beehive_minimal.jsonld").shouldSucceedAndResult() - - entityPayloadService.createEntity( - ngsiLdEntity, - expandedEntity, - "0123456789-1234-5678-987654321" - ).shouldSucceed() - - val (jsonLdEntity, _) = loadSampleData().sampleDataToNgsiLdEntity().shouldSucceedAndResult() - - entityPayloadService.mergeEntity( - beehiveTestCId, - jsonLdEntity.getModifiableMembers(), - now, - "0123456789-1234-5678-987654321" - ).shouldSucceed() - - entityPayloadService.retrieve(beehiveTestCId) - .shouldSucceedWith { - assertTrue(it.modifiedAt != null) - } - - coVerify { - temporalEntityAttributeService.createEntityTemporalReferences( - any(), - any(), - emptyList(), - any(), - eq("0123456789-1234-5678-987654321") - ) - temporalEntityAttributeService.mergeEntityAttributes( - eq(beehiveTestCId), - any(), - any(), - any(), - any(), - eq("0123456789-1234-5678-987654321") - ) - temporalEntityAttributeService.getForEntity( - eq(beehiveTestCId), - emptySet(), - emptySet() - ) - } - } - - @Test - fun `it should merge an entity with new types`() = runTest { - coEvery { - temporalEntityAttributeService.createEntityTemporalReferences(any(), any(), any(), any(), any()) - } returns Unit.right() - coEvery { - temporalEntityAttributeService.mergeEntityAttributes(any(), any(), any(), any(), any(), any()) - } returns UpdateResult( - listOf(UpdatedDetails(INCOMING_PROPERTY, null, UpdateOperationResult.APPENDED)), - emptyList() - ).right() - coEvery { - temporalEntityAttributeService.getForEntity(any(), any(), any()) - } returns emptyList() - - val (expandedEntity, ngsiLdEntity) = - loadAndPrepareSampleData("beehive_minimal.jsonld").shouldSucceedAndResult() - - entityPayloadService.createEntity( - ngsiLdEntity, - expandedEntity, - "0123456789-1234-5678-987654321" - ).shouldSucceed() - - val expandedAttributes = expandAttributes( - loadSampleData("fragments/beehive_merge_entity_multiple_types.jsonld"), - APIC_COMPOUND_CONTEXTS - ) - - entityPayloadService.mergeEntity( - beehiveTestCId, - expandedAttributes, - now, - "0123456789-1234-5678-987654321" - ).shouldSucceed() - - entityPayloadService.retrieve(beehiveTestCId) - .shouldSucceedWith { - assertTrue(it.types.containsAll(setOf(BEEHIVE_TYPE, NGSILD_DEFAULT_VOCAB + "Distribution"))) - } - } - - @Test - fun `it should merge an entity with new types and scopes`() = runTest { - coEvery { - temporalEntityAttributeService.createEntityTemporalReferences(any(), any(), any(), any(), any()) - } returns Unit.right() - coEvery { - temporalEntityAttributeService.mergeEntityAttributes(any(), any(), any(), any(), any(), any()) - } returns UpdateResult( - listOf(UpdatedDetails(INCOMING_PROPERTY, null, UpdateOperationResult.APPENDED)), - emptyList() - ).right() - coEvery { - temporalEntityAttributeService.partialUpdateEntityAttribute(any(), any(), any(), any()) - } returns EMPTY_UPDATE_RESULT.right() - coEvery { - temporalEntityAttributeService.getForEntity(any(), any(), any()) - } returns emptyList() - - val (expandedEntity, ngsiLdEntity) = - loadAndPrepareSampleData("beehive_minimal.jsonld").shouldSucceedAndResult() - - entityPayloadService.createEntity( - ngsiLdEntity, - expandedEntity, - "0123456789-1234-5678-987654321" - ).shouldSucceed() - - val expandedAttributes = expandAttributes( - loadSampleData("fragments/beehive_merge_entity_multiple_types_and_scopes.jsonld"), - APIC_COMPOUND_CONTEXTS - ) - - entityPayloadService.mergeEntity( - beehiveTestCId, - expandedAttributes, - now, - "0123456789-1234-5678-987654321" - ).shouldSucceed() - - entityPayloadService.retrieve(beehiveTestCId) - .shouldSucceedWith { - assertTrue(it.types.containsAll(setOf(BEEHIVE_TYPE, NGSILD_DEFAULT_VOCAB + "Distribution"))) - assertTrue(it.scopes?.containsAll(setOf("/Nantes/BottiereChenaie", "/Agri/Beekeeping")) ?: false) - } - } - - @Test - fun `it should replace an entity payload if entity previously existed`() = runTest { - val (jsonLdEntity, ngsiLdEntity) = loadSampleData().sampleDataToNgsiLdEntity().shouldSucceedAndResult() - entityPayloadService.createEntityPayload(ngsiLdEntity, jsonLdEntity, now).shouldSucceed() - - entityPayloadService.replaceEntityPayload(ngsiLdEntity, jsonLdEntity, now).shouldSucceed() - } - - @Test - fun `it should replace an entity`() = runTest { - val beehiveURI = "urn:ngsi-ld:BeeHive:TESTC".toUri() - coEvery { - temporalEntityAttributeService.createEntityTemporalReferences(any(), any(), any(), any(), any()) - } returns Unit.right() - coEvery { temporalEntityAttributeService.deleteTemporalAttributesOfEntity(any()) } returns Unit.right() - - val (expandedEntity, ngsiLdEntity) = - loadAndPrepareSampleData("beehive_minimal.jsonld").shouldSucceedAndResult() - - entityPayloadService.createEntity( - ngsiLdEntity, - expandedEntity, - "0123456789-1234-5678-987654321" - ).shouldSucceed() - - val (newExpandedEntity, newNgsiLdEntity) = loadSampleData().sampleDataToNgsiLdEntity().shouldSucceedAndResult() - - entityPayloadService.replaceEntity( - beehiveURI, - newNgsiLdEntity, - newExpandedEntity, - "0123456789-1234-5678-987654321" - ).shouldSucceed() - - entityPayloadService.retrieve(beehiveTestCId) - .shouldSucceedWith { - assertTrue(it.modifiedAt != null) - assertEquals(8, it.payload.deserializeAsMap().size) - } - - coVerify { - temporalEntityAttributeService.deleteTemporalAttributesOfEntity(beehiveURI) - temporalEntityAttributeService.createEntityTemporalReferences( - any(), - any(), - emptyList(), - any(), - eq("0123456789-1234-5678-987654321") - ) - } - } - - @Test - fun `it should replace an attribute`() = runTest { - coEvery { temporalEntityAttributeService.getForEntity(any(), any(), any()) } returns emptyList() - coEvery { - temporalEntityAttributeService.replaceEntityAttribute(any(), any(), any(), any(), any()) - } returns UpdateResult( - updated = listOf(UpdatedDetails(INCOMING_PROPERTY, null, UpdateOperationResult.REPLACED)), - notUpdated = emptyList() - ).right() - - val (jsonLdEntity, ngsiLdEntity) = loadSampleData().sampleDataToNgsiLdEntity().shouldSucceedAndResult() - entityPayloadService.createEntityPayload(ngsiLdEntity, jsonLdEntity, now).shouldSucceed() - - val expandedAttribute = expandAttribute( - loadSampleData("fragments/beehive_new_incoming_property.json"), - APIC_COMPOUND_CONTEXTS - ) - - entityPayloadService.replaceAttribute(beehiveTestCId, expandedAttribute, "0123456789-1234-5678-987654321") - .shouldSucceedWith { - it.updated.size == 1 && - it.notUpdated.isEmpty() && - it.updated[0].attributeName == INCOMING_PROPERTY && - it.updated[0].updateOperationResult == UpdateOperationResult.REPLACED - } - } - - @Test - fun `it should retrieve an entity payload`() = runTest { - loadMinimalEntity(entity01Uri, setOf(BEEHIVE_TYPE)) - .sampleDataToNgsiLdEntity() - .map { - entityPayloadService.createEntityPayload( - it.second, - it.first, - now, - ).shouldSucceed() - } - - entityPayloadService.retrieve(entity01Uri) - .shouldSucceedWith { - assertThat(it) - .hasFieldOrPropertyWithValue("entityId", entity01Uri) - .hasFieldOrPropertyWithValue("types", listOf(BEEHIVE_TYPE)) - .hasFieldOrPropertyWithValue("createdAt", now) - .hasFieldOrPropertyWithValue("modifiedAt", null) - .hasFieldOrPropertyWithValue("specificAccessPolicy", null) - } - } - - @Test - fun `it should retrieve an entity payload with specificAccesPolicy`() = runTest { - loadMinimalEntityWithSap(entity01Uri, setOf(BEEHIVE_TYPE), AUTH_READ, AUTHZ_TEST_COMPOUND_CONTEXTS) - .sampleDataToNgsiLdEntity() - .map { - entityPayloadService.createEntityPayload( - it.second, - it.first, - now - ) - } - - entityPayloadService.retrieve(entity01Uri) - .shouldSucceedWith { - assertThat(it) - .hasFieldOrPropertyWithValue("specificAccessPolicy", AUTH_READ) - } - } - - @Test - fun `it should retrieve a list of entity payloads`() = runTest { - val expandedPayload = loadSampleData("beehive_expanded.jsonld") - expandedPayload.sampleDataToNgsiLdEntity().map { - entityPayloadService.createEntityPayload( - it.second, - it.first, - ZonedDateTime.parse("2023-08-20T15:44:10.381090Z") - ) - } - - val entityPayloads = entityPayloadService.retrieve(listOf(beehiveTestCId)) - assertEquals(1, entityPayloads.size) - assertEquals(beehiveTestCId, entityPayloads[0].entityId) - assertJsonPayloadsAreEqual(expandedPayload, entityPayloads[0].payload.asString()) - } - - @Test - fun `it should check the existence or non-existence of an entity`() = runTest { - loadMinimalEntity(entity01Uri, setOf(BEEHIVE_TYPE)) - .sampleDataToNgsiLdEntity() - .map { - entityPayloadService.createEntityPayload( - it.second, - it.first, - now - ) - } - - entityPayloadService.checkEntityExistence(entity01Uri).shouldSucceed() - entityPayloadService.checkEntityExistence(entity02Uri) - .shouldFail { assert(it is ResourceNotFoundException) } - entityPayloadService.checkEntityExistence(entity01Uri, true) - .shouldFail { assert(it is AlreadyExistsException) } - entityPayloadService.checkEntityExistence(entity02Uri, true).shouldSucceed() - } - - @Test - fun `it should filter existing entities from a list of ids`() = runTest { - loadMinimalEntity(entity01Uri, setOf(BEEHIVE_TYPE)) - .sampleDataToNgsiLdEntity() - .map { - entityPayloadService.createEntityPayload( - it.second, - it.first, - now - ) - } - - val existingEntities = entityPayloadService.filterExistingEntitiesAsIds(listOf(entity01Uri, entity02Uri)) - assertEquals(1, existingEntities.size) - assertEquals(entity01Uri, existingEntities[0]) - } - - @Test - fun `it should return an empty list if no ids are provided to the filter on existence`() = runTest { - val existingEntities = entityPayloadService.filterExistingEntitiesAsIds(emptyList()) - assertTrue(existingEntities.isEmpty()) - } - - @Test - fun `it should get the types of an entity`() = runTest { - loadMinimalEntity(entity01Uri, setOf(BEEHIVE_TYPE, APIARY_TYPE)) - .sampleDataToNgsiLdEntity() - .map { - entityPayloadService.createEntityPayload( - it.second, - it.first, - now - ) - } - - entityPayloadService.getTypes(entity01Uri) - .shouldSucceedWith { - assertThatList(it).containsAll(listOf(BEEHIVE_TYPE, APIARY_TYPE)) - } - } - - @Test - fun `it should return an error if trying to get the type of a non-existent entity`() = runTest { - entityPayloadService.getTypes(entity01Uri) - .shouldFail { assertTrue(it is ResourceNotFoundException) } - } - - @Test - fun `it should add a type to an entity`() = runTest { - val expandedPayload = loadSampleData("beehive_expanded.jsonld") - expandedPayload.sampleDataToNgsiLdEntity().map { - entityPayloadService.createEntityPayload( - it.second, - it.first, - now - ) - } - - entityPayloadService.updateTypes(beehiveTestCId, listOf(BEEHIVE_TYPE, APIARY_TYPE), ngsiLdDateTime(), false) - .shouldSucceedWith { - assertTrue(it.isSuccessful()) - assertEquals(1, it.updated.size) - val updatedDetails = it.updated[0] - assertEquals(JSONLD_TYPE, updatedDetails.attributeName) - assertEquals(UpdateOperationResult.APPENDED, updatedDetails.updateOperationResult) - } - - entityPayloadService.retrieve(beehiveTestCId) - .shouldSucceedWith { - assertEquals(listOf(BEEHIVE_TYPE, APIARY_TYPE), it.types) - assertEquals( - listOf(BEEHIVE_TYPE, APIARY_TYPE), - it.payload.asString().deserializeExpandedPayload()[JSONLD_TYPE] - ) - } - } - - @Test - fun `it should add a type to an entity even if existing types are not in the list of types to add`() = runTest { - loadMinimalEntity(entity01Uri, setOf(BEEHIVE_TYPE)) - .sampleDataToNgsiLdEntity() - .map { - entityPayloadService.createEntityPayload( - it.second, - it.first, - now - ) - } - entityPayloadService.updateTypes(entity01Uri, listOf(APIARY_TYPE), ngsiLdDateTime(), false) - .shouldSucceed() - - entityPayloadService.retrieve(entity01Uri) - .shouldSucceedWith { - assertEquals(listOf(BEEHIVE_TYPE, APIARY_TYPE), it.types) - assertEquals( - listOf(BEEHIVE_TYPE, APIARY_TYPE), - it.payload.asString().deserializeExpandedPayload()[JSONLD_TYPE] - ) - } - } - - @Test - fun `it should upsert an entity payload if one already existed`() = runTest { - loadMinimalEntity(entity01Uri, setOf(BEEHIVE_TYPE)) - .sampleDataToNgsiLdEntity() - .map { - entityPayloadService.createEntityPayload( - it.second, - it.first, - now - ) - } - - entityPayloadService.upsertEntityPayload(entity01Uri, EMPTY_PAYLOAD) - .shouldSucceed() - } - - @Test - fun `it should update a specific access policy for a temporal entity`() = runTest { - loadMinimalEntityWithSap(entity01Uri, setOf(BEEHIVE_TYPE), AUTH_READ) - .sampleDataToNgsiLdEntity() - .map { - entityPayloadService.createEntityPayload( - it.second, - it.first, - now - ) - } - loadMinimalEntityWithSap(entity02Uri, setOf(BEEHIVE_TYPE), AUTH_READ) - .sampleDataToNgsiLdEntity() - .map { - entityPayloadService.createEntityPayload( - it.second, - it.first, - now - ) - } - - entityPayloadService.updateSpecificAccessPolicy( - entity01Uri, - buildSapAttribute(AUTH_WRITE) - ).shouldSucceed() - - entityPayloadService.hasSpecificAccessPolicies( - entity01Uri, - listOf(AUTH_READ) - ).shouldSucceedWith { assertFalse(it) } - entityPayloadService.hasSpecificAccessPolicies( - entity01Uri, - listOf(AUTH_WRITE) - ).shouldSucceedWith { assertTrue(it) } - entityPayloadService.hasSpecificAccessPolicies( - entity02Uri, - listOf(AUTH_WRITE) - ).shouldSucceedWith { assertFalse(it) } - } - - @Test - fun `it should delete an entity payload`() = runTest { - coEvery { temporalEntityAttributeService.deleteTemporalAttributesOfEntity(any()) } returns Unit.right() - - loadMinimalEntity(entity01Uri, setOf(BEEHIVE_TYPE)) - .sampleDataToNgsiLdEntity() - .map { - entityPayloadService.createEntityPayload( - it.second, - it.first, - now - ) - } - - entityPayloadService.deleteEntity(entity01Uri) - .shouldSucceedWith { - assertEquals(entity01Uri, it.entityId) - assertNotNull(it.payload) - } - - // if correctly deleted, we should be able to create a new one - loadMinimalEntity(entity01Uri, setOf(BEEHIVE_TYPE)) - .sampleDataToNgsiLdEntity() - .map { - entityPayloadService.createEntityPayload( - it.second, - it.first, - now - ).shouldSucceed() - } - } - - @Test - fun `it should remove the scopes from an entity`() = runTest { - coEvery { - temporalEntityAttributeService.addAttribute(any(), any(), any(), any(), any(), any()) - } returns Unit.right() - coEvery { - temporalEntityAttributeService.getForEntity(any(), any(), any()) - } returns emptyList() - - loadSampleData("beehive_with_scope.jsonld") - .sampleDataToNgsiLdEntity() - .map { - entityPayloadService.createEntityPayload( - it.second, - it.first, - now - ) - } - - entityPayloadService.deleteAttribute(beehiveTestCId, NGSILD_SCOPE_PROPERTY, null) - .shouldSucceed() - - entityPayloadService.retrieve(beehiveTestCId) - .shouldSucceedWith { - assertNull(it.scopes) - } - } - - @Test - fun `it should remove a specific access policy from a entity payload`() = runTest { - loadMinimalEntityWithSap(entity01Uri, setOf(BEEHIVE_TYPE), AUTH_READ, AUTHZ_TEST_COMPOUND_CONTEXTS) - .sampleDataToNgsiLdEntity() - .map { - entityPayloadService.createEntityPayload( - it.second, - it.first, - now - ) - } - - entityPayloadService.hasSpecificAccessPolicies( - entity01Uri, - listOf(AUTH_READ) - ).shouldSucceedWith { assertTrue(it) } - entityPayloadService.removeSpecificAccessPolicy(entity01Uri).shouldSucceed() - entityPayloadService.hasSpecificAccessPolicies( - entity01Uri, - listOf(AUTH_READ) - ).shouldSucceedWith { assertFalse(it) } - } - - @Test - fun `it should return nothing when specific access policy list is empty`() = runTest { - entityPayloadService.hasSpecificAccessPolicies(entity01Uri, emptyList()) - .shouldSucceedWith { assertFalse(it) } - } -} diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/support/BusinessObjectsFactory.kt b/search-service/src/test/kotlin/com/egm/stellio/search/support/BusinessObjectsFactory.kt index 508c350f6..cee889584 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/support/BusinessObjectsFactory.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/support/BusinessObjectsFactory.kt @@ -1,6 +1,12 @@ package com.egm.stellio.search.support -import com.egm.stellio.search.model.* +import com.egm.stellio.search.entity.model.Attribute +import com.egm.stellio.search.entity.model.AttributeMetadata +import com.egm.stellio.search.entity.model.EntitiesQuery +import com.egm.stellio.search.entity.model.Entity +import com.egm.stellio.search.temporal.model.AttributeInstance +import com.egm.stellio.search.temporal.model.TemporalEntitiesQuery +import com.egm.stellio.search.temporal.model.TemporalQuery import com.egm.stellio.shared.model.ExpandedTerm import com.egm.stellio.shared.model.PaginationQuery import com.egm.stellio.shared.model.addNonReifiedTemporalProperty @@ -15,15 +21,15 @@ fun gimmeEntityPayload( entityId: String, types: List = listOf(BEEHIVE_TYPE), payload: String = EMPTY_PAYLOAD -): EntityPayload = +): Entity = gimmeEntityPayload(entityId.toUri(), types, payload) fun gimmeEntityPayload( entityId: URI, types: List = listOf(BEEHIVE_TYPE), payload: String = EMPTY_PAYLOAD -): EntityPayload = - EntityPayload( +): Entity = + Entity( entityId = entityId, types = types, createdAt = ngsiLdDateTime(), @@ -31,16 +37,16 @@ fun gimmeEntityPayload( ) fun gimmeNumericPropertyAttributeInstance( - teaUuid: UUID, + attributeUuid: UUID, timeProperty: AttributeInstance.TemporalProperty = AttributeInstance.TemporalProperty.OBSERVED_AT ): AttributeInstance { val attributeMetadata = AttributeMetadata( measuredValue = Random.nextDouble(), value = null, geoValue = null, - valueType = TemporalEntityAttribute.AttributeValueType.NUMBER, + valueType = Attribute.AttributeValueType.NUMBER, datasetId = null, - type = TemporalEntityAttribute.AttributeType.Property, + type = Attribute.AttributeType.Property, observedAt = ngsiLdDateTime() ) val payload = JsonLdUtils.buildExpandedPropertyValue(attributeMetadata.measuredValue!!) @@ -48,7 +54,7 @@ fun gimmeNumericPropertyAttributeInstance( .getSingleEntry() return AttributeInstance( - temporalEntityAttribute = teaUuid, + attributeUuid = attributeUuid, time = attributeMetadata.observedAt!!, attributeMetadata = attributeMetadata, timeProperty = timeProperty, @@ -57,16 +63,16 @@ fun gimmeNumericPropertyAttributeInstance( } fun gimmeJsonPropertyAttributeInstance( - teaUuid: UUID, + attributeUuid: UUID, timeProperty: AttributeInstance.TemporalProperty = AttributeInstance.TemporalProperty.OBSERVED_AT ): AttributeInstance { val attributeMetadata = AttributeMetadata( measuredValue = null, value = SAMPLE_JSON_PROPERTY_PAYLOAD.asString(), geoValue = null, - valueType = TemporalEntityAttribute.AttributeValueType.JSON, + valueType = Attribute.AttributeValueType.JSON, datasetId = null, - type = TemporalEntityAttribute.AttributeType.JsonProperty, + type = Attribute.AttributeType.JsonProperty, observedAt = ngsiLdDateTime() ) val payload = JsonLdUtils.buildExpandedPropertyValue(attributeMetadata.value!!) @@ -74,7 +80,7 @@ fun gimmeJsonPropertyAttributeInstance( .getSingleEntry() return AttributeInstance( - temporalEntityAttribute = teaUuid, + attributeUuid = attributeUuid, time = attributeMetadata.observedAt!!, attributeMetadata = attributeMetadata, timeProperty = timeProperty, @@ -83,16 +89,16 @@ fun gimmeJsonPropertyAttributeInstance( } fun gimmeLanguagePropertyAttributeInstance( - teaUuid: UUID, + attributeUuid: UUID, timeProperty: AttributeInstance.TemporalProperty = AttributeInstance.TemporalProperty.OBSERVED_AT ): AttributeInstance { val attributeMetadata = AttributeMetadata( measuredValue = null, value = SAMPLE_LANGUAGE_PROPERTY_PAYLOAD.asString(), geoValue = null, - valueType = TemporalEntityAttribute.AttributeValueType.OBJECT, + valueType = Attribute.AttributeValueType.OBJECT, datasetId = null, - type = TemporalEntityAttribute.AttributeType.LanguageProperty, + type = Attribute.AttributeType.LanguageProperty, observedAt = ngsiLdDateTime() ) val payload = JsonLdUtils.buildExpandedPropertyValue(attributeMetadata.value!!) @@ -100,7 +106,7 @@ fun gimmeLanguagePropertyAttributeInstance( .getSingleEntry() return AttributeInstance( - temporalEntityAttribute = teaUuid, + attributeUuid = attributeUuid, time = attributeMetadata.observedAt!!, attributeMetadata = attributeMetadata, timeProperty = timeProperty, @@ -109,16 +115,16 @@ fun gimmeLanguagePropertyAttributeInstance( } fun gimmeVocabPropertyAttributeInstance( - teaUuid: UUID, + attributeUuid: UUID, timeProperty: AttributeInstance.TemporalProperty = AttributeInstance.TemporalProperty.OBSERVED_AT ): AttributeInstance { val attributeMetadata = AttributeMetadata( measuredValue = null, value = SAMPLE_VOCAB_PROPERTY_PAYLOAD.asString(), geoValue = null, - valueType = TemporalEntityAttribute.AttributeValueType.ARRAY, + valueType = Attribute.AttributeValueType.ARRAY, datasetId = null, - type = TemporalEntityAttribute.AttributeType.VocabProperty, + type = Attribute.AttributeType.VocabProperty, observedAt = ngsiLdDateTime() ) val payload = JsonLdUtils.buildExpandedPropertyValue(attributeMetadata.value!!) @@ -126,7 +132,7 @@ fun gimmeVocabPropertyAttributeInstance( .getSingleEntry() return AttributeInstance( - temporalEntityAttribute = teaUuid, + attributeUuid = attributeUuid, time = attributeMetadata.observedAt!!, attributeMetadata = attributeMetadata, timeProperty = timeProperty, diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/support/TestUtils.kt b/search-service/src/test/kotlin/com/egm/stellio/search/support/TestUtils.kt index b83ab9dc0..9cce43e65 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/support/TestUtils.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/support/TestUtils.kt @@ -1,9 +1,9 @@ package com.egm.stellio.search.support -import com.egm.stellio.search.model.AttributeInstance -import com.egm.stellio.search.model.TemporalEntityAttribute -import com.egm.stellio.search.model.TemporalQuery -import com.egm.stellio.search.model.TemporalQuery.Aggregate +import com.egm.stellio.search.entity.model.Attribute +import com.egm.stellio.search.temporal.model.AttributeInstance +import com.egm.stellio.search.temporal.model.TemporalQuery +import com.egm.stellio.search.temporal.model.TemporalQuery.Aggregate import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.model.NgsiLdAttribute import com.egm.stellio.shared.model.toNgsiLdAttributes @@ -62,7 +62,7 @@ fun buildAttributeInstancePayload( observedAt: ZonedDateTime, datasetId: URI? = null, instanceId: URI, - attributeType: TemporalEntityAttribute.AttributeType = TemporalEntityAttribute.AttributeType.Property + attributeType: Attribute.AttributeType = Attribute.AttributeType.Property ): String = serializeObject( mutableMapOf( JSONLD_TYPE to listOf(attributeType.toExpandedName()), @@ -71,7 +71,7 @@ fun buildAttributeInstancePayload( ).apply { if (datasetId != null) put(NGSILD_DATASET_ID_PROPERTY, buildNonReifiedPropertyValue(datasetId.toString())) - if (attributeType == TemporalEntityAttribute.AttributeType.Property) + if (attributeType == Attribute.AttributeType.Property) put(NGSILD_PROPERTY_VALUE, listOf(mapOf(JSONLD_VALUE to value))) else put(NGSILD_RELATIONSHIP_OBJECT, listOf(mapOf(JSONLD_ID to value.toString()))) diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/AggregatedQueryServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/AggregatedTemporalQueryServiceTests.kt similarity index 81% rename from search-service/src/test/kotlin/com/egm/stellio/search/service/AggregatedQueryServiceTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/AggregatedTemporalQueryServiceTests.kt index ac8eab234..859aca541 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/AggregatedQueryServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/AggregatedTemporalQueryServiceTests.kt @@ -1,7 +1,9 @@ -package com.egm.stellio.search.service +package com.egm.stellio.search.temporal.service -import com.egm.stellio.search.model.* +import com.egm.stellio.search.entity.model.Attribute +import com.egm.stellio.search.entity.service.EntityAttributeService import com.egm.stellio.search.support.* +import com.egm.stellio.search.temporal.model.* import com.egm.stellio.shared.model.OperationNotSupportedException import com.egm.stellio.shared.util.* import kotlinx.coroutines.test.runTest @@ -17,6 +19,7 @@ import org.junit.jupiter.params.provider.CsvSource import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.data.r2dbc.core.R2dbcEntityTemplate +import org.springframework.data.r2dbc.core.delete import org.springframework.test.context.ActiveProfiles import java.time.LocalDate import java.time.OffsetTime @@ -25,19 +28,19 @@ import java.util.UUID @SpringBootTest @ActiveProfiles("test") -class AggregatedQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { +class AggregatedTemporalQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { @Autowired private lateinit var attributeInstanceService: AttributeInstanceService @Autowired - private lateinit var temporalEntityAttributeService: TemporalEntityAttributeService + private lateinit var entityAttributeService: EntityAttributeService @Autowired private lateinit var r2dbcEntityTemplate: R2dbcEntityTemplate private val now = ngsiLdDateTime() - private val teaUuid = UUID.randomUUID() + private val attributeUuid = UUID.randomUUID() private val entityId = "urn:ngsi-ld:BeeHive:${UUID.randomUUID()}".toUri() @AfterEach @@ -52,9 +55,7 @@ class AggregatedQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { .rowsUpdated() .block() - r2dbcEntityTemplate.delete(TemporalEntityAttribute::class.java) - .all() - .block() + r2dbcEntityTemplate.delete().from("temporal_entity_attribute").all().block() } @ParameterizedTest @@ -69,15 +70,15 @@ class AggregatedQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { "sumsq, 385.0" ) fun `it should correctly aggregate on JSON Number values`(aggrMethod: String, expectedValue: String) = runTest { - val temporalEntityAttribute = createTemporalEntityAttribute(TemporalEntityAttribute.AttributeValueType.NUMBER) + val attribute = createAttribute(Attribute.AttributeValueType.NUMBER) (1..10).forEach { i -> - val attributeInstance = gimmeNumericPropertyAttributeInstance(teaUuid) + val attributeInstance = gimmeNumericPropertyAttributeInstance(attributeUuid) .copy(measuredValue = i.toDouble()) attributeInstanceService.create(attributeInstance) } val temporalEntitiesQuery = createTemporalEntitiesQuery(aggrMethod) - attributeInstanceService.search(temporalEntitiesQuery, temporalEntityAttribute, now) + attributeInstanceService.search(temporalEntitiesQuery, attribute, now) .shouldSucceedWith { results -> assertAggregatedResult(results, aggrMethod) .matches({ @@ -98,15 +99,15 @@ class AggregatedQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { "sumsq, ''" ) fun `it should correctly aggregate on JSON String values`(aggrMethod: String, expectedValue: String?) = runTest { - val temporalEntityAttribute = createTemporalEntityAttribute(TemporalEntityAttribute.AttributeValueType.STRING) + val attribute = createAttribute(Attribute.AttributeValueType.STRING) (1..10).forEach { i -> - val attributeInstance = gimmeNumericPropertyAttributeInstance(teaUuid) + val attributeInstance = gimmeNumericPropertyAttributeInstance(attributeUuid) .copy(measuredValue = null, value = "a$i") attributeInstanceService.create(attributeInstance) } val temporalEntitiesQuery = createTemporalEntitiesQuery(aggrMethod) - attributeInstanceService.search(temporalEntitiesQuery, temporalEntityAttribute, now) + attributeInstanceService.search(temporalEntitiesQuery, attribute, now) .shouldSucceedWith { results -> assertAggregatedResult(results, aggrMethod) .matches({ @@ -127,9 +128,9 @@ class AggregatedQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { "sumsq, ''" ) fun `it should correctly aggregate on JSON Object values`(aggrMethod: String, expectedValue: String?) = runTest { - val temporalEntityAttribute = createTemporalEntityAttribute(TemporalEntityAttribute.AttributeValueType.OBJECT) + val attribute = createAttribute(Attribute.AttributeValueType.OBJECT) (1..10).forEach { i -> - val attributeInstance = gimmeNumericPropertyAttributeInstance(teaUuid) + val attributeInstance = gimmeNumericPropertyAttributeInstance(attributeUuid) .copy( measuredValue = null, value = """ @@ -142,7 +143,7 @@ class AggregatedQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { } val temporalEntitiesQuery = createTemporalEntitiesQuery(aggrMethod) - attributeInstanceService.search(temporalEntitiesQuery, temporalEntityAttribute, now) + attributeInstanceService.search(temporalEntitiesQuery, attribute, now) .shouldSucceedWith { results -> assertAggregatedResult(results, aggrMethod) .matches({ @@ -163,9 +164,9 @@ class AggregatedQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { "sumsq, ''" ) fun `it should correctly aggregate on JSON Array values`(aggrMethod: String, expectedValue: String?) = runTest { - val temporalEntityAttribute = createTemporalEntityAttribute(TemporalEntityAttribute.AttributeValueType.ARRAY) + val attribute = createAttribute(Attribute.AttributeValueType.ARRAY) (1..10).forEach { i -> - val attributeInstance = gimmeNumericPropertyAttributeInstance(teaUuid) + val attributeInstance = gimmeNumericPropertyAttributeInstance(attributeUuid) .copy( measuredValue = null, value = """ @@ -176,7 +177,7 @@ class AggregatedQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { } val temporalEntitiesQuery = createTemporalEntitiesQuery(aggrMethod) - attributeInstanceService.search(temporalEntitiesQuery, temporalEntityAttribute, now) + attributeInstanceService.search(temporalEntitiesQuery, attribute, now) .shouldSucceedWith { results -> assertAggregatedResult(results, aggrMethod) .matches({ @@ -197,9 +198,9 @@ class AggregatedQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { "sumsq, 5.0" ) fun `it should correctly aggregate on JSON Boolean values`(aggrMethod: String, expectedValue: String?) = runTest { - val temporalEntityAttribute = createTemporalEntityAttribute(TemporalEntityAttribute.AttributeValueType.BOOLEAN) + val attribute = createAttribute(Attribute.AttributeValueType.BOOLEAN) (1..10).forEach { i -> - val attributeInstance = gimmeNumericPropertyAttributeInstance(teaUuid) + val attributeInstance = gimmeNumericPropertyAttributeInstance(attributeUuid) .copy( measuredValue = null, value = if (i % 2 == 0) "true" else "false" @@ -208,7 +209,7 @@ class AggregatedQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { } val temporalEntitiesQuery = createTemporalEntitiesQuery(aggrMethod) - attributeInstanceService.search(temporalEntitiesQuery, temporalEntityAttribute, now) + attributeInstanceService.search(temporalEntitiesQuery, attribute, now) .shouldSucceedWith { results -> assertAggregatedResult(results, aggrMethod) .matches({ @@ -229,10 +230,10 @@ class AggregatedQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { "sumsq, ''" ) fun `it should correctly aggregate on DateTime values`(aggrMethod: String, expectedValue: String?) = runTest { - val temporalEntityAttribute = createTemporalEntityAttribute(TemporalEntityAttribute.AttributeValueType.DATETIME) + val attribute = createAttribute(Attribute.AttributeValueType.DATETIME) val baseDateTime = ZonedDateTime.parse("2023-03-05T00:01:01Z") (1..10).forEach { i -> - val attributeInstance = gimmeNumericPropertyAttributeInstance(teaUuid) + val attributeInstance = gimmeNumericPropertyAttributeInstance(attributeUuid) .copy( measuredValue = null, value = baseDateTime.plusHours(i.toLong()).toString() @@ -241,7 +242,7 @@ class AggregatedQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { } val temporalEntitiesQuery = createTemporalEntitiesQuery(aggrMethod) - attributeInstanceService.search(temporalEntitiesQuery, temporalEntityAttribute, now) + attributeInstanceService.search(temporalEntitiesQuery, attribute, now) .shouldSucceedWith { results -> assertAggregatedResult(results, aggrMethod) .matches({ @@ -262,10 +263,10 @@ class AggregatedQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { "sumsq, ''" ) fun `it should correctly aggregate on Date values`(aggrMethod: String, expectedValue: String?) = runTest { - val temporalEntityAttribute = createTemporalEntityAttribute(TemporalEntityAttribute.AttributeValueType.DATE) + val attribute = createAttribute(Attribute.AttributeValueType.DATE) val baseDateTime = LocalDate.parse("2023-03-05") (1..10).forEach { i -> - val attributeInstance = gimmeNumericPropertyAttributeInstance(teaUuid) + val attributeInstance = gimmeNumericPropertyAttributeInstance(attributeUuid) .copy( measuredValue = null, value = baseDateTime.plusDays(i.toLong()).toString() @@ -274,7 +275,7 @@ class AggregatedQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { } val temporalEntitiesQuery = createTemporalEntitiesQuery(aggrMethod) - attributeInstanceService.search(temporalEntitiesQuery, temporalEntityAttribute, now) + attributeInstanceService.search(temporalEntitiesQuery, attribute, now) .shouldSucceedWith { results -> assertAggregatedResult(results, aggrMethod) .matches({ @@ -295,10 +296,10 @@ class AggregatedQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { "sumsq, ''" ) fun `it should correctly aggregate on Time values`(aggrMethod: String, expectedValue: String?) = runTest { - val temporalEntityAttribute = createTemporalEntityAttribute(TemporalEntityAttribute.AttributeValueType.TIME) + val attribute = createAttribute(Attribute.AttributeValueType.TIME) val baseDateTime = OffsetTime.parse("00:00:01Z") (1..10).forEach { i -> - val attributeInstance = gimmeNumericPropertyAttributeInstance(teaUuid) + val attributeInstance = gimmeNumericPropertyAttributeInstance(attributeUuid) .copy( measuredValue = null, value = baseDateTime.plusHours(i.toLong()).toString() @@ -307,7 +308,7 @@ class AggregatedQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { } val temporalEntitiesQuery = createTemporalEntitiesQuery(aggrMethod) - attributeInstanceService.search(temporalEntitiesQuery, temporalEntityAttribute, now) + attributeInstanceService.search(temporalEntitiesQuery, attribute, now) .shouldSucceedWith { results -> assertAggregatedResult(results, aggrMethod) .matches({ @@ -328,9 +329,9 @@ class AggregatedQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { "sumsq, ''" ) fun `it should correctly aggregate on URI values`(aggrMethod: String, expectedValue: String?) = runTest { - val temporalEntityAttribute = createTemporalEntityAttribute(TemporalEntityAttribute.AttributeValueType.URI) + val attribute = createAttribute(Attribute.AttributeValueType.URI) (1..10).forEach { i -> - val attributeInstance = gimmeNumericPropertyAttributeInstance(teaUuid) + val attributeInstance = gimmeNumericPropertyAttributeInstance(attributeUuid) .copy( measuredValue = null, value = "urn:ngsi-ld:Entity:$i" @@ -339,7 +340,7 @@ class AggregatedQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { } val temporalEntitiesQuery = createTemporalEntitiesQuery(aggrMethod) - attributeInstanceService.search(temporalEntitiesQuery, temporalEntityAttribute, now) + attributeInstanceService.search(temporalEntitiesQuery, attribute, now) .shouldSucceedWith { results -> assertAggregatedResult(results, aggrMethod) .matches({ @@ -350,9 +351,9 @@ class AggregatedQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { @Test fun `it should aggregate on the whole time range if no aggrPeriodDuration is given`() = runTest { - val temporalEntityAttribute = createTemporalEntityAttribute(TemporalEntityAttribute.AttributeValueType.NUMBER) + val attribute = createAttribute(Attribute.AttributeValueType.NUMBER) (1..10).forEach { i -> - val attributeInstance = gimmeNumericPropertyAttributeInstance(teaUuid) + val attributeInstance = gimmeNumericPropertyAttributeInstance(attributeUuid) .copy(measuredValue = i.toDouble()) attributeInstanceService.create(attributeInstance) } @@ -362,7 +363,7 @@ class AggregatedQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { temporalEntitiesQuery.copy( temporalQuery = temporalEntitiesQuery.temporalQuery.copy(aggrPeriodDuration = "PT0S") ), - temporalEntityAttribute, + attribute, now ).shouldSucceedWith { results -> assertAggregatedResult(results, "avg") @@ -386,10 +387,10 @@ class AggregatedQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { aggrPeriodDuration: String, expectedNumberOfBuckets: Int ) = runTest { - val temporalEntityAttribute = createTemporalEntityAttribute(TemporalEntityAttribute.AttributeValueType.NUMBER) + val attribute = createAttribute(Attribute.AttributeValueType.NUMBER) val startTimestamp = ZonedDateTime.parse("2023-12-28T12:00:00Z") (1..10).forEach { i -> - val attributeInstance = gimmeNumericPropertyAttributeInstance(teaUuid) + val attributeInstance = gimmeNumericPropertyAttributeInstance(attributeUuid) .copy(time = startTimestamp.plusDays(i.toLong())) attributeInstanceService.create(attributeInstance) } @@ -399,7 +400,7 @@ class AggregatedQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { temporalEntitiesQuery.copy( temporalQuery = temporalEntitiesQuery.temporalQuery.copy(timeAt = startTimestamp) ), - temporalEntityAttribute, + attribute, startTimestamp ) .shouldSucceedWith { results -> @@ -409,37 +410,37 @@ class AggregatedQueryServiceTests : WithTimescaleContainer, WithKafkaContainer { @Test fun `it should handle aggregates for an attribute having different types of values in history`() = runTest { - val temporalEntityAttribute = createTemporalEntityAttribute(TemporalEntityAttribute.AttributeValueType.ARRAY) + val attribute = createAttribute(Attribute.AttributeValueType.ARRAY) (1..10).forEach { i -> - val attributeInstanceWithArrayValue = gimmeNumericPropertyAttributeInstance(teaUuid) + val attributeInstanceWithArrayValue = gimmeNumericPropertyAttributeInstance(attributeUuid) .copy(measuredValue = null, value = "[ $i ]") attributeInstanceService.create(attributeInstanceWithArrayValue) - val attributeInstanceWithStringValue = gimmeNumericPropertyAttributeInstance(teaUuid) + val attributeInstanceWithStringValue = gimmeNumericPropertyAttributeInstance(attributeUuid) .copy(measuredValue = null, value = "$i") attributeInstanceService.create(attributeInstanceWithStringValue) } val temporalEntitiesQuery = createTemporalEntitiesQuery("max") - attributeInstanceService.search(temporalEntitiesQuery, temporalEntityAttribute, now) + attributeInstanceService.search(temporalEntitiesQuery, attribute, now) .shouldFail { assertInstanceOf(OperationNotSupportedException::class.java, it) assertEquals("cannot get array length of a scalar", it.message) } } - private suspend fun createTemporalEntityAttribute( - attributeValueType: TemporalEntityAttribute.AttributeValueType - ): TemporalEntityAttribute { - val temporalEntityAttribute = TemporalEntityAttribute( - id = teaUuid, + private suspend fun createAttribute( + attributeValueType: Attribute.AttributeValueType + ): Attribute { + val attribute = Attribute( + id = attributeUuid, entityId = entityId, attributeName = INCOMING_PROPERTY, attributeValueType = attributeValueType, createdAt = now, payload = EMPTY_JSON_PAYLOAD ) - temporalEntityAttributeService.create(temporalEntityAttribute) - return temporalEntityAttribute + entityAttributeService.create(attribute) + return attribute } private fun createTemporalEntitiesQuery( diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/AttributeInstanceServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/AttributeInstanceServiceTests.kt similarity index 84% rename from search-service/src/test/kotlin/com/egm/stellio/search/service/AttributeInstanceServiceTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/AttributeInstanceServiceTests.kt index 8f6170bdb..44998267f 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/AttributeInstanceServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/AttributeInstanceServiceTests.kt @@ -1,8 +1,14 @@ -package com.egm.stellio.search.service +package com.egm.stellio.search.temporal.service -import com.egm.stellio.search.model.* -import com.egm.stellio.search.model.TemporalQuery.Timerel +import com.egm.stellio.search.entity.model.Attribute +import com.egm.stellio.search.entity.model.AttributeMetadata +import com.egm.stellio.search.entity.service.EntityAttributeService import com.egm.stellio.search.support.* +import com.egm.stellio.search.temporal.model.AttributeInstance +import com.egm.stellio.search.temporal.model.FullAttributeInstanceResult +import com.egm.stellio.search.temporal.model.SimplifiedAttributeInstanceResult +import com.egm.stellio.search.temporal.model.TemporalQuery +import com.egm.stellio.search.temporal.model.TemporalQuery.Timerel import com.egm.stellio.shared.model.ExpandedAttributes import com.egm.stellio.shared.model.ResourceNotFoundException import com.egm.stellio.shared.model.addNonReifiedTemporalProperty @@ -50,7 +56,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer private lateinit var attributeInstanceService: AttributeInstanceService @Autowired - private lateinit var temporalEntityAttributeService: TemporalEntityAttributeService + private lateinit var entityAttributeService: EntityAttributeService @Autowired private lateinit var databaseClient: DatabaseClient @@ -59,74 +65,74 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer private lateinit var r2dbcEntityTemplate: R2dbcEntityTemplate private val now = Instant.now().atZone(ZoneOffset.UTC) - private lateinit var incomingTemporalEntityAttribute: TemporalEntityAttribute - private lateinit var outgoingTemporalEntityAttribute: TemporalEntityAttribute - private lateinit var jsonTemporalEntityAttribute: TemporalEntityAttribute - private lateinit var languageTemporalEntityAttribute: TemporalEntityAttribute - private lateinit var vocabTemporalEntityAttribute: TemporalEntityAttribute + private lateinit var incomingAttribute: Attribute + private lateinit var outgoingAttribute: Attribute + private lateinit var jsonAttribute: Attribute + private lateinit var languageAttribute: Attribute + private lateinit var vocabAttribute: Attribute val entityId = "urn:ngsi-ld:BeeHive:TESTC".toUri() @BeforeAll - fun createTemporalEntityAttribute() { - incomingTemporalEntityAttribute = TemporalEntityAttribute( + fun createAttribute() { + incomingAttribute = Attribute( entityId = entityId, attributeName = INCOMING_PROPERTY, - attributeValueType = TemporalEntityAttribute.AttributeValueType.NUMBER, + attributeValueType = Attribute.AttributeValueType.NUMBER, createdAt = now, payload = EMPTY_JSON_PAYLOAD ) runBlocking { - temporalEntityAttributeService.create(incomingTemporalEntityAttribute) + entityAttributeService.create(incomingAttribute) } - outgoingTemporalEntityAttribute = TemporalEntityAttribute( + outgoingAttribute = Attribute( entityId = entityId, attributeName = OUTGOING_PROPERTY, - attributeValueType = TemporalEntityAttribute.AttributeValueType.NUMBER, + attributeValueType = Attribute.AttributeValueType.NUMBER, createdAt = now, payload = EMPTY_JSON_PAYLOAD ) runBlocking { - temporalEntityAttributeService.create(outgoingTemporalEntityAttribute) + entityAttributeService.create(outgoingAttribute) } - jsonTemporalEntityAttribute = TemporalEntityAttribute( + jsonAttribute = Attribute( entityId = entityId, attributeName = LUMINOSITY_JSONPROPERTY, - attributeValueType = TemporalEntityAttribute.AttributeValueType.JSON, + attributeValueType = Attribute.AttributeValueType.JSON, createdAt = now, payload = SAMPLE_JSON_PROPERTY_PAYLOAD ) runBlocking { - temporalEntityAttributeService.create(jsonTemporalEntityAttribute) + entityAttributeService.create(jsonAttribute) } - languageTemporalEntityAttribute = TemporalEntityAttribute( + languageAttribute = Attribute( entityId = entityId, attributeName = FRIENDLYNAME_LANGUAGEPROPERTY, - attributeValueType = TemporalEntityAttribute.AttributeValueType.ARRAY, + attributeValueType = Attribute.AttributeValueType.ARRAY, createdAt = now, payload = SAMPLE_LANGUAGE_PROPERTY_PAYLOAD ) runBlocking { - temporalEntityAttributeService.create(languageTemporalEntityAttribute) + entityAttributeService.create(languageAttribute) } - vocabTemporalEntityAttribute = TemporalEntityAttribute( + vocabAttribute = Attribute( entityId = entityId, attributeName = CATEGORY_VOCAPPROPERTY, - attributeValueType = TemporalEntityAttribute.AttributeValueType.ARRAY, + attributeValueType = Attribute.AttributeValueType.ARRAY, createdAt = now, payload = SAMPLE_VOCAB_PROPERTY_PAYLOAD ) runBlocking { - temporalEntityAttributeService.create(vocabTemporalEntityAttribute) + entityAttributeService.create(vocabAttribute) } } @@ -145,7 +151,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer @Test fun `it should retrieve a full instance if temporalValues are not asked for`() = runTest { - val observation = gimmeNumericPropertyAttributeInstance(incomingTemporalEntityAttribute.id).copy( + val observation = gimmeNumericPropertyAttributeInstance(incomingAttribute.id).copy( time = now, measuredValue = 12.4 ) @@ -157,17 +163,17 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer timeAt = now.minusHours(1) ) ) - attributeInstanceService.search(temporalEntitiesQuery, incomingTemporalEntityAttribute) + attributeInstanceService.search(temporalEntitiesQuery, incomingAttribute) .shouldSucceedWith { assertThat(it) .singleElement() - .hasFieldOrPropertyWithValue("temporalEntityAttribute", incomingTemporalEntityAttribute.id) + .hasFieldOrPropertyWithValue("attributeUuid", incomingAttribute.id) } } @Test fun `it should retrieve an instance having the corresponding time property value`() = runTest { - val observation = gimmeNumericPropertyAttributeInstance(incomingTemporalEntityAttribute.id).copy( + val observation = gimmeNumericPropertyAttributeInstance(incomingAttribute.id).copy( timeProperty = AttributeInstance.TemporalProperty.CREATED_AT, time = now, measuredValue = 12.4 @@ -181,7 +187,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer timeproperty = AttributeInstance.TemporalProperty.CREATED_AT ) ) - attributeInstanceService.search(temporalEntitiesQuery, incomingTemporalEntityAttribute) + attributeInstanceService.search(temporalEntitiesQuery, incomingAttribute) .shouldSucceedWith { assertThat(it) .singleElement() @@ -190,7 +196,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer @Test fun `it should retrieve an instance with audit info if time property is not observedAt`() = runTest { - val observation = gimmeNumericPropertyAttributeInstance(incomingTemporalEntityAttribute.id).copy( + val observation = gimmeNumericPropertyAttributeInstance(incomingAttribute.id).copy( timeProperty = AttributeInstance.TemporalProperty.CREATED_AT, time = now, measuredValue = 12.4, @@ -205,7 +211,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer timeproperty = AttributeInstance.TemporalProperty.CREATED_AT ) ) - attributeInstanceService.search(temporalEntitiesQuery, incomingTemporalEntityAttribute) + attributeInstanceService.search(temporalEntitiesQuery, incomingAttribute) .shouldSucceedWith { assertThat(it) .singleElement() @@ -215,7 +221,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer @Test fun `it should not retrieve an instance not having the corresponding time property value`() = runTest { - val observation = gimmeNumericPropertyAttributeInstance(incomingTemporalEntityAttribute.id).copy( + val observation = gimmeNumericPropertyAttributeInstance(incomingAttribute.id).copy( timeProperty = AttributeInstance.TemporalProperty.CREATED_AT, time = now, measuredValue = 12.4 @@ -229,7 +235,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer timeproperty = AttributeInstance.TemporalProperty.MODIFIED_AT ) ) - attributeInstanceService.search(temporalEntitiesQuery, incomingTemporalEntityAttribute) + attributeInstanceService.search(temporalEntitiesQuery, incomingAttribute) .shouldSucceedWith { assertThat(it) .isEmpty() @@ -239,7 +245,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer @Test fun `it should retrieve all full instances if temporalValues are not asked for`() = runTest { (1..10).forEach { _ -> - attributeInstanceService.create(gimmeNumericPropertyAttributeInstance(incomingTemporalEntityAttribute.id)) + attributeInstanceService.create(gimmeNumericPropertyAttributeInstance(incomingAttribute.id)) } val temporalEntitiesQuery = gimmeTemporalEntitiesQuery( @@ -248,7 +254,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer timeAt = now.minusHours(1) ) ) - attributeInstanceService.search(temporalEntitiesQuery, incomingTemporalEntityAttribute) + attributeInstanceService.search(temporalEntitiesQuery, incomingAttribute) .shouldSucceedWith { assertThat(it) .hasSize(10) @@ -258,12 +264,12 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer @Test fun `it should retrieve all instances when no timerel and time parameters are provided`() = runTest { (1..10).forEach { _ -> - attributeInstanceService.create(gimmeNumericPropertyAttributeInstance(incomingTemporalEntityAttribute.id)) + attributeInstanceService.create(gimmeNumericPropertyAttributeInstance(incomingAttribute.id)) } attributeInstanceService.search( gimmeTemporalEntitiesQuery(buildDefaultTestTemporalQuery()), - incomingTemporalEntityAttribute + incomingAttribute ) .shouldSucceedWith { assertThat(it) @@ -273,15 +279,15 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer @Test fun `it should retrieve instances of a temporal entity attribute whose value type is Any`() = runTest { - val temporalEntityAttribute2 = TemporalEntityAttribute( + val attribute2 = Attribute( entityId = entityId, attributeName = "propWithStringValue", - attributeValueType = TemporalEntityAttribute.AttributeValueType.STRING, + attributeValueType = Attribute.AttributeValueType.STRING, createdAt = now, payload = EMPTY_JSON_PAYLOAD ) - temporalEntityAttributeService.create(temporalEntityAttribute2) + entityAttributeService.create(attribute2) (1..10).forEach { _ -> val observedAt = Instant.now().atZone(ZoneOffset.UTC) @@ -289,13 +295,13 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer measuredValue = null, value = "some value", geoValue = null, - valueType = TemporalEntityAttribute.AttributeValueType.STRING, + valueType = Attribute.AttributeValueType.STRING, datasetId = null, - type = TemporalEntityAttribute.AttributeType.Property, + type = Attribute.AttributeType.Property, observedAt = observedAt ) val attributeInstance = AttributeInstance( - temporalEntityAttribute = temporalEntityAttribute2.id, + attributeUuid = attribute2.id, time = observedAt, attributeMetadata = attributeMetadata, payload = buildExpandedPropertyValue(attributeMetadata.value!!) @@ -307,12 +313,12 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer attributeInstanceService.search( gimmeTemporalEntitiesQuery(buildDefaultTestTemporalQuery(), withTemporalValues = true), - temporalEntityAttribute2 + attribute2 ).shouldSucceedWith { results -> assertThat(results) .hasSize(10) .allMatch { - it.temporalEntityAttribute == temporalEntityAttribute2.id && + it.attributeUuid == attribute2.id && (it as SimplifiedAttributeInstanceResult).value == "some value" } } @@ -322,7 +328,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer fun `it should set the start time to the oldest value if asking for no timerel`() = runTest { (1..9).forEachIndexed { index, _ -> val attributeInstance = - gimmeNumericPropertyAttributeInstance(incomingTemporalEntityAttribute.id) + gimmeNumericPropertyAttributeInstance(incomingAttribute.id) .copy( measuredValue = index.toDouble(), time = ZonedDateTime.parse("2022-07-0${index + 1}T00:00:00Z") @@ -339,7 +345,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer val origin = attributeInstanceService.selectOldestDate( temporalEntitiesQuery.temporalQuery, - listOf(incomingTemporalEntityAttribute) + listOf(incomingAttribute) ) assertNotNull(origin) @@ -350,7 +356,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer fun `it should include lower bound of interval with after timerel`() = runTest { (1..5).forEachIndexed { index, _ -> val attributeInstance = - gimmeNumericPropertyAttributeInstance(incomingTemporalEntityAttribute.id) + gimmeNumericPropertyAttributeInstance(incomingAttribute.id) .copy( measuredValue = index.toDouble(), time = ZonedDateTime.parse("2022-07-0${index + 1}T00:00:00Z") @@ -365,7 +371,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer ) ) - attributeInstanceService.search(temporalEntitiesQuery, incomingTemporalEntityAttribute) + attributeInstanceService.search(temporalEntitiesQuery, incomingAttribute) .shouldSucceedWith { assertEquals(5, it.size) } @@ -375,7 +381,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer fun `it should exclude upper bound of interval with between timerel`() = runTest { (1..5).forEachIndexed { index, _ -> val attributeInstance = - gimmeNumericPropertyAttributeInstance(incomingTemporalEntityAttribute.id) + gimmeNumericPropertyAttributeInstance(incomingAttribute.id) .copy( measuredValue = index.toDouble(), time = ZonedDateTime.parse("2022-07-0${index + 1}T00:00:00Z") @@ -391,7 +397,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer ) ) - attributeInstanceService.search(temporalEntitiesQuery, incomingTemporalEntityAttribute) + attributeInstanceService.search(temporalEntitiesQuery, incomingAttribute) .shouldSucceedWith { assertEquals(4, it.size) (it as List).forEach { result -> @@ -403,7 +409,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer @Test fun `it should only return the limited instances asked in the temporal query`() = runTest { (1..10).forEach { _ -> - val attributeInstance = gimmeNumericPropertyAttributeInstance(incomingTemporalEntityAttribute.id) + val attributeInstance = gimmeNumericPropertyAttributeInstance(incomingAttribute.id) .copy(measuredValue = 1.0) attributeInstanceService.create(attributeInstance) } @@ -415,7 +421,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer lastN = 5 ) ) - attributeInstanceService.search(temporalEntitiesQuery, incomingTemporalEntityAttribute) + attributeInstanceService.search(temporalEntitiesQuery, incomingAttribute) .shouldSucceedWith { assertThat(it) .hasSize(5) @@ -427,7 +433,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer val now = ngsiLdDateTime() (1..10).forEachIndexed { index, _ -> val attributeInstance = - gimmeNumericPropertyAttributeInstance(incomingTemporalEntityAttribute.id) + gimmeNumericPropertyAttributeInstance(incomingAttribute.id) .copy( measuredValue = 1.0, time = now.minusSeconds(index.toLong()) @@ -445,7 +451,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer ), withAggregatedValues = true ) - attributeInstanceService.search(temporalEntitiesQuery, incomingTemporalEntityAttribute) + attributeInstanceService.search(temporalEntitiesQuery, incomingAttribute) .shouldSucceedWith { assertThat(it) .hasSize(5) @@ -454,21 +460,21 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer @Test fun `it should only retrieve the temporal evolution of the provided temporal entity attribute`() = runTest { - val temporalEntityAttribute2 = TemporalEntityAttribute( + val attribute2 = Attribute( entityId = entityId, attributeName = OUTGOING_COMPACT_PROPERTY, - attributeValueType = TemporalEntityAttribute.AttributeValueType.NUMBER, + attributeValueType = Attribute.AttributeValueType.NUMBER, createdAt = now, payload = EMPTY_JSON_PAYLOAD ) - temporalEntityAttributeService.create(temporalEntityAttribute2) + entityAttributeService.create(attribute2) (1..10).forEach { _ -> - attributeInstanceService.create(gimmeNumericPropertyAttributeInstance(incomingTemporalEntityAttribute.id)) + attributeInstanceService.create(gimmeNumericPropertyAttributeInstance(incomingAttribute.id)) } (1..5).forEach { _ -> - attributeInstanceService.create(gimmeNumericPropertyAttributeInstance(temporalEntityAttribute2.id)) + attributeInstanceService.create(gimmeNumericPropertyAttributeInstance(attribute2.id)) } val temporalEntitiesQuery = gimmeTemporalEntitiesQuery( @@ -477,12 +483,12 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer timeAt = now.minusHours(1) ) ) - attributeInstanceService.search(temporalEntitiesQuery, incomingTemporalEntityAttribute) + attributeInstanceService.search(temporalEntitiesQuery, incomingAttribute) .shouldSucceedWith { results -> assertThat(results) .hasSize(10) .allMatch { - it.temporalEntityAttribute == incomingTemporalEntityAttribute.id + it.attributeUuid == incomingAttribute.id } } } @@ -490,7 +496,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer @Test fun `it should not retrieve any instance if temporal entity does not match`() = runTest { (1..10).forEach { _ -> - attributeInstanceService.create(gimmeNumericPropertyAttributeInstance(incomingTemporalEntityAttribute.id)) + attributeInstanceService.create(gimmeNumericPropertyAttributeInstance(incomingAttribute.id)) } val temporalEntitiesQuery = gimmeTemporalEntitiesQuery( @@ -501,7 +507,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer ) attributeInstanceService.search( temporalEntitiesQuery, - incomingTemporalEntityAttribute.copy(id = UUID.randomUUID()) + incomingAttribute.copy(id = UUID.randomUUID()) ).shouldSucceedWith { assertThat(it) .isEmpty() @@ -511,7 +517,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer @Test fun `it should not retrieve any instance if there is no value in the time interval`() = runTest { (1..10).forEach { _ -> - attributeInstanceService.create(gimmeNumericPropertyAttributeInstance(incomingTemporalEntityAttribute.id)) + attributeInstanceService.create(gimmeNumericPropertyAttributeInstance(incomingAttribute.id)) } val temporalEntitiesQuery = gimmeTemporalEntitiesQuery( @@ -520,7 +526,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer timeAt = now.plusHours(1) ) ) - attributeInstanceService.search(temporalEntitiesQuery, incomingTemporalEntityAttribute) + attributeInstanceService.search(temporalEntitiesQuery, incomingAttribute) .shouldSucceedWith { assertThat(it) .isEmpty() @@ -529,7 +535,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer @Test fun `it should update an existing attribute instance with same observation date`() = runTest { - val attributeInstance = gimmeNumericPropertyAttributeInstance(incomingTemporalEntityAttribute.id) + val attributeInstance = gimmeNumericPropertyAttributeInstance(incomingAttribute.id) attributeInstanceService.create(attributeInstance) @@ -544,7 +550,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer ), withTemporalValues = true ), - incomingTemporalEntityAttribute + incomingAttribute ).shouldSucceedWith { results -> assertThat(results) .singleElement() @@ -561,9 +567,9 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer measuredValue = 550.0, value = null, geoValue = null, - valueType = TemporalEntityAttribute.AttributeValueType.NUMBER, + valueType = Attribute.AttributeValueType.NUMBER, datasetId = null, - type = TemporalEntityAttribute.AttributeType.Property, + type = Attribute.AttributeType.Property, observedAt = ZonedDateTime.parse("2015-10-18T11:20:30.000001Z") ) val attributeValues = mapOf( @@ -581,7 +587,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer ) attributeInstanceService.addAttributeInstance( - incomingTemporalEntityAttribute.id, + incomingAttribute.id, attributeMetadata, attributeValues ) @@ -620,9 +626,9 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer measuredValue = null, value = false.toString(), geoValue = null, - valueType = TemporalEntityAttribute.AttributeValueType.BOOLEAN, + valueType = Attribute.AttributeValueType.BOOLEAN, datasetId = null, - type = TemporalEntityAttribute.AttributeType.Property, + type = Attribute.AttributeType.Property, observedAt = ZonedDateTime.parse("2015-10-18T11:20:30.000001Z") ) val attributeValues = mapOf( @@ -640,7 +646,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer ) attributeInstanceService.addAttributeInstance( - incomingTemporalEntityAttribute.id, + incomingAttribute.id, attributeMetadata, attributeValues ) @@ -674,7 +680,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer @Test fun `it should modify attribute instance for a property`() = runTest { - val attributeInstance = gimmeNumericPropertyAttributeInstance(incomingTemporalEntityAttribute.id) + val attributeInstance = gimmeNumericPropertyAttributeInstance(incomingAttribute.id) attributeInstanceService.create(attributeInstance) val instanceTemporalFragment = @@ -699,7 +705,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer jsonLdAttribute.entries.first().value ).shouldSucceed() - attributeInstanceService.search(temporalEntitiesQuery, incomingTemporalEntityAttribute) + attributeInstanceService.search(temporalEntitiesQuery, incomingAttribute) .shouldSucceedWith { (it as List).single { result -> result.time == ZonedDateTime.parse("2023-03-13T12:33:06Z") && @@ -711,7 +717,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer @Test fun `it should modify attribute instance for a JSON property`() = runTest { - val attributeInstance = gimmeJsonPropertyAttributeInstance(jsonTemporalEntityAttribute.id) + val attributeInstance = gimmeJsonPropertyAttributeInstance(jsonAttribute.id) attributeInstanceService.create(attributeInstance) val instanceTemporalFragment = @@ -737,7 +743,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer jsonLdAttribute.entries.first().value ).shouldSucceed() - attributeInstanceService.search(temporalEntitiesQuery, jsonTemporalEntityAttribute) + attributeInstanceService.search(temporalEntitiesQuery, jsonAttribute) .shouldSucceedWith { (it as List).single { result -> result.time == ZonedDateTime.parse("2023-03-13T12:33:06Z") && @@ -750,7 +756,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer @Test fun `it should modify attribute instance for a LanguageProperty property`() = runTest { - val attributeInstance = gimmeLanguagePropertyAttributeInstance(languageTemporalEntityAttribute.id) + val attributeInstance = gimmeLanguagePropertyAttributeInstance(languageAttribute.id) attributeInstanceService.create(attributeInstance) val instanceTemporalFragment = @@ -776,7 +782,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer jsonLdAttribute.entries.first().value ).shouldSucceed() - attributeInstanceService.search(temporalEntitiesQuery, languageTemporalEntityAttribute) + attributeInstanceService.search(temporalEntitiesQuery, languageAttribute) .shouldSucceedWith { (it as List).single { result -> val deserializedPayload = result.payload.deserializeAsMap() @@ -794,7 +800,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer @Test fun `it should modify attribute instance for a VocabProperty property`() = runTest { - val attributeInstance = gimmeVocabPropertyAttributeInstance(vocabTemporalEntityAttribute.id) + val attributeInstance = gimmeVocabPropertyAttributeInstance(vocabAttribute.id) attributeInstanceService.create(attributeInstance) val instanceTemporalFragment = @@ -820,7 +826,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer jsonLdAttribute.entries.first().value ).shouldSucceed() - attributeInstanceService.search(temporalEntitiesQuery, vocabTemporalEntityAttribute) + attributeInstanceService.search(temporalEntitiesQuery, vocabAttribute) .shouldSucceedWith { (it as List).single { result -> val deserializedPayload = result.payload.deserializeAsMap() @@ -839,18 +845,18 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer @Test fun `it should delete attribute instance`() = runTest { - val attributeInstance = gimmeNumericPropertyAttributeInstance(incomingTemporalEntityAttribute.id) + val attributeInstance = gimmeNumericPropertyAttributeInstance(incomingAttribute.id) attributeInstanceService.create(attributeInstance).shouldSucceed() attributeInstanceService.deleteInstance( - incomingTemporalEntityAttribute.entityId, - incomingTemporalEntityAttribute.attributeName, + incomingAttribute.entityId, + incomingAttribute.attributeName, attributeInstance.instanceId ).shouldSucceed() attributeInstanceService.search( gimmeTemporalEntitiesQuery(buildDefaultTestTemporalQuery()), - incomingTemporalEntityAttribute + incomingAttribute ) .shouldSucceedWith { assertThat(it) @@ -860,18 +866,18 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer @Test fun `it should not delete attribute instance if attribute name is not found`() = runTest { - val attributeInstance = gimmeNumericPropertyAttributeInstance(incomingTemporalEntityAttribute.id) + val attributeInstance = gimmeNumericPropertyAttributeInstance(incomingAttribute.id) attributeInstanceService.create(attributeInstance).shouldSucceed() attributeInstanceService.deleteInstance( - incomingTemporalEntityAttribute.entityId, - outgoingTemporalEntityAttribute.attributeName, + incomingAttribute.entityId, + outgoingAttribute.attributeName, attributeInstance.instanceId ).shouldFail { assertInstanceOf(ResourceNotFoundException::class.java, it) assertEquals( attributeOrInstanceNotFoundMessage( - outgoingTemporalEntityAttribute.attributeName, + outgoingAttribute.attributeName, attributeInstance.instanceId.toString() ), it.message @@ -881,19 +887,19 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer @Test fun `it should not delete attribute instance if instanceID is not found`() = runTest { - val attributeInstance = gimmeNumericPropertyAttributeInstance(incomingTemporalEntityAttribute.id) + val attributeInstance = gimmeNumericPropertyAttributeInstance(incomingAttribute.id) val instanceId = "urn:ngsi-ld:Instance:notFound".toUri() attributeInstanceService.create(attributeInstance).shouldSucceed() attributeInstanceService.deleteInstance( - incomingTemporalEntityAttribute.entityId, - incomingTemporalEntityAttribute.attributeName, + incomingAttribute.entityId, + incomingAttribute.attributeName, instanceId ).shouldFail { assertInstanceOf(ResourceNotFoundException::class.java, it) assertEquals( attributeOrInstanceNotFoundMessage( - incomingTemporalEntityAttribute.attributeName, + incomingAttribute.attributeName, instanceId.toString() ), it.message @@ -906,18 +912,18 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer (1..10).forEachIndexed { index, _ -> if (index % 2 == 0) attributeInstanceService.create( - gimmeNumericPropertyAttributeInstance(incomingTemporalEntityAttribute.id) + gimmeNumericPropertyAttributeInstance(incomingAttribute.id) ) else attributeInstanceService.create( gimmeNumericPropertyAttributeInstance( - teaUuid = incomingTemporalEntityAttribute.id, + attributeUuid = incomingAttribute.id, timeProperty = AttributeInstance.TemporalProperty.CREATED_AT ) ) } - attributeInstanceService.deleteInstancesOfEntity(listOf(incomingTemporalEntityAttribute.id)).shouldSucceed() + attributeInstanceService.deleteInstancesOfEntity(listOf(incomingAttribute.id)).shouldSucceed() val temporalEntitiesQuery = gimmeTemporalEntitiesQuery( buildDefaultTestTemporalQuery( @@ -925,7 +931,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer timeAt = now.minusHours(1) ) ) - attributeInstanceService.search(temporalEntitiesQuery, incomingTemporalEntityAttribute) + attributeInstanceService.search(temporalEntitiesQuery, incomingAttribute) .shouldSucceedWith { assertThat(it).isEmpty() } val temporalEntitiesAuditQuery = gimmeTemporalEntitiesQuery( @@ -936,7 +942,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer ) ) - attributeInstanceService.search(temporalEntitiesAuditQuery, incomingTemporalEntityAttribute) + attributeInstanceService.search(temporalEntitiesAuditQuery, incomingAttribute) .shouldSucceedWith { assertThat(it).isEmpty() } } @@ -945,11 +951,11 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer (1..10).forEachIndexed { index, _ -> if (index % 2 == 0) attributeInstanceService.create( - gimmeNumericPropertyAttributeInstance(teaUuid = incomingTemporalEntityAttribute.id) + gimmeNumericPropertyAttributeInstance(attributeUuid = incomingAttribute.id) ) else attributeInstanceService.create( - gimmeNumericPropertyAttributeInstance(teaUuid = outgoingTemporalEntityAttribute.id) + gimmeNumericPropertyAttributeInstance(attributeUuid = outgoingAttribute.id) ) } @@ -961,9 +967,9 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer timeAt = now.minusHours(1) ) ) - attributeInstanceService.search(temporalEntitiesQuery, outgoingTemporalEntityAttribute) + attributeInstanceService.search(temporalEntitiesQuery, outgoingAttribute) .shouldSucceedWith { assertThat(it).hasSize(5) } - attributeInstanceService.search(temporalEntitiesQuery, incomingTemporalEntityAttribute) + attributeInstanceService.search(temporalEntitiesQuery, incomingAttribute) .shouldSucceedWith { assertThat(it).isEmpty() } } @@ -972,11 +978,11 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer (1..10).forEachIndexed { index, _ -> if (index % 2 == 0) attributeInstanceService.create( - gimmeNumericPropertyAttributeInstance(teaUuid = incomingTemporalEntityAttribute.id) + gimmeNumericPropertyAttributeInstance(attributeUuid = incomingAttribute.id) ) else attributeInstanceService.create( - gimmeNumericPropertyAttributeInstance(teaUuid = outgoingTemporalEntityAttribute.id) + gimmeNumericPropertyAttributeInstance(attributeUuid = outgoingAttribute.id) ) } @@ -988,9 +994,9 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer timeAt = now.minusHours(1) ) ) - attributeInstanceService.search(temporalEntitiesQuery, outgoingTemporalEntityAttribute) + attributeInstanceService.search(temporalEntitiesQuery, outgoingAttribute) .shouldSucceedWith { assertThat(it).hasSize(5) } - attributeInstanceService.search(temporalEntitiesQuery, incomingTemporalEntityAttribute) + attributeInstanceService.search(temporalEntitiesQuery, incomingAttribute) .shouldSucceedWith { assertThat(it).isEmpty() } } } diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalPaginationServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/TemporalPaginationServiceTests.kt similarity index 75% rename from search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalPaginationServiceTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/TemporalPaginationServiceTests.kt index 763f14faf..b53affd98 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalPaginationServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/TemporalPaginationServiceTests.kt @@ -1,10 +1,13 @@ -package com.egm.stellio.search.service +package com.egm.stellio.search.temporal.service -import com.egm.stellio.search.config.SearchProperties -import com.egm.stellio.search.model.* -import com.egm.stellio.search.service.TemporalPaginationService.getRangeAndPaginatedTEA +import com.egm.stellio.search.common.config.SearchProperties +import com.egm.stellio.search.entity.model.Attribute +import com.egm.stellio.search.entity.model.EntitiesQuery import com.egm.stellio.search.support.EMPTY_JSON_PAYLOAD import com.egm.stellio.search.support.buildDefaultTestTemporalQuery +import com.egm.stellio.search.temporal.model.* +import com.egm.stellio.search.temporal.service.TemporalPaginationService.getPaginatedAttributeWithInstancesAndRange +import com.egm.stellio.search.temporal.util.AttributesWithInstances import com.egm.stellio.shared.model.PaginationQuery import com.egm.stellio.shared.util.APIC_COMPOUND_CONTEXTS import com.egm.stellio.shared.util.INCOMING_PROPERTY @@ -32,35 +35,35 @@ class TemporalPaginationServiceTests { private val mostRecentTimestamp = leastRecentTimestamp.plusMinutes(4) // from discrimination attribute private val entityUri = "urn:ngsi-ld:BeeHive:TESTC".toUri() - private val teaIncoming = TemporalEntityAttribute( + private val attributeIncoming = Attribute( entityId = entityUri, attributeName = INCOMING_PROPERTY, - attributeValueType = TemporalEntityAttribute.AttributeValueType.NUMBER, + attributeValueType = Attribute.AttributeValueType.NUMBER, createdAt = now(), payload = EMPTY_JSON_PAYLOAD ) - private val teaOutgoing = TemporalEntityAttribute( + private val attributeOutgoing = Attribute( entityId = entityUri, attributeName = OUTGOING_PROPERTY, - attributeValueType = TemporalEntityAttribute.AttributeValueType.NUMBER, + attributeValueType = Attribute.AttributeValueType.NUMBER, createdAt = now(), payload = EMPTY_JSON_PAYLOAD ) private fun getInstance(time: ZonedDateTime): AttributeInstanceResult { - return SimplifiedAttributeInstanceResult(value = 1, time = time, temporalEntityAttribute = UUID.randomUUID()) + return SimplifiedAttributeInstanceResult(value = 1, time = time, attributeUuid = UUID.randomUUID()) } - private val teaWithInstances: TEAWithInstances = mapOf( - teaIncoming to listOf( + private val attributesWithInstances: AttributesWithInstances = mapOf( + attributeIncoming to listOf( getInstance(leastRecentTimestamp), getInstance(leastRecentTimestamp.plusMinutes(1)), getInstance(leastRecentTimestamp.plusMinutes(2)), getInstance(leastRecentTimestamp.plusMinutes(3)), getInstance(leastRecentTimestamp.plusMinutes(4)), ), - teaOutgoing to listOf( + attributeOutgoing to listOf( getInstance(leastRecentTimestamp.plusMinutes(3)), getInstance(leastRecentTimestamp.plusMinutes(4)), getInstance(leastRecentTimestamp.plusMinutes(5)), @@ -69,15 +72,15 @@ class TemporalPaginationServiceTests { ) ) - private val teaWithInstancesForLastN: TEAWithInstances = mapOf( - teaIncoming to listOf( + private val attributesWithInstancesForLastN: AttributesWithInstances = mapOf( + attributeIncoming to listOf( getInstance(leastRecentTimestamp), getInstance(leastRecentTimestamp.plusMinutes(1)), getInstance(leastRecentTimestamp.plusMinutes(2)), getInstance(leastRecentTimestamp.plusMinutes(3)), getInstance(leastRecentTimestamp.plusMinutes(4)), ), - teaOutgoing to listOf( + attributeOutgoing to listOf( getInstance(leastRecentTimestamp.minusMinutes(3)), getInstance(leastRecentTimestamp.minusMinutes(2)), getInstance(leastRecentTimestamp.minusMinutes(1)), @@ -88,7 +91,7 @@ class TemporalPaginationServiceTests { private val aggregationInstances = listOf( AggregatedAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), values = listOf( AggregatedAttributeInstanceResult.AggregateResult( TemporalQuery.Aggregate.SUM, @@ -105,7 +108,7 @@ class TemporalPaginationServiceTests { ) ), AggregatedAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), values = listOf( AggregatedAttributeInstanceResult.AggregateResult( TemporalQuery.Aggregate.SUM, @@ -145,11 +148,11 @@ class TemporalPaginationServiceTests { endTimeAt = endTimeAt ) ) - val (newTeas, range) = getRangeAndPaginatedTEA(teaWithInstances, query) + val (newTeas, range) = getPaginatedAttributeWithInstancesAndRange(attributesWithInstances, query) assertNotNull(range) - assertEquals(5, newTeas[teaIncoming]?.size) - assertEquals(2, newTeas[teaOutgoing]?.size) + assertEquals(5, newTeas[attributeIncoming]?.size) + assertEquals(2, newTeas[attributeOutgoing]?.size) assertEquals(timeAt, range!!.first) assertEquals(mostRecentTimestamp, range.second) @@ -164,12 +167,12 @@ class TemporalPaginationServiceTests { timeAt = timeAt, ) ) - val (newTeas, range) = getRangeAndPaginatedTEA(teaWithInstances, query) + val (newTeas, range) = getPaginatedAttributeWithInstancesAndRange(attributesWithInstances, query) assertNotNull(range) - assertEquals(5, newTeas[teaIncoming]?.size) - assertEquals(2, newTeas[teaOutgoing]?.size) + assertEquals(5, newTeas[attributeIncoming]?.size) + assertEquals(2, newTeas[attributeOutgoing]?.size) assertEquals(timeAt, range!!.first) assertEquals(mostRecentTimestamp, range.second) @@ -184,12 +187,12 @@ class TemporalPaginationServiceTests { timeAt = endTimeAt, ) ) - val (newTeas, range) = getRangeAndPaginatedTEA(teaWithInstances, query) + val (newTeas, range) = getPaginatedAttributeWithInstancesAndRange(attributesWithInstances, query) assertNotNull(range) - assertEquals(5, newTeas[teaIncoming]?.size) - assertEquals(2, newTeas[teaOutgoing]?.size) + assertEquals(5, newTeas[attributeIncoming]?.size) + assertEquals(2, newTeas[attributeOutgoing]?.size) assertEquals(leastRecentTimestamp, range!!.first) assertEquals(mostRecentTimestamp, range.second) @@ -206,12 +209,12 @@ class TemporalPaginationServiceTests { lastN = 100 ) ) - val (newTeas, range) = getRangeAndPaginatedTEA(teaWithInstancesForLastN, query) + val (newTeas, range) = getPaginatedAttributeWithInstancesAndRange(attributesWithInstancesForLastN, query) assertNotNull(range) - assertEquals(5, newTeas[teaIncoming]?.size) - assertEquals(2, newTeas[teaOutgoing]?.size) + assertEquals(5, newTeas[attributeIncoming]?.size) + assertEquals(2, newTeas[attributeOutgoing]?.size) assertEquals(endTimeAt, range!!.first) assertEquals(leastRecentTimestamp, range.second) @@ -228,12 +231,12 @@ class TemporalPaginationServiceTests { ) ) - val (newTeas, range) = getRangeAndPaginatedTEA(teaWithInstancesForLastN, query) + val (newTeas, range) = getPaginatedAttributeWithInstancesAndRange(attributesWithInstancesForLastN, query) assertNotNull(range) - assertEquals(5, newTeas[teaIncoming]?.size) - assertEquals(2, newTeas[teaOutgoing]?.size) + assertEquals(5, newTeas[attributeIncoming]?.size) + assertEquals(2, newTeas[attributeOutgoing]?.size) assertEquals(mostRecentTimestamp, range!!.first) assertEquals(leastRecentTimestamp, range.second) @@ -249,12 +252,12 @@ class TemporalPaginationServiceTests { lastN = 100 ) ) - val (newTeas, range) = getRangeAndPaginatedTEA(teaWithInstancesForLastN, query) + val (newAttributes, range) = getPaginatedAttributeWithInstancesAndRange(attributesWithInstancesForLastN, query) assertNotNull(range) - assertEquals(5, newTeas[teaIncoming]?.size) - assertEquals(2, newTeas[teaOutgoing]?.size) + assertEquals(5, newAttributes[attributeIncoming]?.size) + assertEquals(2, newAttributes[attributeOutgoing]?.size) assertEquals(endTimeAt, range!!.first) assertEquals(leastRecentTimestamp, range.second) @@ -280,14 +283,14 @@ class TemporalPaginationServiceTests { withAggregatedValues = true ) - val teaWithInstances: TEAWithInstances = - mapOf(teaIncoming to aggregationInstances, teaOutgoing to aggregationInstances) - val (newTeas, range) = getRangeAndPaginatedTEA(teaWithInstances, query) + val attributesWithInstances: AttributesWithInstances = + mapOf(attributeIncoming to aggregationInstances, attributeOutgoing to aggregationInstances) + val (newAttributes, range) = getPaginatedAttributeWithInstancesAndRange(attributesWithInstances, query) assertNotNull(range) - assertEquals(2, newTeas[teaIncoming]?.size) - assertEquals(2, newTeas[teaOutgoing]?.size) + assertEquals(2, newAttributes[attributeIncoming]?.size) + assertEquals(2, newAttributes[attributeOutgoing]?.size) assertEquals(leastRecentTimestamp.plusMinutes(1).plusSeconds(59), range!!.second) assertEquals(timeAt, range.first) diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/QueryServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/TemporalQueryServiceTests.kt similarity index 65% rename from search-service/src/test/kotlin/com/egm/stellio/search/service/QueryServiceTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/TemporalQueryServiceTests.kt index fd86af44a..ce03e3e01 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/QueryServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/TemporalQueryServiceTests.kt @@ -1,11 +1,17 @@ -package com.egm.stellio.search.service +package com.egm.stellio.search.temporal.service +import arrow.core.None import arrow.core.left import arrow.core.right -import com.egm.stellio.search.model.* +import com.egm.stellio.search.authorization.service.AuthorizationService +import com.egm.stellio.search.entity.model.Attribute +import com.egm.stellio.search.entity.model.EntitiesQuery +import com.egm.stellio.search.entity.service.EntityAttributeService +import com.egm.stellio.search.entity.service.EntityQueryService import com.egm.stellio.search.scope.ScopeInstanceResult import com.egm.stellio.search.scope.ScopeService import com.egm.stellio.search.support.* +import com.egm.stellio.search.temporal.model.* import com.egm.stellio.shared.model.PaginationQuery import com.egm.stellio.shared.model.ResourceNotFoundException import com.egm.stellio.shared.util.* @@ -25,16 +31,16 @@ import org.springframework.test.context.ActiveProfiles import java.net.URI import java.time.ZonedDateTime -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = [QueryService::class]) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = [TemporalQueryService::class]) @ActiveProfiles("test") @ExperimentalCoroutinesApi -class QueryServiceTests { +class TemporalQueryServiceTests { @Autowired - private lateinit var queryService: QueryService + private lateinit var temporalQueryService: TemporalQueryService @MockkBean - private lateinit var entityPayloadService: EntityPayloadService + private lateinit var entityQueryService: EntityQueryService @MockkBean private lateinit var scopeService: ScopeService @@ -43,88 +49,33 @@ class QueryServiceTests { private lateinit var attributeInstanceService: AttributeInstanceService @MockkBean - private lateinit var temporalEntityAttributeService: TemporalEntityAttributeService + private lateinit var entityAttributeService: EntityAttributeService + + @MockkBean + private lateinit var authorizationService: AuthorizationService private val now = ngsiLdDateTime() private val entityUri = "urn:ngsi-ld:BeeHive:TESTC".toUri() - @Test - fun `it should return a JSON-LD entity when querying by id`() = runTest { - coEvery { entityPayloadService.retrieve(any()) } returns gimmeEntityPayload().right() - - queryService.queryEntity(entityUri) - .shouldSucceedWith { - assertEquals(entityUri.toString(), it.id) - assertEquals(listOf(BEEHIVE_TYPE), it.types) - assertEquals(7, it.members.size) - } - } - - @Test - fun `it should return an API exception if no entity exists with the given id`() = runTest { - coEvery { entityPayloadService.retrieve(any()) } returns ResourceNotFoundException("").left() - - queryService.queryEntity(entityUri) - .shouldFail { - assertTrue(it is ResourceNotFoundException) - } - } - - @Test - fun `it should return a list of JSON-LD entities when querying entities`() = runTest { - coEvery { entityPayloadService.queryEntities(any(), any()) } returns listOf(entityUri) - coEvery { entityPayloadService.queryEntitiesCount(any(), any()) } returns 1.right() - coEvery { entityPayloadService.retrieve(any>()) } returns listOf(gimmeEntityPayload()) - - queryService.queryEntities(buildDefaultQueryParams()) { null } - .shouldSucceedWith { - assertEquals(1, it.second) - assertEquals(entityUri.toString(), it.first[0].id) - assertEquals(listOf(BEEHIVE_TYPE), it.first[0].types) - assertEquals(7, it.first[0].members.size) - } - } - - @Test - fun `it should return an empty list if no entity matched the query`() = runTest { - coEvery { entityPayloadService.queryEntities(any(), any()) } returns emptyList() - coEvery { entityPayloadService.queryEntitiesCount(any(), any()) } returns 0.right() - - queryService.queryEntities(buildDefaultQueryParams()) { null } - .shouldSucceedWith { - assertEquals(0, it.second) - assertTrue(it.first.isEmpty()) - } - } - @Test fun `it should return an API exception if the entity does not exist`() = runTest { - coEvery { temporalEntityAttributeService.getForEntity(any(), any(), any()) } returns emptyList() + coEvery { + entityQueryService.checkEntityExistence(any()) + } returns ResourceNotFoundException(entityNotFoundMessage(entityUri.toString())).left() - queryService.queryTemporalEntity( + temporalQueryService.queryTemporalEntity( entityUri, TemporalEntitiesQuery( - temporalQuery = buildDefaultTestTemporalQuery( - timerel = TemporalQuery.Timerel.AFTER, - timeAt = ZonedDateTime.parse("2019-10-17T07:31:39Z") - ), - entitiesQuery = EntitiesQuery( - paginationQuery = PaginationQuery(limit = 0, offset = 50), - attrs = setOf(INCOMING_PROPERTY, OUTGOING_PROPERTY), - contexts = APIC_COMPOUND_CONTEXTS - ), - withTemporalValues = false, + entitiesQuery = buildDefaultQueryParams(), + temporalQuery = buildDefaultTestTemporalQuery(), + withTemporalValues = true, withAudit = false, withAggregatedValues = false ) ).fold({ assertInstanceOf(ResourceNotFoundException::class.java, it) - assertEquals( - "Entity $entityUri does not exist or it has none of the requested attributes : " + - "[$INCOMING_PROPERTY, $OUTGOING_PROPERTY]", - it.message - ) + assertEquals("Entity $entityUri was not found", it.message) }, { fail("it should have returned an API exception if the entity does not exist") }) @@ -132,29 +83,31 @@ class QueryServiceTests { @Test fun `it should query a temporal entity as requested by query params`() = runTest { - val teas = + val attributes = listOf(INCOMING_PROPERTY, OUTGOING_PROPERTY).map { - TemporalEntityAttribute( + Attribute( entityId = entityUri, attributeName = it, - attributeValueType = TemporalEntityAttribute.AttributeValueType.NUMBER, + attributeValueType = Attribute.AttributeValueType.NUMBER, createdAt = now, payload = EMPTY_JSON_PAYLOAD ) } - coEvery { temporalEntityAttributeService.getForEntity(any(), any(), any()) } returns teas - coEvery { entityPayloadService.retrieve(any()) } returns gimmeEntityPayload().right() + coEvery { entityQueryService.checkEntityExistence(any()) } returns Unit.right() + coEvery { authorizationService.userCanReadEntity(any(), any()) } returns Unit.right() + coEvery { entityAttributeService.getForEntity(any(), any(), any()) } returns attributes + coEvery { entityQueryService.retrieve(any()) } returns gimmeEntityPayload().right() coEvery { scopeService.retrieveHistory(any(), any()) } returns emptyList().right() coEvery { - attributeInstanceService.search(any(), any>()) + attributeInstanceService.search(any(), any>()) } returns listOf( - FullAttributeInstanceResult(teas[0].id, EMPTY_PAYLOAD, now, NGSILD_CREATED_AT_TERM, null), - FullAttributeInstanceResult(teas[1].id, EMPTY_PAYLOAD, now, NGSILD_CREATED_AT_TERM, null) + FullAttributeInstanceResult(attributes[0].id, EMPTY_PAYLOAD, now, NGSILD_CREATED_AT_TERM, null), + FullAttributeInstanceResult(attributes[1].id, EMPTY_PAYLOAD, now, NGSILD_CREATED_AT_TERM, null) ).right() - queryService.queryTemporalEntity( + temporalQueryService.queryTemporalEntity( entityUri, TemporalEntitiesQuery( temporalQuery = buildDefaultTestTemporalQuery( @@ -172,7 +125,9 @@ class QueryServiceTests { ) coVerify { - temporalEntityAttributeService.getForEntity(entityUri, emptySet(), emptySet()) + entityQueryService.checkEntityExistence(entityUri) + authorizationService.userCanReadEntity(entityUri, None) + entityAttributeService.getForEntity(entityUri, emptySet(), emptySet()) attributeInstanceService.search( match { temporalEntitiesQuery -> temporalEntitiesQuery.temporalQuery.timerel == TemporalQuery.Timerel.AFTER && @@ -180,7 +135,7 @@ class QueryServiceTests { ZonedDateTime.parse("2019-10-17T07:31:39Z") ) }, - any>() + any>() ) scopeService.retrieveHistory(listOf(entityUri), any()) } @@ -188,7 +143,7 @@ class QueryServiceTests { @Test fun `it should not return an oldest timestamp if not in an aggregattion query`() = runTest { - val origin = queryService.calculateOldestTimestamp( + val origin = temporalQueryService.calculateOldestTimestamp( entityUri, TemporalEntitiesQuery( temporalQuery = buildDefaultTestTemporalQuery(), @@ -208,7 +163,7 @@ class QueryServiceTests { @Test fun `it should return timeAt as the oldest timestamp if it is provided in the temporal query`() = runTest { - val origin = queryService.calculateOldestTimestamp( + val origin = temporalQueryService.calculateOldestTimestamp( entityUri, TemporalEntitiesQuery( temporalQuery = buildDefaultTestTemporalQuery( @@ -239,7 +194,7 @@ class QueryServiceTests { scopeService.selectOldestDate(any(), any()) } returns ZonedDateTime.parse("2023-09-03T14:22:55Z") - val origin = queryService.calculateOldestTimestamp( + val origin = temporalQueryService.calculateOldestTimestamp( entityUri, TemporalEntitiesQuery( temporalQuery = buildDefaultTestTemporalQuery(), @@ -260,33 +215,34 @@ class QueryServiceTests { @Test fun `it should query temporal entities as requested by query params`() = runTest { - val temporalEntityAttribute = TemporalEntityAttribute( + val attribute = Attribute( entityId = entityUri, attributeName = INCOMING_PROPERTY, - attributeValueType = TemporalEntityAttribute.AttributeValueType.NUMBER, + attributeValueType = Attribute.AttributeValueType.NUMBER, createdAt = now, payload = EMPTY_JSON_PAYLOAD ) - coEvery { entityPayloadService.queryEntities(any(), any()) } returns listOf(entityUri) + coEvery { authorizationService.computeAccessRightFilter(any()) } returns { null } + coEvery { entityQueryService.queryEntities(any(), any<() -> String?>()) } returns listOf(entityUri) coEvery { - temporalEntityAttributeService.getForTemporalEntities(any(), any()) - } returns listOf(temporalEntityAttribute) - coEvery { entityPayloadService.queryEntitiesCount(any(), any()) } returns 1.right() + entityAttributeService.getForEntities(any(), any()) + } returns listOf(attribute) + coEvery { entityQueryService.queryEntitiesCount(any(), any()) } returns 1.right() coEvery { scopeService.retrieveHistory(any(), any()) } returns emptyList().right() - coEvery { entityPayloadService.retrieve(any()) } returns gimmeEntityPayload().right() + coEvery { entityQueryService.retrieve(any()) } returns gimmeEntityPayload().right() coEvery { - attributeInstanceService.search(any(), any>()) + attributeInstanceService.search(any(), any>()) } returns listOf( SimplifiedAttributeInstanceResult( - temporalEntityAttribute = temporalEntityAttribute.id, + attributeUuid = attribute.id, value = 2.0, time = ngsiLdDateTime() ) ).right() - queryService.queryTemporalEntities( + temporalQueryService.queryTemporalEntities( TemporalEntitiesQuery( EntitiesQuery( typeSelection = "$BEEHIVE_TYPE,$APIARY_TYPE", @@ -301,10 +257,10 @@ class QueryServiceTests { withAudit = false, withAggregatedValues = false ) - ) { null } + ) coVerify { - temporalEntityAttributeService.getForTemporalEntities( + entityAttributeService.getForEntities( listOf(entityUri), EntitiesQuery( typeSelection = "$BEEHIVE_TYPE,$APIARY_TYPE", @@ -319,9 +275,9 @@ class QueryServiceTests { ZonedDateTime.parse("2019-10-17T07:31:39Z") ) }, - any>() + any>() ) - entityPayloadService.queryEntitiesCount( + entityQueryService.queryEntitiesCount( EntitiesQuery( typeSelection = "$BEEHIVE_TYPE,$APIARY_TYPE", paginationQuery = PaginationQuery(limit = 2, offset = 2), @@ -335,26 +291,27 @@ class QueryServiceTests { @Test fun `it should return an empty list for an attribute if it has no temporal values`() = runTest { - val temporalEntityAttribute = TemporalEntityAttribute( + val attribute = Attribute( entityId = entityUri, attributeName = INCOMING_PROPERTY, - attributeValueType = TemporalEntityAttribute.AttributeValueType.NUMBER, + attributeValueType = Attribute.AttributeValueType.NUMBER, createdAt = now, payload = EMPTY_JSON_PAYLOAD ) - coEvery { entityPayloadService.queryEntities(any(), any()) } returns listOf(entityUri) + coEvery { authorizationService.computeAccessRightFilter(any()) } returns { null } + coEvery { entityQueryService.queryEntities(any(), any<() -> String?>()) } returns listOf(entityUri) coEvery { - temporalEntityAttributeService.getForTemporalEntities(any(), any()) - } returns listOf(temporalEntityAttribute) + entityAttributeService.getForEntities(any(), any()) + } returns listOf(attribute) coEvery { scopeService.retrieveHistory(any(), any()) } returns emptyList().right() coEvery { - attributeInstanceService.search(any(), any>()) + attributeInstanceService.search(any(), any>()) } returns emptyList().right() - coEvery { entityPayloadService.retrieve(any()) } returns gimmeEntityPayload().right() - coEvery { entityPayloadService.queryEntitiesCount(any(), any()) } returns 1.right() + coEvery { entityQueryService.retrieve(any()) } returns gimmeEntityPayload().right() + coEvery { entityQueryService.queryEntitiesCount(any(), any()) } returns 1.right() - queryService.queryTemporalEntities( + temporalQueryService.queryTemporalEntities( TemporalEntitiesQuery( EntitiesQuery( typeSelection = "$BEEHIVE_TYPE,$APIARY_TYPE", @@ -370,7 +327,7 @@ class QueryServiceTests { withAudit = false, withAggregatedValues = true ) - ) { null } + ) .fold({ fail("it should have returned an empty list") }, { diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/util/TemporalEntitiesParameterizedSource.kt b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/util/TemporalEntitiesParameterizedSource.kt similarity index 80% rename from search-service/src/test/kotlin/com/egm/stellio/search/util/TemporalEntitiesParameterizedSource.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/temporal/util/TemporalEntitiesParameterizedSource.kt index 8dca9b1bc..2c74de4b7 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/util/TemporalEntitiesParameterizedSource.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/util/TemporalEntitiesParameterizedSource.kt @@ -1,8 +1,12 @@ -package com.egm.stellio.search.util +package com.egm.stellio.search.temporal.util -import com.egm.stellio.search.model.* +import com.egm.stellio.search.entity.model.Attribute +import com.egm.stellio.search.entity.model.Entity import com.egm.stellio.search.support.EMPTY_JSON_PAYLOAD import com.egm.stellio.search.support.buildAttributeInstancePayload +import com.egm.stellio.search.temporal.model.EntityTemporalResult +import com.egm.stellio.search.temporal.model.FullAttributeInstanceResult +import com.egm.stellio.search.temporal.model.SimplifiedAttributeInstanceResult import com.egm.stellio.shared.util.BEEHIVE_TYPE import com.egm.stellio.shared.util.JsonLdUtils import com.egm.stellio.shared.util.loadSampleData @@ -23,7 +27,7 @@ class TemporalEntitiesParameterizedSource { private val simplifiedResultOfTwoEntitiesWithOneProperty = listOf( EntityTemporalResult( - EntityPayload( + Entity( entityId = "urn:ngsi-ld:BeeHive:TESTC".toUri(), types = listOf(BEEHIVE_TYPE), createdAt = now, @@ -31,15 +35,15 @@ class TemporalEntitiesParameterizedSource { ), emptyList(), mapOf( - TemporalEntityAttribute( + Attribute( entityId = "urn:ngsi-ld:BeeHive:TESTC".toUri(), attributeName = "https://ontology.eglobalmark.com/apic#incoming", - attributeValueType = TemporalEntityAttribute.AttributeValueType.NUMBER, + attributeValueType = Attribute.AttributeValueType.NUMBER, createdAt = now, payload = EMPTY_JSON_PAYLOAD ) to listOf( SimplifiedAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), value = 20, time = ZonedDateTime.parse("2020-03-25T08:33:17.965206Z") ) @@ -47,7 +51,7 @@ class TemporalEntitiesParameterizedSource { ) ), EntityTemporalResult( - EntityPayload( + Entity( entityId = "urn:ngsi-ld:BeeHive:TESTD".toUri(), types = listOf(BEEHIVE_TYPE), createdAt = now, @@ -55,15 +59,15 @@ class TemporalEntitiesParameterizedSource { ), emptyList(), mapOf( - TemporalEntityAttribute( + Attribute( entityId = "urn:ngsi-ld:BeeHive:TESTD".toUri(), attributeName = "https://ontology.eglobalmark.com/apic#outgoing", - attributeValueType = TemporalEntityAttribute.AttributeValueType.NUMBER, + attributeValueType = Attribute.AttributeValueType.NUMBER, createdAt = now, payload = EMPTY_JSON_PAYLOAD ) to listOf( SimplifiedAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), value = 25, time = ZonedDateTime.parse("2020-03-25T08:33:17.965206Z") ) @@ -75,7 +79,7 @@ class TemporalEntitiesParameterizedSource { private val resultOfTwoEntitiesWithOneProperty = listOf( EntityTemporalResult( - EntityPayload( + Entity( entityId = "urn:ngsi-ld:BeeHive:TESTC".toUri(), types = listOf(BEEHIVE_TYPE), createdAt = now, @@ -83,15 +87,15 @@ class TemporalEntitiesParameterizedSource { ), emptyList(), mapOf( - TemporalEntityAttribute( + Attribute( entityId = "urn:ngsi-ld:BeeHive:TESTC".toUri(), attributeName = "https://ontology.eglobalmark.com/apic#incoming", - attributeValueType = TemporalEntityAttribute.AttributeValueType.NUMBER, + attributeValueType = Attribute.AttributeValueType.NUMBER, createdAt = now, payload = EMPTY_JSON_PAYLOAD ) to listOf( FullAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), payload = buildAttributeInstancePayload( 20, ZonedDateTime.parse("2020-03-25T08:33:17.965206Z"), @@ -106,7 +110,7 @@ class TemporalEntitiesParameterizedSource { ) ), EntityTemporalResult( - EntityPayload( + Entity( entityId = "urn:ngsi-ld:BeeHive:TESTD".toUri(), types = listOf(BEEHIVE_TYPE), createdAt = now, @@ -114,15 +118,15 @@ class TemporalEntitiesParameterizedSource { ), emptyList(), mapOf( - TemporalEntityAttribute( + Attribute( entityId = "urn:ngsi-ld:BeeHive:TESTD".toUri(), attributeName = "https://ontology.eglobalmark.com/apic#outgoing", - attributeValueType = TemporalEntityAttribute.AttributeValueType.NUMBER, + attributeValueType = Attribute.AttributeValueType.NUMBER, createdAt = now, payload = EMPTY_JSON_PAYLOAD ) to listOf( FullAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), payload = buildAttributeInstancePayload( 25, ZonedDateTime.parse("2020-03-25T08:33:17.965206Z"), @@ -141,7 +145,7 @@ class TemporalEntitiesParameterizedSource { private val simplifiedResultOfTwoEntitiesWithOnePropertyAndOneRelationship = listOf( EntityTemporalResult( - EntityPayload( + Entity( entityId = "urn:ngsi-ld:BeeHive:TESTC".toUri(), types = listOf(BEEHIVE_TYPE), createdAt = now, @@ -149,29 +153,29 @@ class TemporalEntitiesParameterizedSource { ), emptyList(), mapOf( - TemporalEntityAttribute( + Attribute( entityId = "urn:ngsi-ld:BeeHive:TESTC".toUri(), attributeName = "https://ontology.eglobalmark.com/apic#incoming", - attributeValueType = TemporalEntityAttribute.AttributeValueType.NUMBER, + attributeValueType = Attribute.AttributeValueType.NUMBER, createdAt = now, payload = EMPTY_JSON_PAYLOAD ) to listOf( SimplifiedAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), value = 20, time = ZonedDateTime.parse("2020-03-25T08:33:17.965206Z") ) ), - TemporalEntityAttribute( + Attribute( entityId = "urn:ngsi-ld:BeeHive:TESTC".toUri(), attributeName = "https://ontology.eglobalmark.com/egm#managedBy", - attributeType = TemporalEntityAttribute.AttributeType.Relationship, - attributeValueType = TemporalEntityAttribute.AttributeValueType.NUMBER, + attributeType = Attribute.AttributeType.Relationship, + attributeValueType = Attribute.AttributeValueType.NUMBER, createdAt = now, payload = EMPTY_JSON_PAYLOAD ) to listOf( SimplifiedAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), value = "urn:ngsi-ld:Beekeeper:1234", time = ZonedDateTime.parse("2020-03-25T08:33:17.965206Z") ) @@ -179,7 +183,7 @@ class TemporalEntitiesParameterizedSource { ) ), EntityTemporalResult( - EntityPayload( + Entity( entityId = "urn:ngsi-ld:BeeHive:TESTD".toUri(), types = listOf(BEEHIVE_TYPE), createdAt = now, @@ -187,29 +191,29 @@ class TemporalEntitiesParameterizedSource { ), emptyList(), mapOf( - TemporalEntityAttribute( + Attribute( entityId = "urn:ngsi-ld:BeeHive:TESTD".toUri(), attributeName = "https://ontology.eglobalmark.com/apic#outgoing", - attributeValueType = TemporalEntityAttribute.AttributeValueType.NUMBER, + attributeValueType = Attribute.AttributeValueType.NUMBER, createdAt = now, payload = EMPTY_JSON_PAYLOAD ) to listOf( SimplifiedAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), value = 25, time = ZonedDateTime.parse("2020-03-25T08:33:17.965206Z") ) ), - TemporalEntityAttribute( + Attribute( entityId = "urn:ngsi-ld:BeeHive:TESTD".toUri(), attributeName = "https://ontology.eglobalmark.com/egm#managedBy", - attributeType = TemporalEntityAttribute.AttributeType.Relationship, - attributeValueType = TemporalEntityAttribute.AttributeValueType.NUMBER, + attributeType = Attribute.AttributeType.Relationship, + attributeValueType = Attribute.AttributeValueType.NUMBER, createdAt = now, payload = EMPTY_JSON_PAYLOAD ) to listOf( SimplifiedAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), value = "urn:ngsi-ld:Beekeeper:5678", time = ZonedDateTime.parse("2020-03-25T08:33:17.965206Z") ) diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/util/TemporalEntityBuilderTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/util/TemporalEntityBuilderTests.kt similarity index 82% rename from search-service/src/test/kotlin/com/egm/stellio/search/util/TemporalEntityBuilderTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/temporal/util/TemporalEntityBuilderTests.kt index 4077a2a10..921738a43 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/util/TemporalEntityBuilderTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/util/TemporalEntityBuilderTests.kt @@ -1,11 +1,13 @@ -package com.egm.stellio.search.util +package com.egm.stellio.search.temporal.util -import com.egm.stellio.search.model.* -import com.egm.stellio.search.model.AggregatedAttributeInstanceResult.AggregateResult +import com.egm.stellio.search.entity.model.Attribute +import com.egm.stellio.search.entity.model.Entity import com.egm.stellio.search.scope.ScopeInstanceResult import com.egm.stellio.search.support.EMPTY_JSON_PAYLOAD import com.egm.stellio.search.support.buildDefaultQueryParams import com.egm.stellio.search.support.buildDefaultTestTemporalQuery +import com.egm.stellio.search.temporal.model.* +import com.egm.stellio.search.temporal.model.AggregatedAttributeInstanceResult.AggregateResult import com.egm.stellio.shared.util.* import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_CREATED_AT_PROPERTY import com.egm.stellio.shared.util.JsonUtils.serializeObject @@ -24,24 +26,24 @@ class TemporalEntityBuilderTests { @Test fun `it should return a temporal entity with an empty array of instances if it has no temporal history`() { - val temporalEntityAttribute = TemporalEntityAttribute( + val attribute = Attribute( entityId = "urn:ngsi-ld:Beehive:1234".toUri(), attributeName = OUTGOING_PROPERTY, - attributeValueType = TemporalEntityAttribute.AttributeValueType.STRING, + attributeValueType = Attribute.AttributeValueType.STRING, createdAt = now, payload = EMPTY_JSON_PAYLOAD ) val attributeAndResultsMap = mapOf( - temporalEntityAttribute to emptyList() + attribute to emptyList() ) - val entityPayload = EntityPayload( + val entity = Entity( entityId = "urn:ngsi-ld:Beehive:1234".toUri(), types = listOf(BEEHIVE_TYPE), createdAt = now, payload = EMPTY_JSON_PAYLOAD ) val temporalEntity = TemporalEntityBuilder.buildTemporalEntity( - EntityTemporalResult(entityPayload, emptyList(), attributeAndResultsMap), + EntityTemporalResult(entity, emptyList(), attributeAndResultsMap), TemporalEntitiesQuery( entitiesQuery = buildDefaultQueryParams(), temporalQuery = buildDefaultTestTemporalQuery(), @@ -58,15 +60,15 @@ class TemporalEntityBuilderTests { } @ParameterizedTest - @MethodSource("com.egm.stellio.search.util.TemporalEntityParameterizedSource#rawResultsProvider") + @MethodSource("com.egm.stellio.search.temporal.util.TemporalEntityParameterizedSource#rawResultsProvider") fun `it should correctly build a temporal entity`( scopeHistory: List, - attributeAndResultsMap: TemporalEntityAttributeInstancesResult, + attributeAndResultsMap: AttributesWithInstances, withTemporalValues: Boolean, withAudit: Boolean, expectation: String ) { - val entityPayload = EntityPayload( + val entity = Entity( entityId = "urn:ngsi-ld:BeeHive:TESTC".toUri(), types = listOf(BEEHIVE_TYPE), createdAt = now, @@ -74,7 +76,7 @@ class TemporalEntityBuilderTests { ) val temporalEntity = TemporalEntityBuilder.buildTemporalEntity( - EntityTemporalResult(entityPayload, scopeHistory, attributeAndResultsMap), + EntityTemporalResult(entity, scopeHistory, attributeAndResultsMap), TemporalEntitiesQuery( entitiesQuery = buildDefaultQueryParams(), temporalQuery = buildDefaultTestTemporalQuery(), @@ -91,7 +93,7 @@ class TemporalEntityBuilderTests { } @ParameterizedTest - @MethodSource("com.egm.stellio.search.util.TemporalEntitiesParameterizedSource#rawResultsProvider") + @MethodSource("com.egm.stellio.search.temporal.util.TemporalEntitiesParameterizedSource#rawResultsProvider") fun `it should correctly build temporal entities`( entityTemporalResults: List, withTemporalValues: Boolean, @@ -118,17 +120,17 @@ class TemporalEntityBuilderTests { @SuppressWarnings("LongMethod") @Test fun `it should return a temporal entity with values aggregated`() { - val temporalEntityAttribute = TemporalEntityAttribute( + val attribute = Attribute( entityId = "urn:ngsi-ld:Beehive:1234".toUri(), attributeName = OUTGOING_PROPERTY, - attributeValueType = TemporalEntityAttribute.AttributeValueType.NUMBER, + attributeValueType = Attribute.AttributeValueType.NUMBER, createdAt = now, payload = EMPTY_JSON_PAYLOAD ) val attributeAndResultsMap = mapOf( - temporalEntityAttribute to listOf( + attribute to listOf( AggregatedAttributeInstanceResult( - temporalEntityAttribute = temporalEntityAttribute.id, + attributeUuid = attribute.id, listOf( AggregateResult( TemporalQuery.Aggregate.SUM, @@ -145,7 +147,7 @@ class TemporalEntityBuilderTests { ) ), AggregatedAttributeInstanceResult( - temporalEntityAttribute = temporalEntityAttribute.id, + attributeUuid = attribute.id, listOf( AggregateResult( TemporalQuery.Aggregate.SUM, @@ -170,7 +172,7 @@ class TemporalEntityBuilderTests { "P1D", listOf(TemporalQuery.Aggregate.SUM, TemporalQuery.Aggregate.AVG) ) - val entityPayload = EntityPayload( + val entity = Entity( entityId = "urn:ngsi-ld:Beehive:1234".toUri(), types = listOf(BEEHIVE_TYPE), createdAt = now, @@ -178,7 +180,7 @@ class TemporalEntityBuilderTests { ) val temporalEntity = TemporalEntityBuilder.buildTemporalEntity( - EntityTemporalResult(entityPayload, emptyList(), attributeAndResultsMap), + EntityTemporalResult(entity, emptyList(), attributeAndResultsMap), TemporalEntitiesQuery( entitiesQuery = buildDefaultQueryParams(), temporalQuery = temporalQuery, diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/util/TemporalEntityParameterizedSource.kt b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/util/TemporalEntityParameterizedSource.kt similarity index 84% rename from search-service/src/test/kotlin/com/egm/stellio/search/util/TemporalEntityParameterizedSource.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/temporal/util/TemporalEntityParameterizedSource.kt index a6c2d87d0..b5d31e06a 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/util/TemporalEntityParameterizedSource.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/util/TemporalEntityParameterizedSource.kt @@ -1,10 +1,11 @@ -package com.egm.stellio.search.util +package com.egm.stellio.search.temporal.util -import com.egm.stellio.search.model.* +import com.egm.stellio.search.entity.model.Attribute import com.egm.stellio.search.scope.FullScopeInstanceResult import com.egm.stellio.search.scope.ScopeInstanceResult import com.egm.stellio.search.scope.SimplifiedScopeInstanceResult import com.egm.stellio.search.support.* +import com.egm.stellio.search.temporal.model.* import com.egm.stellio.shared.util.JsonLdUtils import com.egm.stellio.shared.util.loadSampleData import com.egm.stellio.shared.util.toUri @@ -27,16 +28,16 @@ class TemporalEntityParameterizedSource { Arguments.arguments( emptyList(), mapOf( - TemporalEntityAttribute( + Attribute( entityId = entityId, attributeName = "https://ontology.eglobalmark.com/apic#incoming", - attributeValueType = TemporalEntityAttribute.AttributeValueType.NUMBER, + attributeValueType = Attribute.AttributeValueType.NUMBER, createdAt = now, payload = EMPTY_JSON_PAYLOAD ) to listOf( FullAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), payload = buildAttributeInstancePayload( 550.0, ZonedDateTime.parse("2020-03-25T08:29:17.965206Z"), @@ -48,7 +49,7 @@ class TemporalEntityParameterizedSource { sub = null ), FullAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), payload = buildAttributeInstancePayload( 650.0, ZonedDateTime.parse("2020-03-25T08:33:17.965206Z"), @@ -70,36 +71,36 @@ class TemporalEntityParameterizedSource { Arguments.arguments( emptyList(), mapOf( - TemporalEntityAttribute( + Attribute( entityId = entityId, attributeName = "https://ontology.eglobalmark.com/apic#incoming", - attributeType = TemporalEntityAttribute.AttributeType.Relationship, - attributeValueType = TemporalEntityAttribute.AttributeValueType.STRING, + attributeType = Attribute.AttributeType.Relationship, + attributeValueType = Attribute.AttributeValueType.STRING, createdAt = now, payload = EMPTY_JSON_PAYLOAD ) to listOf( FullAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), payload = buildAttributeInstancePayload( "urn:ngsi-ld:Entity:1234", ZonedDateTime.parse("2020-03-25T08:29:17.965206Z"), null, "urn:ngsi-ld:Instance:45678".toUri(), - TemporalEntityAttribute.AttributeType.Relationship + Attribute.AttributeType.Relationship ), time = ZonedDateTime.parse("2020-03-25T08:29:17.965206Z"), timeproperty = JsonLdUtils.NGSILD_OBSERVED_AT_TERM, sub = null ), FullAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), payload = buildAttributeInstancePayload( "urn:ngsi-ld:Entity:5678", ZonedDateTime.parse("2020-03-25T08:33:17.965206Z"), null, "urn:ngsi-ld:Instance:45679".toUri(), - TemporalEntityAttribute.AttributeType.Relationship + Attribute.AttributeType.Relationship ), time = ZonedDateTime.parse("2020-03-25T08:33:17.965206Z"), timeproperty = JsonLdUtils.NGSILD_OBSERVED_AT_TERM, @@ -116,17 +117,17 @@ class TemporalEntityParameterizedSource { Arguments.arguments( emptyList(), mapOf( - TemporalEntityAttribute( + Attribute( entityId = entityId, attributeName = "https://ontology.eglobalmark.com/apic#incoming", - attributeValueType = TemporalEntityAttribute.AttributeValueType.NUMBER, + attributeValueType = Attribute.AttributeValueType.NUMBER, datasetId = "urn:ngsi-ld:Dataset:01234".toUri(), createdAt = now, payload = EMPTY_JSON_PAYLOAD ) to listOf( FullAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), payload = buildAttributeInstancePayload( 550.0, ZonedDateTime.parse("2020-03-25T08:29:17.965206Z"), @@ -138,7 +139,7 @@ class TemporalEntityParameterizedSource { sub = null ), FullAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), payload = buildAttributeInstancePayload( 650.0, ZonedDateTime.parse("2020-03-25T08:33:17.965206Z"), @@ -150,17 +151,17 @@ class TemporalEntityParameterizedSource { sub = null ) ), - TemporalEntityAttribute( + Attribute( entityId = entityId, attributeName = "https://ontology.eglobalmark.com/apic#incoming", - attributeValueType = TemporalEntityAttribute.AttributeValueType.NUMBER, + attributeValueType = Attribute.AttributeValueType.NUMBER, datasetId = "urn:ngsi-ld:Dataset:45678".toUri(), createdAt = now, payload = EMPTY_JSON_PAYLOAD ) to listOf( FullAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), payload = buildAttributeInstancePayload( 487.0, ZonedDateTime.parse("2020-03-25T08:29:17.965206Z"), @@ -172,7 +173,7 @@ class TemporalEntityParameterizedSource { sub = null ), FullAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), payload = buildAttributeInstancePayload( 698.0, ZonedDateTime.parse("2020-03-25T08:33:17.965206Z"), @@ -194,17 +195,17 @@ class TemporalEntityParameterizedSource { Arguments.arguments( emptyList(), mapOf( - TemporalEntityAttribute( + Attribute( entityId = entityId, attributeName = "https://ontology.eglobalmark.com/apic#incoming", - attributeValueType = TemporalEntityAttribute.AttributeValueType.STRING, + attributeValueType = Attribute.AttributeValueType.STRING, datasetId = "urn:ngsi-ld:Dataset:45678".toUri(), createdAt = now, payload = EMPTY_JSON_PAYLOAD ) to listOf( FullAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), payload = buildAttributeInstancePayload( "Beehive_incoming_123", ZonedDateTime.parse("2020-03-25T08:29:17.965206Z"), @@ -216,7 +217,7 @@ class TemporalEntityParameterizedSource { sub = null ), FullAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), payload = buildAttributeInstancePayload( "Beehive_incoming_124", ZonedDateTime.parse("2020-03-25T08:33:17.965206Z"), @@ -238,17 +239,17 @@ class TemporalEntityParameterizedSource { Arguments.arguments( emptyList(), mapOf( - TemporalEntityAttribute( + Attribute( entityId = entityId, attributeName = "https://ontology.eglobalmark.com/apic#incoming", - attributeValueType = TemporalEntityAttribute.AttributeValueType.STRING, + attributeValueType = Attribute.AttributeValueType.STRING, datasetId = "urn:ngsi-ld:Dataset:45678".toUri(), createdAt = now, payload = EMPTY_JSON_PAYLOAD ) to listOf( FullAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), payload = buildAttributeInstancePayload( "Beehive_incoming_123", ZonedDateTime.parse("2020-03-25T08:29:17.965206Z"), @@ -260,7 +261,7 @@ class TemporalEntityParameterizedSource { sub = "sub1" ), FullAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), payload = buildAttributeInstancePayload( "Beehive_incoming_124", ZonedDateTime.parse("2020-03-25T08:33:17.965206Z"), @@ -282,16 +283,16 @@ class TemporalEntityParameterizedSource { Arguments.arguments( emptyList(), mapOf( - TemporalEntityAttribute( + Attribute( entityId = entityId, attributeName = "https://ontology.eglobalmark.com/apic#incoming", - attributeValueType = TemporalEntityAttribute.AttributeValueType.STRING, + attributeValueType = Attribute.AttributeValueType.STRING, createdAt = now, payload = EMPTY_JSON_PAYLOAD ) to listOf( FullAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), payload = buildAttributeInstancePayload( "Beehive_incoming_123", ZonedDateTime.parse("2020-03-25T08:29:17.965206Z"), @@ -303,7 +304,7 @@ class TemporalEntityParameterizedSource { sub = null ), FullAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), payload = buildAttributeInstancePayload( "Beehive_incoming_124", ZonedDateTime.parse("2020-03-25T08:33:17.965206Z"), @@ -327,21 +328,21 @@ class TemporalEntityParameterizedSource { Arguments.arguments( emptyList(), mapOf( - TemporalEntityAttribute( + Attribute( entityId = entityId, attributeName = "https://ontology.eglobalmark.com/apic#incoming", - attributeValueType = TemporalEntityAttribute.AttributeValueType.NUMBER, + attributeValueType = Attribute.AttributeValueType.NUMBER, createdAt = now, payload = EMPTY_JSON_PAYLOAD ) to listOf( SimplifiedAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), value = 550.0, time = ZonedDateTime.parse("2020-03-25T08:29:17.965206Z") ), SimplifiedAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), value = 650.0, time = ZonedDateTime.parse("2020-03-25T08:33:17.965206Z") ) @@ -358,42 +359,42 @@ class TemporalEntityParameterizedSource { Arguments.arguments( emptyList(), mapOf( - TemporalEntityAttribute( + Attribute( entityId = entityId, attributeName = "https://ontology.eglobalmark.com/apic#incoming", - attributeValueType = TemporalEntityAttribute.AttributeValueType.NUMBER, + attributeValueType = Attribute.AttributeValueType.NUMBER, datasetId = "urn:ngsi-ld:Dataset:01234".toUri(), createdAt = now, payload = EMPTY_JSON_PAYLOAD ) to listOf( SimplifiedAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), value = 550.0, time = ZonedDateTime.parse("2020-03-25T08:29:17.965206Z") ), SimplifiedAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), value = 650.0, time = ZonedDateTime.parse("2020-03-25T08:33:17.965206Z") ) ), - TemporalEntityAttribute( + Attribute( entityId = entityId, attributeName = "https://ontology.eglobalmark.com/apic#incoming", - attributeValueType = TemporalEntityAttribute.AttributeValueType.NUMBER, + attributeValueType = Attribute.AttributeValueType.NUMBER, datasetId = "urn:ngsi-ld:Dataset:45678".toUri(), createdAt = now, payload = EMPTY_JSON_PAYLOAD ) to listOf( SimplifiedAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), value = 487.0, time = ZonedDateTime.parse("2020-03-25T08:29:17.965206Z") ), SimplifiedAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), value = 698.0, time = ZonedDateTime.parse("2020-03-25T08:33:17.965206Z") ) @@ -408,22 +409,22 @@ class TemporalEntityParameterizedSource { Arguments.arguments( emptyList(), mapOf( - TemporalEntityAttribute( + Attribute( entityId = entityId, attributeName = "https://ontology.eglobalmark.com/apic#incoming", - attributeValueType = TemporalEntityAttribute.AttributeValueType.STRING, + attributeValueType = Attribute.AttributeValueType.STRING, datasetId = "urn:ngsi-ld:Dataset:45678".toUri(), createdAt = now, payload = EMPTY_JSON_PAYLOAD ) to listOf( SimplifiedAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), value = "Beehive_incoming_123", time = ZonedDateTime.parse("2020-03-25T08:29:17.965206Z") ), SimplifiedAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), value = "Beehive_incoming_124", time = ZonedDateTime.parse("2020-03-25T08:33:17.965206Z") ) @@ -438,21 +439,21 @@ class TemporalEntityParameterizedSource { Arguments.arguments( emptyList(), mapOf( - TemporalEntityAttribute( + Attribute( entityId = entityId, attributeName = "https://ontology.eglobalmark.com/apic#incoming", - attributeValueType = TemporalEntityAttribute.AttributeValueType.STRING, + attributeValueType = Attribute.AttributeValueType.STRING, createdAt = now, payload = EMPTY_JSON_PAYLOAD ) to listOf( SimplifiedAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), value = "Beehive_incoming_123", time = ZonedDateTime.parse("2020-03-25T08:29:17.965206Z") ), SimplifiedAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), value = "Beehive_incoming_124", time = ZonedDateTime.parse("2020-03-25T08:33:17.965206Z") ) @@ -469,23 +470,23 @@ class TemporalEntityParameterizedSource { Arguments.arguments( emptyList(), mapOf( - TemporalEntityAttribute( + Attribute( entityId = entityId, attributeName = "https://ontology.eglobalmark.com/apic#incoming", - attributeType = TemporalEntityAttribute.AttributeType.Relationship, - attributeValueType = TemporalEntityAttribute.AttributeValueType.STRING, + attributeType = Attribute.AttributeType.Relationship, + attributeValueType = Attribute.AttributeValueType.STRING, datasetId = "urn:ngsi-ld:Dataset:45678".toUri(), createdAt = now, payload = EMPTY_JSON_PAYLOAD ) to listOf( SimplifiedAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), value = "urn:ngsi-ld:Entity:1234", time = ZonedDateTime.parse("2020-03-25T08:29:17.965206Z") ), SimplifiedAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), value = "urn:ngsi-ld:Entity:5678", time = ZonedDateTime.parse("2020-03-25T08:33:17.965206Z") ) @@ -510,7 +511,7 @@ class TemporalEntityParameterizedSource { time = ZonedDateTime.parse("2020-03-25T09:29:17.965206Z") ) ), - emptyMap>(), + emptyMap>(), true, false, loadSampleData("expectations/beehive_scope_multi_instances_temporal_values.jsonld") @@ -532,7 +533,7 @@ class TemporalEntityParameterizedSource { time = ZonedDateTime.parse("2020-03-25T09:29:17.965206Z") ) ), - emptyMap>(), + emptyMap>(), false, false, loadSampleData("expectations/beehive_scope_multi_instances.jsonld") @@ -542,25 +543,25 @@ class TemporalEntityParameterizedSource { Arguments.arguments( emptyList(), mapOf( - TemporalEntityAttribute( + Attribute( entityId = entityId, attributeName = "https://ontology.eglobalmark.com/apic#luminosity", - attributeType = TemporalEntityAttribute.AttributeType.JsonProperty, - attributeValueType = TemporalEntityAttribute.AttributeValueType.JSON, + attributeType = Attribute.AttributeType.JsonProperty, + attributeValueType = Attribute.AttributeValueType.JSON, datasetId = null, createdAt = now, payload = SAMPLE_JSON_PROPERTY_PAYLOAD ) to listOf( SimplifiedAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), value = """ { "id": "123", "stringValue": "value", "nullValue": null } """, time = ZonedDateTime.parse("2020-03-25T08:29:17.965206Z") ), SimplifiedAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), value = """ { "id": "456", "stringValue": "anotherValue" } """, @@ -577,18 +578,18 @@ class TemporalEntityParameterizedSource { Arguments.arguments( emptyList(), mapOf( - TemporalEntityAttribute( + Attribute( entityId = entityId, attributeName = "https://ontology.eglobalmark.com/apic#friendlyName", - attributeType = TemporalEntityAttribute.AttributeType.LanguageProperty, - attributeValueType = TemporalEntityAttribute.AttributeValueType.OBJECT, + attributeType = Attribute.AttributeType.LanguageProperty, + attributeValueType = Attribute.AttributeValueType.OBJECT, datasetId = null, createdAt = now, payload = SAMPLE_LANGUAGE_PROPERTY_PAYLOAD ) to listOf( SimplifiedAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), value = """ [{ "@value": "One beautiful beehive", @@ -602,7 +603,7 @@ class TemporalEntityParameterizedSource { time = ZonedDateTime.parse("2020-03-25T08:29:17.965206Z") ), SimplifiedAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), value = """ [{ "@value": "My beautiful beehive", @@ -626,18 +627,18 @@ class TemporalEntityParameterizedSource { Arguments.arguments( emptyList(), mapOf( - TemporalEntityAttribute( + Attribute( entityId = entityId, attributeName = "https://ontology.eglobalmark.com/apic#category", - attributeType = TemporalEntityAttribute.AttributeType.VocabProperty, - attributeValueType = TemporalEntityAttribute.AttributeValueType.ARRAY, + attributeType = Attribute.AttributeType.VocabProperty, + attributeValueType = Attribute.AttributeValueType.ARRAY, datasetId = null, createdAt = now, payload = SAMPLE_VOCAB_PROPERTY_PAYLOAD ) to listOf( SimplifiedAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), value = """ [{ "@id": "https://uri.etsi.org/ngsi-ld/default-context/stellio" @@ -649,7 +650,7 @@ class TemporalEntityParameterizedSource { time = ZonedDateTime.parse("2020-03-25T08:29:17.965206Z") ), SimplifiedAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), + attributeUuid = UUID.randomUUID(), value = """ [{ "@id": "https://uri.etsi.org/ngsi-ld/default-context/stellio" diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/util/EntitiesQueryUtilsTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/util/TemporalQueryUtilsTests.kt similarity index 58% rename from search-service/src/test/kotlin/com/egm/stellio/search/util/EntitiesQueryUtilsTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/temporal/util/TemporalQueryUtilsTests.kt index c3c259b80..76e68a8cf 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/util/EntitiesQueryUtilsTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/util/TemporalQueryUtilsTests.kt @@ -1,249 +1,35 @@ -package com.egm.stellio.search.util - -import arrow.core.Either -import arrow.core.raise.either -import com.egm.stellio.search.model.AttributeInstance -import com.egm.stellio.search.model.EntitiesQuery -import com.egm.stellio.search.model.Query -import com.egm.stellio.search.model.TemporalQuery +package com.egm.stellio.search.temporal.util + import com.egm.stellio.search.support.buildDefaultPagination import com.egm.stellio.search.support.buildDefaultTestTemporalQuery +import com.egm.stellio.search.temporal.model.AttributeInstance +import com.egm.stellio.search.temporal.model.TemporalQuery 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.GeoQuery -import com.egm.stellio.shared.util.* -import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_DEFAULT_VOCAB -import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_OBSERVATION_SPACE_PROPERTY +import com.egm.stellio.shared.util.APIARY_TYPE +import com.egm.stellio.shared.util.APIC_COMPOUND_CONTEXTS +import com.egm.stellio.shared.util.BEEHIVE_TYPE +import com.egm.stellio.shared.util.INCOMING_PROPERTY +import com.egm.stellio.shared.util.OUTGOING_PROPERTY +import com.egm.stellio.shared.util.shouldFail +import com.egm.stellio.shared.util.shouldSucceedAndResult +import com.egm.stellio.shared.util.toUri import io.mockk.every import io.mockk.mockkClass import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertInstanceOf +import org.junit.jupiter.api.Assertions.assertIterableEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.springframework.test.context.ActiveProfiles import org.springframework.util.LinkedMultiValueMap -import org.springframework.util.MultiValueMap -import java.net.URI import java.time.ZonedDateTime @ActiveProfiles("test") -class EntitiesQueryUtilsTests { - - @Test - fun `it should parse query parameters`() = runTest { - val requestParams = gimmeEntitiesQueryParams() - val entitiesQuery = composeEntitiesQuery( - buildDefaultPagination(1, 20), - requestParams, - APIC_COMPOUND_CONTEXTS - ).shouldSucceedAndResult() - - assertEquals("$BEEHIVE_TYPE,$APIARY_TYPE", entitiesQuery.typeSelection) - assertEquals(setOf(INCOMING_PROPERTY, OUTGOING_PROPERTY), entitiesQuery.attrs) - assertEquals( - setOf("urn:ngsi-ld:BeeHive:TESTC".toUri(), "urn:ngsi-ld:BeeHive:TESTB".toUri()), - entitiesQuery.ids - ) - assertEquals(".*BeeHive.*", entitiesQuery.idPattern) - assertEquals("brandName!=Mercedes", entitiesQuery.q) - assertEquals(setOf("urn:ngsi-ld:Dataset:Test1", "urn:ngsi-ld:Dataset:Test2"), entitiesQuery.datasetId) - assertEquals(true, entitiesQuery.paginationQuery.count) - assertEquals(1, entitiesQuery.paginationQuery.offset) - assertEquals(10, entitiesQuery.paginationQuery.limit) - } - - @Test - fun `it should decode q in query parameters`() = runTest { - val requestParams = LinkedMultiValueMap() - requestParams.add("q", "speed%3E50%3BfoodName%3D%3Ddietary+fibres") - val entitiesQuery = composeEntitiesQuery( - buildDefaultPagination(30, 100), - requestParams, - NGSILD_TEST_CORE_CONTEXTS - ).shouldSucceedAndResult() - - assertEquals("speed>50;foodName==dietary fibres", entitiesQuery.q) - } - - @Test - fun `it should set default values in query parameters`() = runTest { - val requestParams = LinkedMultiValueMap() - val entitiesQuery = composeEntitiesQuery( - buildDefaultPagination(30, 100), - requestParams, - NGSILD_TEST_CORE_CONTEXTS - ).shouldSucceedAndResult() - - assertEquals(null, entitiesQuery.typeSelection) - assertEquals(emptySet(), entitiesQuery.attrs) - assertEquals(emptySet(), entitiesQuery.ids) - assertEquals(null, entitiesQuery.idPattern) - assertEquals(null, entitiesQuery.q) - assertEquals(emptySet(), entitiesQuery.datasetId) - assertEquals(false, entitiesQuery.paginationQuery.count) - assertEquals(0, entitiesQuery.paginationQuery.offset) - assertEquals(30, entitiesQuery.paginationQuery.limit) - } - - private fun gimmeEntitiesQueryParams(): LinkedMultiValueMap { - val requestParams = LinkedMultiValueMap() - requestParams.add("type", "BeeHive,Apiary") - requestParams.add("attrs", "incoming,outgoing") - requestParams.add("id", "urn:ngsi-ld:BeeHive:TESTC,urn:ngsi-ld:BeeHive:TESTB") - requestParams.add("idPattern", ".*BeeHive.*") - requestParams.add("q", "brandName!=Mercedes") - requestParams.add("datasetId", "urn:ngsi-ld:Dataset:Test1,urn:ngsi-ld:Dataset:Test2") - requestParams.add("count", "true") - requestParams.add("offset", "1") - requestParams.add("limit", "10") - requestParams.add("options", "keyValues") - return requestParams - } - - @Test - fun `it should parse a valid complete query`() = runTest { - val query = """ - { - "type": "Query", - "entities": [{ - "id": "urn:ngsi-ld:BeeHive:TESTC", - "idPattern": "urn:ngsi-ld:BeeHive:*", - "type": "BeeHive" - }], - "attrs": ["attr1", "attr2"], - "q": "temperature>32", - "geoQ": { - "geometry": "Point", - "coordinates": [1.0, 1.0], - "georel": "equals", - "geoproperty": "observationSpace" - }, - "temporalQ": { - "timerel": "between", - "timeAt": "2023-10-01T12:34:56Z", - "endTimeAt": "2023-10-02T12:34:56Z", - "lastN": 10, - "timeproperty": "observedAt" - }, - "scopeQ": "/Nantes", - "datasetId": ["urn:ngsi-ld:Dataset:Test1", "urn:ngsi-ld:Dataset:Test2"] - } - """.trimIndent() - - composeEntitiesQueryFromPostRequest( - buildDefaultPagination(30, 100), - query, - LinkedMultiValueMap(), - APIC_COMPOUND_CONTEXTS - ).shouldSucceedWith { - assertEquals(setOf("urn:ngsi-ld:BeeHive:TESTC".toUri()), it.ids) - assertEquals("urn:ngsi-ld:BeeHive:*", it.idPattern) - assertEquals(BEEHIVE_TYPE, it.typeSelection) - assertEquals(setOf("${NGSILD_DEFAULT_VOCAB}attr1", "${NGSILD_DEFAULT_VOCAB}attr2"), it.attrs) - assertEquals("temperature>32", it.q) - assertEquals(GeoQuery.GeometryType.POINT, it.geoQuery?.geometry) - assertEquals("[1.0, 1.0]", it.geoQuery?.coordinates) - assertEquals(GEO_QUERY_GEOREL_EQUALS, it.geoQuery?.georel) - assertEquals(NGSILD_OBSERVATION_SPACE_PROPERTY, it.geoQuery?.geoproperty) - assertEquals("/Nantes", it.scopeQ) - assertEquals(setOf("urn:ngsi-ld:Dataset:Test1", "urn:ngsi-ld:Dataset:Test2"), it.datasetId) - } - } - - @Test - fun `it should parse a valid simple query`() = runTest { - val query = """ - { - "type": "Query", - "entities": [{ - "type": "BeeHive" - }], - "attrs": ["attr1"], - "q": "temperature>32" - } - """.trimIndent() - - composeEntitiesQueryFromPostRequest( - buildDefaultPagination(30, 100), - query, - LinkedMultiValueMap(), - APIC_COMPOUND_CONTEXTS - ).shouldSucceedWith { - assertEquals(BEEHIVE_TYPE, it.typeSelection) - assertEquals(setOf("${NGSILD_DEFAULT_VOCAB}attr1"), it.attrs) - assertEquals("temperature>32", it.q) - } - } - - @Test - fun `it should not validate a query if the type is not correct`() { - val query = """ - { - "type": "NotAQuery", - "attrs": ["attr1", "attr2"] - } - """.trimIndent() - - composeEntitiesQueryFromPostRequest( - buildDefaultPagination(30, 100), - query, - LinkedMultiValueMap(), - APIC_COMPOUND_CONTEXTS - ).shouldFailWith { - it is BadRequestDataException && - it.message == "The type parameter should be equals to 'Query'" - } - } - - @Test - fun `it should not validate a query if the payload could not be parsed because the JSON is invalid`() { - val query = """ - { - "type": "Query",, - "attrs": ["attr1", "attr2"] - } - """.trimIndent() - - composeEntitiesQueryFromPostRequest( - buildDefaultPagination(30, 100), - query, - LinkedMultiValueMap(), - APIC_COMPOUND_CONTEXTS - ).shouldFailWith { - it is BadRequestDataException && - it.message.startsWith("The supplied query could not be parsed") - } - } - - @Test - fun `it should not validate a query if the payload contains unexpected parameters`() { - val query = """ - { - "type": "Query", - "property": "anUnexpectedProperty" - } - """.trimIndent() - - composeEntitiesQueryFromPostRequest( - buildDefaultPagination(30, 100), - query, - LinkedMultiValueMap(), - APIC_COMPOUND_CONTEXTS - ).shouldFailWith { - it is BadRequestDataException && - it.message.startsWith("The supplied query could not be parsed") - } - } - - private fun composeEntitiesQueryFromPostRequest( - defaultPagination: ApplicationProperties.Pagination, - requestBody: String, - requestParams: MultiValueMap, - contexts: List - ): Either = either { - val query = Query(requestBody).bind() - composeEntitiesQueryFromPostRequest(defaultPagination, query, requestParams, contexts).bind() - } +class TemporalQueryUtilsTests { @Test fun `it should not validate the temporal query if type or attrs are not present`() = runTest { diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerPaginationTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityHandlerPaginationTests.kt similarity index 85% rename from search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerPaginationTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityHandlerPaginationTests.kt index 3b2ecb606..a6bd20dc8 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerPaginationTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityHandlerPaginationTests.kt @@ -1,9 +1,7 @@ -package com.egm.stellio.search.web +package com.egm.stellio.search.temporal.web import arrow.core.Either -import arrow.core.right -import com.egm.stellio.search.config.SearchProperties -import com.egm.stellio.shared.util.BEEHIVE_TYPE +import com.egm.stellio.search.common.config.SearchProperties import com.egm.stellio.shared.util.loadAndExpandSampleData import com.egm.stellio.shared.util.toNgsiLdFormat import io.mockk.coEvery @@ -33,9 +31,8 @@ class TemporalEntityHandlerPaginationTests : TemporalEntityHandlerTestCommon() { "/temporal/beehive_create_temporal_entity.jsonld" ) val secondTemporalEntity = loadAndExpandSampleData("beehive.jsonld") - coEvery { authorizationService.computeAccessRightFilter(any()) } returns { null } coEvery { - queryService.queryTemporalEntities(any(), any()) + temporalQueryService.queryTemporalEntities(any(), any()) } returns Either.Right( Triple( listOf(firstTemporalEntity, secondTemporalEntity), @@ -46,14 +43,9 @@ class TemporalEntityHandlerPaginationTests : TemporalEntityHandlerTestCommon() { } private suspend fun mockQueryEntityService(range: Range? = null) { - val firstTemporalEntity = loadAndExpandSampleData( - "/temporal/beehive_create_temporal_entity.jsonld" - ) - coEvery { authorizationService.userCanReadEntity(any(), any()) } returns Unit.right() - coEvery { entityPayloadService.checkEntityExistence(any()) } returns Unit.right() - coEvery { entityPayloadService.getTypes(any()) } returns listOf(BEEHIVE_TYPE).right() + val firstTemporalEntity = loadAndExpandSampleData("/temporal/beehive_create_temporal_entity.jsonld") coEvery { - queryService.queryTemporalEntity(any(), any()) + temporalQueryService.queryTemporalEntity(any(), any(), any()) } returns Either.Right( firstTemporalEntity to range ) @@ -72,7 +64,7 @@ class TemporalEntityHandlerPaginationTests : TemporalEntityHandlerTestCommon() { .exchange() coVerify { - queryService.queryTemporalEntities( + temporalQueryService.queryTemporalEntities( match { temporalEntitiesQuery -> temporalEntitiesQuery.temporalQuery.instanceLimit == 5 && !temporalEntitiesQuery.temporalQuery.hasLastN() @@ -96,7 +88,7 @@ class TemporalEntityHandlerPaginationTests : TemporalEntityHandlerTestCommon() { .exchange() coVerify { - queryService.queryTemporalEntities( + temporalQueryService.queryTemporalEntities( match { temporalEntitiesQuery -> temporalEntitiesQuery.temporalQuery.instanceLimit == 5 && temporalEntitiesQuery.temporalQuery.lastN == 7 diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerTestCommon.kt b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityHandlerTestCommon.kt similarity index 71% rename from search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerTestCommon.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityHandlerTestCommon.kt index de1c2fbcd..0102fd19a 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerTestCommon.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityHandlerTestCommon.kt @@ -1,8 +1,9 @@ -package com.egm.stellio.search.web +package com.egm.stellio.search.temporal.web -import com.egm.stellio.search.authorization.AuthorizationService -import com.egm.stellio.search.config.SearchProperties -import com.egm.stellio.search.service.* +import com.egm.stellio.search.common.config.SearchProperties +import com.egm.stellio.search.entity.service.EntityService +import com.egm.stellio.search.temporal.service.TemporalQueryService +import com.egm.stellio.search.temporal.service.TemporalService import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.util.JSON_LD_MEDIA_TYPE import com.egm.stellio.shared.util.MOCK_USER_SUB @@ -23,20 +24,14 @@ open class TemporalEntityHandlerTestCommon { @Autowired protected lateinit var webClient: WebTestClient - @MockkBean(relaxed = true) - protected lateinit var queryService: QueryService - - @MockkBean - protected lateinit var entityPayloadService: EntityPayloadService - @MockkBean - protected lateinit var attributeInstanceService: AttributeInstanceService + protected lateinit var temporalService: TemporalService @MockkBean(relaxed = true) - protected lateinit var temporalEntityAttributeService: TemporalEntityAttributeService + protected lateinit var temporalQueryService: TemporalQueryService @MockkBean - protected lateinit var authorizationService: AuthorizationService + protected lateinit var entityService: EntityService @BeforeAll fun configureWebClientDefaults() { diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityHandlerTests.kt similarity index 72% rename from search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityHandlerTests.kt index a05fbbfd9..a1a40d479 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityHandlerTests.kt @@ -1,36 +1,32 @@ -package com.egm.stellio.search.web +package com.egm.stellio.search.temporal.web import arrow.core.Either -import arrow.core.Some import arrow.core.left import arrow.core.right -import com.egm.stellio.search.config.SearchProperties -import com.egm.stellio.search.model.EntityPayload -import com.egm.stellio.search.model.SimplifiedAttributeInstanceResult -import com.egm.stellio.search.model.TemporalEntityAttribute -import com.egm.stellio.search.model.TemporalQuery -import com.egm.stellio.search.support.EMPTY_JSON_PAYLOAD +import com.egm.stellio.search.common.config.SearchProperties import com.egm.stellio.search.support.buildDefaultTestTemporalQuery +import com.egm.stellio.search.temporal.model.TemporalQuery +import com.egm.stellio.search.temporal.service.TemporalService.CreateOrUpdateResult import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.model.* import com.egm.stellio.shared.util.* -import com.egm.stellio.shared.util.JsonUtils.deserializeAsMap -import io.mockk.* +import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_DEFAULT_VOCAB +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.confirmVerified +import io.mockk.mockkClass import kotlinx.coroutines.test.runTest import org.hamcrest.core.Is import org.junit.jupiter.api.Test import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest -import org.springframework.core.io.ClassPathResource import org.springframework.http.HttpHeaders import org.springframework.http.HttpMethod import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.test.context.ActiveProfiles import org.springframework.web.reactive.function.BodyInserters -import java.time.ZoneOffset import java.time.ZonedDateTime -import java.util.UUID @ActiveProfiles("test") @WebFluxTest(TemporalEntityHandler::class) @@ -38,32 +34,17 @@ import java.util.UUID class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { private val entityUri = "urn:ngsi-ld:BeeHive:TESTC".toUri() - private val temporalEntityAttributeName = "speed" + private val attributeName = "speed" private val attributeInstanceId = "urn:ngsi-ld:Instance:01".toUri() - private fun buildDefaultMockResponsesForAddAttributes() { - coEvery { entityPayloadService.checkEntityExistence(any()) } returns Unit.right() - coEvery { entityPayloadService.getTypes(any()) } returns listOf(BEEHIVE_TYPE).right() - coEvery { authorizationService.userCanUpdateEntity(any(), any()) } returns Unit.right() - } - @Test fun `create temporal entity should return a 201`() = runTest { - val jsonLdFile = ClassPathResource("/ngsild/temporal/beehive_create_temporal_entity.jsonld") - - coEvery { entityPayloadService.checkEntityExistence(any(), any()) } returns Unit.right() - coEvery { authorizationService.userCanCreateEntities(sub) } returns Unit.right() - coEvery { entityPayloadService.createEntity(any(), any(), any()) } returns Unit.right() - coEvery { entityPayloadService.upsertAttributes(any(), any(), any()) } returns Unit.right() - coEvery { authorizationService.createOwnerRight(any(), any()) } returns Unit.right() - - val data = - loadSampleData("/temporal/beehive_create_temporal_entity_first_instance.jsonld").deserializeAsMap() - val expandedEntity = JsonLdUtils.expandJsonLdEntity(data, APIC_COMPOUND_CONTEXTS) - val expectedInstancesFilePath = - "/temporal/beehive_create_temporal_entity_without_first_instance_expanded.jsonld" - val jsonInstances = - loadSampleData(expectedInstancesFilePath).deserializeAsMap() as ExpandedAttributes + val jsonLdFile = loadSampleData("temporal/beehive_create_temporal_entity.jsonld") + val expandedEntity = JsonLdUtils.expandJsonLdEntity(jsonLdFile, APIC_COMPOUND_CONTEXTS) + + coEvery { + temporalService.createOrUpdateTemporalEntity(any(), any(), any()) + } returns CreateOrUpdateResult.CREATED.right() webClient.post() .uri("/ngsi-ld/v1/temporal/entities") @@ -72,39 +53,23 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { .expectStatus().isCreated .expectHeader().value("Location", Is.`is`("/ngsi-ld/v1/temporal/entities/$entityUri")) - coVerify { authorizationService.userCanCreateEntities(eq(sub)) } - coVerify { entityPayloadService.checkEntityExistence(eq(entityUri), true) } - coVerify { - entityPayloadService.createEntity( - any(), - eq(expandedEntity), - eq(sub.value) - ) - } coVerify { - entityPayloadService.upsertAttributes( + temporalService.createOrUpdateTemporalEntity( eq(entityUri), - eq(jsonInstances), + eq(expandedEntity), eq(sub.value) ) } - coVerify { authorizationService.createOwnerRight(eq(entityUri), eq(sub)) } } @Test - fun `update temporal entity should return a 204`() { - val jsonLdFile = ClassPathResource("/ngsild/temporal/beehive_update_temporal_entity.jsonld") + fun `update temporal entity should return a 204`() = runTest { + val jsonLdFile = loadSampleData("temporal/beehive_update_temporal_entity.jsonld") + val expandedEntity = JsonLdUtils.expandJsonLdEntity(jsonLdFile, APIC_COMPOUND_CONTEXTS) - coEvery { authorizationService.userCanUpdateEntity(entityUri, sub) } returns Unit.right() coEvery { - entityPayloadService.checkEntityExistence(any(), any()) - } returns ResourceNotFoundException(entityNotFoundMessage("urn:ngsi-ld:BeeHive:TESTC")).left() - coEvery { entityPayloadService.upsertAttributes(any(), any(), any()) } returns Unit.right() - - val expectedInstancesFilePath = - "/temporal/beehive_update_temporal_entity_without_mandatory_fields_expanded.jsonld" - val jsonInstances = - loadSampleData(expectedInstancesFilePath).deserializeAsMap() as ExpandedAttributes + temporalService.createOrUpdateTemporalEntity(any(), any(), any()) + } returns CreateOrUpdateResult.UPSERTED.right() webClient.post() .uri("/ngsi-ld/v1/temporal/entities") @@ -112,11 +77,10 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { .exchange() .expectStatus().isNoContent - coVerify { authorizationService.userCanUpdateEntity(eq(entityUri), eq(sub)) } coVerify { - entityPayloadService.upsertAttributes( + temporalService.createOrUpdateTemporalEntity( eq(entityUri), - eq(jsonInstances), + eq(expandedEntity), eq(sub.value) ) } @@ -127,8 +91,7 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { val entityTemporalFragment = loadSampleData("fragments/temporal_entity_fragment_one_attribute_one_instance.jsonld") - buildDefaultMockResponsesForAddAttributes() - coEvery { entityPayloadService.upsertAttributes(any(), any(), any()) } returns Unit.right() + coEvery { temporalService.upsertAttributes(any(), any(), any()) } returns Unit.right() webClient.post() .uri("/ngsi-ld/v1/temporal/entities/$entityUri/attrs") @@ -139,12 +102,12 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { .expectStatus().isNoContent coVerify { - entityPayloadService.upsertAttributes( + temporalService.upsertAttributes( eq(entityUri), match { it.size == 1 }, - any() + eq(sub.value) ) } } @@ -154,8 +117,7 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { val entityTemporalFragment = loadSampleData("fragments/temporal_entity_fragment_one_attribute_many_instances.jsonld") - buildDefaultMockResponsesForAddAttributes() - coEvery { entityPayloadService.upsertAttributes(any(), any(), any()) } returns Unit.right() + coEvery { temporalService.upsertAttributes(any(), any(), any()) } returns Unit.right() webClient.post() .uri("/ngsi-ld/v1/temporal/entities/$entityUri/attrs") @@ -166,12 +128,12 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { .expectStatus().isNoContent coVerify { - entityPayloadService.upsertAttributes( + temporalService.upsertAttributes( eq(entityUri), match { it.size == 1 }, - any() + eq(sub.value) ) } } @@ -181,8 +143,7 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { val entityTemporalFragment = loadSampleData("fragments/temporal_entity_fragment_many_attributes_one_instance.jsonld") - buildDefaultMockResponsesForAddAttributes() - coEvery { entityPayloadService.upsertAttributes(any(), any(), any()) } returns Unit.right() + coEvery { temporalService.upsertAttributes(any(), any(), any()) } returns Unit.right() webClient.post() .uri("/ngsi-ld/v1/temporal/entities/$entityUri/attrs") @@ -193,12 +154,12 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { .expectStatus().isNoContent coVerify { - entityPayloadService.upsertAttributes( + temporalService.upsertAttributes( eq(entityUri), match { it.size == 2 }, - any() + eq(sub.value) ) } } @@ -208,8 +169,7 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { val entityTemporalFragment = loadSampleData("fragments/temporal_entity_fragment_many_attributes_many_instances.jsonld") - buildDefaultMockResponsesForAddAttributes() - coEvery { entityPayloadService.upsertAttributes(any(), any(), any()) } returns Unit.right() + coEvery { temporalService.upsertAttributes(any(), any(), any()) } returns Unit.right() webClient.post() .uri("/ngsi-ld/v1/temporal/entities/$entityUri/attrs") @@ -220,20 +180,18 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { .expectStatus().isNoContent coVerify { - entityPayloadService.upsertAttributes( + temporalService.upsertAttributes( eq(entityUri), match { it.size == 2 }, - any() + eq(sub.value) ) } } @Test fun `it should return a 400 if temporal entity fragment is badly formed`() { - buildDefaultMockResponsesForAddAttributes() - webClient.post() .uri("/ngsi-ld/v1/temporal/entities/$entityUri/attrs") .header("Link", APIC_HEADER_LINK) @@ -257,10 +215,8 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { val entityTemporalFragment = loadSampleData("fragments/temporal_entity_fragment_many_attributes_many_instances.jsonld") - coEvery { entityPayloadService.checkEntityExistence(any()) } returns Unit.right() - coEvery { entityPayloadService.getTypes(any()) } returns listOf(BEEHIVE_TYPE).right() coEvery { - authorizationService.userCanUpdateEntity(any(), any()) + temporalService.upsertAttributes(any(), any(), any()) } returns AccessDeniedException("User forbidden write access to entity $entityUri").left() webClient.post() @@ -270,21 +226,10 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { .body(BodyInserters.fromValue(entityTemporalFragment)) .exchange() .expectStatus().isForbidden - - coVerify { temporalEntityAttributeService.getForEntityAndAttribute(any(), any()) wasNot Called } - coVerify { attributeInstanceService.addAttributeInstance(any(), any(), any()) wasNot Called } - } - - private fun buildDefaultMockResponsesForGetEntity() { - coEvery { entityPayloadService.checkEntityExistence(any()) } returns Unit.right() - coEvery { entityPayloadService.getTypes(any()) } returns listOf(BEEHIVE_TYPE).right() - coEvery { authorizationService.userCanReadEntity(any(), any()) } returns Unit.right() } @Test fun `it should raise a 400 if timerel is present without time query param`() { - buildDefaultMockResponsesForGetEntity() - webClient.get() .uri("/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?timerel=before") .exchange() @@ -302,8 +247,6 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { @Test fun `it should raise a 400 if time is present without timerel query param`() { - buildDefaultMockResponsesForGetEntity() - webClient.get() .uri("/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?timeAt=2020-10-29T18:00:00Z") .exchange() @@ -321,27 +264,19 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { @Test fun `it should give a 200 if no timerel and no time query params are in the request`() { - buildDefaultMockResponsesForGetEntity() - val returnedExpandedEntity = mockkClass(ExpandedEntity::class, relaxed = true) coEvery { - queryService.queryTemporalEntity(any(), any()) + temporalQueryService.queryTemporalEntity(any(), any(), any()) } returns (returnedExpandedEntity to null).right() webClient.get() .uri("/ngsi-ld/v1/temporal/entities/$entityUri") .exchange() .expectStatus().isOk - - coVerify { - authorizationService.userCanReadEntity(eq(entityUri), eq(Some(MOCK_USER_SUB))) - } } @Test fun `it should raise a 400 if timerel is between and no endTimeAt provided`() { - buildDefaultMockResponsesForGetEntity() - webClient.get() .uri("/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?timerel=between&timeAt=2020-10-29T18:00:00Z") .exchange() @@ -359,8 +294,6 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { @Test fun `it should raise a 400 if time is not parsable`() { - buildDefaultMockResponsesForGetEntity() - webClient.get() .uri("/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?timerel=before&timeAt=badTime") .exchange() @@ -378,8 +311,6 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { @Test fun `it should raise a 400 if timerel is not a valid value`() { - buildDefaultMockResponsesForGetEntity() - webClient.get() .uri("/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?timerel=befor&timeAt=badTime") .exchange() @@ -397,8 +328,6 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { @Test fun `it should raise a 400 if timerel is between and endTimeAt is not parseable`() { - buildDefaultMockResponsesForGetEntity() - webClient.get() .uri( "/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?" + @@ -419,8 +348,6 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { @Test fun `it should raise a 400 if one of time bucket or aggregate is missing`() { - buildDefaultMockResponsesForGetEntity() - webClient.get() .uri( "/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?" + @@ -441,8 +368,6 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { @Test fun `it should raise a 400 if aggregate function is unknown`() { - buildDefaultMockResponsesForGetEntity() - webClient.get() .uri( "/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?" + @@ -463,8 +388,6 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { @Test fun `it should raise a 400 if aggrPeriodDuration is not in the correct format`() { - buildDefaultMockResponsesForGetEntity() - webClient.get() .uri( "/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?" + @@ -485,11 +408,9 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { @Test fun `it should return a 404 if temporal entity attribute does not exist`() { - buildDefaultMockResponsesForGetEntity() - coEvery { - queryService.queryTemporalEntity(any(), any()) - } throws ResourceNotFoundException("Entity urn:ngsi-ld:BeeHive:TESTC was not found") + temporalQueryService.queryTemporalEntity(any(), any(), any()) + } returns ResourceNotFoundException("Entity urn:ngsi-ld:BeeHive:TESTC was not found").left() webClient.get() .uri( @@ -511,11 +432,9 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { @Test fun `it should return a 200 if minimal required parameters are valid`() { - buildDefaultMockResponsesForGetEntity() - val returnedExpandedEntity = mockkClass(ExpandedEntity::class, relaxed = true) coEvery { - queryService.queryTemporalEntity(any(), any()) + temporalQueryService.queryTemporalEntity(any(), any(), any()) } returns (returnedExpandedEntity to null).right() webClient.get() @@ -528,7 +447,7 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { .expectStatus().isOk coVerify { - queryService.queryTemporalEntity( + temporalQueryService.queryTemporalEntity( eq(entityUri), match { temporalEntitiesQuery -> temporalEntitiesQuery.temporalQuery.timerel == TemporalQuery.Timerel.BETWEEN && @@ -537,60 +456,15 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { ) && !temporalEntitiesQuery.withTemporalValues && !temporalEntitiesQuery.withAudit - } - ) - } - confirmVerified(queryService) - } - - @Test - fun `it should return a 200 if minimal required parameters are valid and entity is publicly readable`() { - buildDefaultMockResponsesForGetEntity() - - val returnedExpandedEntity = mockkClass(ExpandedEntity::class, relaxed = true) - coEvery { - queryService.queryTemporalEntity(any(), any()) - } returns (returnedExpandedEntity to null).right() - - webClient.get() - .uri( - "/ngsi-ld/v1/temporal/entities/$entityUri?" + - "timerel=between&timeAt=2019-10-17T07:31:39Z&endTimeAt=2019-10-18T07:31:39Z" - ) - .header("Link", APIC_HEADER_LINK) - .exchange() - .expectStatus().isOk - - coVerify { authorizationService.userCanReadEntity(eq(entityUri), any()) } - } - - @Test - fun `it should return a 200 if minimal required parameters are valid and user can read the entity`() { - buildDefaultMockResponsesForGetEntity() - - val returnedExpandedEntity = mockkClass(ExpandedEntity::class, relaxed = true) - coEvery { - queryService.queryTemporalEntity(any(), any()) - } returns (returnedExpandedEntity to null).right() - - webClient.get() - .uri( - "/ngsi-ld/v1/temporal/entities/$entityUri?" + - "timerel=between&timeAt=2019-10-17T07:31:39Z&endTimeAt=2019-10-18T07:31:39Z" + }, + eq(sub.value) ) - .header("Link", APIC_HEADER_LINK) - .exchange() - .expectStatus().isOk - - coVerify { - authorizationService.userCanReadEntity(eq(entityUri), eq(Some(MOCK_USER_SUB))) } + confirmVerified(temporalQueryService) } @Test fun `it should return an entity with two temporal properties evolution`() = runTest { - buildDefaultMockResponsesForGetEntity() - mockWithIncomingAndOutgoingTemporalProperties(false) webClient.get() @@ -611,8 +485,6 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { @Test fun `it should return a json entity with two temporal properties evolution`() = runTest { - buildDefaultMockResponsesForGetEntity() - mockWithIncomingAndOutgoingTemporalProperties(false) webClient.get() @@ -635,8 +507,6 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { @Test fun `it should return an entity with two temporal properties evolution with temporalValues option`() = runTest { - buildDefaultMockResponsesForGetEntity() - mockWithIncomingAndOutgoingTemporalProperties(true) webClient.get() @@ -656,16 +526,6 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { } private suspend fun mockWithIncomingAndOutgoingTemporalProperties(withTemporalValues: Boolean) { - val entityTemporalProperties = listOf(INCOMING_PROPERTY, OUTGOING_PROPERTY) - .map { - TemporalEntityAttribute( - entityId = entityUri, - attributeName = it, - attributeValueType = TemporalEntityAttribute.AttributeValueType.NUMBER, - createdAt = ZonedDateTime.now(ZoneOffset.UTC), - payload = EMPTY_JSON_PAYLOAD - ) - } val entityFileName = if (withTemporalValues) "beehive_with_two_temporal_attributes_evolution_temporal_values.jsonld" else @@ -673,35 +533,12 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { val entityResponseWith2temporalEvolutions = loadAndExpandSampleData(entityFileName) coEvery { - temporalEntityAttributeService.getForEntity(any(), any(), any()) - } returns listOf(entityTemporalProperties[0], entityTemporalProperties[1]) - - val attributes = listOf(INCOMING_PROPERTY, OUTGOING_PROPERTY) - val values = listOf(Pair(1543, "2020-01-24T13:01:22.066Z"), Pair(1600, "2020-01-24T14:01:22.066Z")) - val attInstanceResults = attributes.flatMap { - values.map { - SimplifiedAttributeInstanceResult( - temporalEntityAttribute = UUID.randomUUID(), - value = it.first, - time = ZonedDateTime.parse(it.second) - ) - } - } - listOf(Pair(0, entityTemporalProperties[0]), Pair(2, entityTemporalProperties[1])).forEach { - coEvery { - attributeInstanceService.search(any(), it.second) - } returns listOf(attInstanceResults[it.first], attInstanceResults[it.first + 1]).right() - } - - coEvery { - queryService.queryTemporalEntity(any(), any()) + temporalQueryService.queryTemporalEntity(any(), any(), any()) } returns (entityResponseWith2temporalEvolutions to null).right() } @Test fun `it should raise a 400 and return an error response`() { - coEvery { authorizationService.computeAccessRightFilter(any()) } returns { null } - webClient.get() .uri("/ngsi-ld/v1/temporal/entities?type=Beehive&timerel=before") .exchange() @@ -725,9 +562,8 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { endTimeAt = ZonedDateTime.parse("2019-10-18T07:31:39Z") ) - coEvery { authorizationService.computeAccessRightFilter(any()) } returns { null } coEvery { - queryService.queryTemporalEntities(any(), any()) + temporalQueryService.queryTemporalEntities(any(), any()) } returns Either.Right(Triple(emptyList(), 2, null)) webClient.get() @@ -741,7 +577,7 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { .expectBody().json("[]") coVerify { - queryService.queryTemporalEntities( + temporalQueryService.queryTemporalEntities( match { temporalEntitiesQuery -> temporalEntitiesQuery.entitiesQuery.paginationQuery.limit == 30 && temporalEntitiesQuery.entitiesQuery.paginationQuery.offset == 0 && @@ -753,7 +589,6 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { any() ) } - confirmVerified(attributeInstanceService) } @Test @@ -761,9 +596,8 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { val firstTemporalEntity = loadAndExpandSampleData("beehive_with_two_temporal_attributes_evolution.jsonld") val secondTemporalEntity = loadAndExpandSampleData("beehive.jsonld") - coEvery { authorizationService.computeAccessRightFilter(any()) } returns { null } coEvery { - queryService.queryTemporalEntities(any(), any()) + temporalQueryService.queryTemporalEntities(any(), any()) } returns Either.Right(Triple(listOf(firstTemporalEntity, secondTemporalEntity), 2, null)) webClient.get() @@ -787,9 +621,8 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { val firstTemporalEntity = loadAndExpandSampleData("beehive_with_two_temporal_attributes_evolution.jsonld") val secondTemporalEntity = loadAndExpandSampleData("beehive.jsonld") - coEvery { authorizationService.computeAccessRightFilter(any()) } returns { null } coEvery { - queryService.queryTemporalEntities(any(), any()) + temporalQueryService.queryTemporalEntities(any(), any()) } returns Either.Right(Triple(listOf(firstTemporalEntity, secondTemporalEntity), 2, null)) webClient.get() @@ -815,9 +648,8 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { val firstTemporalEntity = loadAndExpandSampleData("beehive_with_two_temporal_attributes_evolution.jsonld") val secondTemporalEntity = loadAndExpandSampleData("beehive.jsonld") - coEvery { authorizationService.computeAccessRightFilter(any()) } returns { null } coEvery { - queryService.queryTemporalEntities(any(), any()) + temporalQueryService.queryTemporalEntities(any(), any()) } returns Either.Right(Triple(listOf(firstTemporalEntity, secondTemporalEntity), 2, null)) webClient.get() @@ -839,8 +671,9 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { @Test fun `query temporal entity should return 200 and empty response if requested offset does not exist`() { - coEvery { authorizationService.computeAccessRightFilter(any()) } returns { null } - coEvery { queryService.queryTemporalEntities(any(), any()) } returns Either.Right(Triple(emptyList(), 2, null)) + coEvery { + temporalQueryService.queryTemporalEntities(any(), any()) + } returns Either.Right(Triple(emptyList(), 2, null)) webClient.get() .uri( @@ -855,8 +688,9 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { @Test fun `query temporal entities should return 200 and the number of results if count is asked for`() { - coEvery { authorizationService.computeAccessRightFilter(any()) } returns { null } - coEvery { queryService.queryTemporalEntities(any(), any()) } returns Either.Right(Triple(emptyList(), 2, null)) + coEvery { + temporalQueryService.queryTemporalEntities(any(), any()) + } returns Either.Right(Triple(emptyList(), 2, null)) webClient.get() .uri( @@ -875,9 +709,8 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { val firstTemporalEntity = loadAndExpandSampleData("beehive_with_two_temporal_attributes_evolution.jsonld") val secondTemporalEntity = loadAndExpandSampleData("beehive.jsonld") - coEvery { authorizationService.computeAccessRightFilter(any()) } returns { null } coEvery { - queryService.queryTemporalEntities(any(), any()) + temporalQueryService.queryTemporalEntities(any(), any()) } returns Either.Right(Triple(listOf(firstTemporalEntity, secondTemporalEntity), 2, null)) webClient.get() @@ -902,9 +735,8 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { val firstTemporalEntity = loadAndExpandSampleData("beehive_with_two_temporal_attributes_evolution.jsonld") val secondTemporalEntity = loadAndExpandSampleData("beehive.jsonld") - coEvery { authorizationService.computeAccessRightFilter(any()) } returns { null } coEvery { - queryService.queryTemporalEntities(any(), any()) + temporalQueryService.queryTemporalEntities(any(), any()) } returns Either.Right(Triple(listOf(firstTemporalEntity, secondTemporalEntity), 3, null)) webClient.get() @@ -929,9 +761,8 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { @Test fun `query temporal entity should return 400 if requested offset is less than zero`() { - coEvery { authorizationService.computeAccessRightFilter(any()) } returns { null } coEvery { - queryService.queryTemporalEntities(any(), any()) + temporalQueryService.queryTemporalEntities(any(), any()) } throws BadRequestDataException( "Offset must be greater than zero and limit must be strictly greater than zero" ) @@ -957,9 +788,8 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { @Test fun `query temporal entity should return 400 if limit is equal or less than zero`() { - coEvery { authorizationService.computeAccessRightFilter(any()) } returns { null } coEvery { - queryService.queryTemporalEntities(any(), any()) + temporalQueryService.queryTemporalEntities(any(), any()) } throws BadRequestDataException( "Offset must be greater than zero and limit must be strictly greater than zero" ) @@ -985,9 +815,8 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { @Test fun `query temporal entity should return 403 if limit is greater than the maximum authorized limit`() { - coEvery { authorizationService.computeAccessRightFilter(any()) } returns { null } coEvery { - queryService.queryTemporalEntities(any(), any()) + temporalQueryService.queryTemporalEntities(any(), any()) } throws TooManyResultsException( "You asked for 200 results, but the supported maximum limit is 100" ) @@ -1016,9 +845,7 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { val instanceTemporalFragment = loadSampleData("fragments/temporal_instance_fragment.jsonld") - coEvery { entityPayloadService.checkEntityExistence(any()) } returns Unit.right() - coEvery { authorizationService.userCanUpdateEntity(any(), any()) } returns Unit.right() - coEvery { attributeInstanceService.modifyAttributeInstance(any(), any(), any(), any()) } returns Unit.right() + coEvery { temporalService.modifyAttributeInstance(any(), any(), any(), any()) } returns Unit.right() webClient.patch() .uri("/ngsi-ld/v1/temporal/entities/$entityUri/attrs/$TEMPERATURE_COMPACT_PROPERTY/$attributeInstanceId") @@ -1029,14 +856,12 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { .expectStatus().isNoContent .expectBody().isEmpty - coVerify { entityPayloadService.checkEntityExistence(entityUri) } - coVerify { authorizationService.userCanUpdateEntity(entityUri, sub) } coVerify { - attributeInstanceService.modifyAttributeInstance( + temporalService.modifyAttributeInstance( entityUri, - TEMPERATURE_PROPERTY, attributeInstanceId, - any() + any(), + eq(sub.value) ) } } @@ -1046,9 +871,8 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { val instanceTemporalFragment = loadSampleData("fragments/temporal_instance_fragment.jsonld") - coEvery { entityPayloadService.checkEntityExistence(any()) } returns Unit.right() coEvery { - authorizationService.userCanUpdateEntity(any(), sub) + temporalService.modifyAttributeInstance(any(), any(), any(), sub.getOrNull()) } returns AccessDeniedException("User forbidden write access to entity $entityUri").left() webClient.patch() @@ -1067,9 +891,6 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { } """.trimIndent() ) - - coVerify { entityPayloadService.checkEntityExistence(entityUri) } - coVerify { authorizationService.userCanUpdateEntity(entityUri, sub) } } @Test @@ -1078,7 +899,7 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { loadSampleData("fragments/temporal_instance_fragment.jsonld") coEvery { - entityPayloadService.checkEntityExistence(any()) + temporalService.modifyAttributeInstance(any(), any(), any(), sub.getOrNull()) } returns ResourceNotFoundException(entityNotFoundMessage(entityUri.toString())).left() webClient.patch() @@ -1097,19 +918,16 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { } """.trimIndent() ) - coVerify { entityPayloadService.checkEntityExistence(entityUri) } } @Test fun `modify attribute instance should return a 404 if attributeInstanceId or attribute name is not found`() { val instanceTemporalFragment = loadSampleData("fragments/temporal_instance_fragment.jsonld") - val expandedAttr = JsonLdUtils.expandJsonLdTerm(temporalEntityAttributeName, NGSILD_TEST_CORE_CONTEXT) + val expandedAttr = JsonLdUtils.expandJsonLdTerm(attributeName, NGSILD_TEST_CORE_CONTEXT) - coEvery { entityPayloadService.checkEntityExistence(any()) } returns Unit.right() - coEvery { authorizationService.userCanUpdateEntity(any(), sub) } returns Unit.right() coEvery { - attributeInstanceService.modifyAttributeInstance(any(), any(), any(), any()) + temporalService.modifyAttributeInstance(any(), any(), any(), sub.getOrNull()) } returns ResourceNotFoundException( attributeOrInstanceNotFoundMessage(expandedAttr, attributeInstanceId.toString()) ).left() @@ -1130,17 +948,11 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { } """.trimIndent() ) - - coVerify { entityPayloadService.checkEntityExistence(entityUri) } - coVerify { authorizationService.userCanUpdateEntity(entityUri, sub) } } @Test fun `delete temporal entity should return a 204 if an entity has been successfully deleted`() { - coEvery { entityPayloadService.checkEntityExistence(entityUri) } returns Unit.right() - coEvery { authorizationService.userCanAdminEntity(entityUri, sub) } returns Unit.right() - coEvery { entityPayloadService.deleteEntity(any()) } returns mockkClass(EntityPayload::class).right() - coEvery { authorizationService.removeRightsOnEntity(any()) } returns Unit.right() + coEvery { entityService.deleteEntity(any(), any()) } returns Unit.right() webClient.delete() .uri("/ngsi-ld/v1/temporal/entities/$entityUri") @@ -1149,17 +961,14 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { .expectBody().isEmpty coVerify { - entityPayloadService.checkEntityExistence(entityUri) - authorizationService.userCanAdminEntity(eq(entityUri), eq(sub)) - entityPayloadService.deleteEntity(eq(entityUri)) - authorizationService.removeRightsOnEntity(eq(entityUri)) + entityService.deleteEntity(eq(entityUri), eq(sub.value)) } } @Test fun `delete temporal entity should return a 404 if entity to be deleted has not been found`() { coEvery { - entityPayloadService.checkEntityExistence(entityUri) + entityService.deleteEntity(entityUri, sub.getOrNull()) } returns ResourceNotFoundException(entityNotFoundMessage(entityUri.toString())).left() webClient.delete() @@ -1204,10 +1013,8 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { @Test fun `delete temporal entity should return a 500 if entity could not be deleted`() { - coEvery { entityPayloadService.checkEntityExistence(entityUri) } returns Unit.right() - coEvery { authorizationService.userCanAdminEntity(entityUri, sub) } returns Unit.right() coEvery { - entityPayloadService.deleteEntity(any()) + entityService.deleteEntity(any(), any()) } throws RuntimeException("Unexpected server error") webClient.delete() @@ -1227,9 +1034,8 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { @Test fun `delete temporal entity should return a 403 is user is not authorized to delete an entity`() { - coEvery { entityPayloadService.checkEntityExistence(entityUri) } returns Unit.right() coEvery { - authorizationService.userCanAdminEntity(entityUri, sub) + entityService.deleteEntity(any(), any()) } returns AccessDeniedException("User forbidden admin access to entity $entityUri").left() webClient.delete() @@ -1250,10 +1056,8 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { @Test fun `delete attribute temporal should return a 204 if the attribute has been successfully deleted`() { - coEvery { temporalEntityAttributeService.checkEntityAndAttributeExistence(any(), any()) } returns Unit.right() - coEvery { authorizationService.userCanUpdateEntity(any(), sub) } returns Unit.right() coEvery { - entityPayloadService.deleteAttribute(any(), any(), any()) + entityService.deleteAttribute(any(), any(), any(), any(), any()) } returns Unit.right() webClient.method(HttpMethod.DELETE) @@ -1265,22 +1069,20 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { .expectBody().isEmpty coVerify { - temporalEntityAttributeService.checkEntityAndAttributeExistence(eq(entityUri), eq(TEMPERATURE_PROPERTY)) - authorizationService.userCanUpdateEntity(eq(entityUri), eq(sub)) - entityPayloadService.deleteAttribute( + entityService.deleteAttribute( eq(entityUri), eq(TEMPERATURE_PROPERTY), - null + null, + eq(false), + eq(sub.value) ) } } @Test fun `delete attribute temporal should delete all instances if deleteAll flag is true`() { - coEvery { temporalEntityAttributeService.checkEntityAndAttributeExistence(any(), any()) } returns Unit.right() - coEvery { authorizationService.userCanUpdateEntity(any(), sub) } returns Unit.right() coEvery { - entityPayloadService.deleteAttribute(any(), any(), any(), any()) + entityService.deleteAttribute(any(), any(), any(), any(), any()) } returns Unit.right() webClient.method(HttpMethod.DELETE) @@ -1292,11 +1094,12 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { .expectBody().isEmpty coVerify { - entityPayloadService.deleteAttribute( + entityService.deleteAttribute( eq(entityUri), eq(TEMPERATURE_PROPERTY), null, - eq(true) + eq(true), + eq(sub.value) ) } } @@ -1304,13 +1107,8 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { @Test fun `delete attribute temporal should delete instance with the provided datasetId`() { val datasetId = "urn:ngsi-ld:Dataset:temperature:1" - coEvery { temporalEntityAttributeService.checkEntityAndAttributeExistence(any(), any()) } returns Unit.right() - coEvery { authorizationService.userCanUpdateEntity(any(), sub) } returns Unit.right() coEvery { - temporalEntityAttributeService.checkEntityAndAttributeExistence(any(), any(), any()) - } returns Unit.right() - coEvery { - entityPayloadService.deleteAttribute(any(), any(), any()) + entityService.deleteAttribute(any(), any(), any(), any(), any()) } returns Unit.right() webClient.method(HttpMethod.DELETE) @@ -1322,10 +1120,12 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { .expectBody().isEmpty coVerify { - entityPayloadService.deleteAttribute( + entityService.deleteAttribute( eq(entityUri), eq(TEMPERATURE_PROPERTY), - eq(datasetId.toUri()) + eq(datasetId.toUri()), + eq(false), + eq(sub.value) ) } } @@ -1333,7 +1133,7 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { @Test fun `delete attribute temporal should return a 404 if the entity is not found`() { coEvery { - temporalEntityAttributeService.checkEntityAndAttributeExistence(any(), any()) + entityService.deleteAttribute(any(), any(), any(), any(), any()) } returns ResourceNotFoundException(entityNotFoundMessage(entityUri.toString())).left() webClient.method(HttpMethod.DELETE) @@ -1355,11 +1155,9 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { @Test fun `delete attribute temporal should return a 404 if the attribute is not found`() { - coEvery { temporalEntityAttributeService.checkEntityAndAttributeExistence(any(), any()) } returns Unit.right() - coEvery { authorizationService.userCanUpdateEntity(any(), sub) } returns Unit.right() coEvery { - entityPayloadService.deleteAttribute(any(), any(), any(), any()) - } throws ResourceNotFoundException("Attribute Not Found") + entityService.deleteAttribute(any(), any(), any(), any(), any()) + } returns ResourceNotFoundException("Attribute Not Found").left() webClient.method(HttpMethod.DELETE) .uri("/ngsi-ld/v1/temporal/entities/$entityUri/attrs/$TEMPERATURE_COMPACT_PROPERTY?deleteAll=true") @@ -1380,10 +1178,8 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { @Test fun `delete attribute temporal should return a 400 if the request is not correct`() { - coEvery { temporalEntityAttributeService.checkEntityAndAttributeExistence(any(), any()) } returns Unit.right() - coEvery { authorizationService.userCanUpdateEntity(any(), sub) } returns Unit.right() coEvery { - entityPayloadService.deleteAttribute(any(), any(), any()) + entityService.deleteAttribute(any(), any(), any(), any(), any()) } returns BadRequestDataException("Something is wrong with the request").left() webClient.method(HttpMethod.DELETE) @@ -1456,10 +1252,8 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { @Test fun `delete attribute temporal should return a 403 if user is not allowed to update entity`() { - coEvery { temporalEntityAttributeService.checkEntityAndAttributeExistence(any(), any()) } returns Unit.right() - coEvery { entityPayloadService.getTypes(any()) } returns listOf(BEEHIVE_TYPE).right() coEvery { - authorizationService.userCanUpdateEntity(any(), sub) + entityService.deleteAttribute(any(), any(), any(), any(), any()) } returns AccessDeniedException("User forbidden write access to entity $entityUri").left() webClient.method(HttpMethod.DELETE) @@ -1481,42 +1275,33 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { @Test fun `delete attribute instance temporal should return 204`() { - val expandedAttr = JsonLdUtils.expandJsonLdTerm(temporalEntityAttributeName, NGSILD_TEST_CORE_CONTEXT) - coEvery { - entityPayloadService.checkEntityExistence(any()) - } returns Unit.right() - coEvery { entityPayloadService.getTypes(any()) } returns listOf(BEEHIVE_TYPE).right() - coEvery { authorizationService.userCanUpdateEntity(any(), any()) } returns Unit.right() + val expandedAttr = JsonLdUtils.expandJsonLdTerm(attributeName, NGSILD_TEST_CORE_CONTEXT) coEvery { - attributeInstanceService.deleteInstance(any(), any(), any()) + temporalService.deleteAttributeInstance(any(), any(), any(), any()) } returns Unit.right() webClient .method(HttpMethod.DELETE) - .uri("/ngsi-ld/v1/temporal/entities/$entityUri/attrs/$temporalEntityAttributeName/$attributeInstanceId") + .uri("/ngsi-ld/v1/temporal/entities/$entityUri/attrs/$attributeName/$attributeInstanceId") .header("Link", APIC_HEADER_LINK) .exchange() .expectStatus().isNoContent .expectBody().isEmpty - coVerify { entityPayloadService.checkEntityExistence(entityUri) } coVerify { - attributeInstanceService.deleteInstance(entityUri, expandedAttr, attributeInstanceId) - } - coVerify { - authorizationService.userCanUpdateEntity(eq(entityUri), eq(Some(MOCK_USER_SUB))) + temporalService.deleteAttributeInstance(entityUri, expandedAttr, attributeInstanceId, sub.value) } } @Test fun `delete attribute instance temporal should return 404 if entityId is not found`() { coEvery { - entityPayloadService.checkEntityExistence(any()) + temporalService.deleteAttributeInstance(any(), any(), any(), any()) } returns ResourceNotFoundException(entityNotFoundMessage(entityUri.toString())).left() webClient .method(HttpMethod.DELETE) - .uri("/ngsi-ld/v1/temporal/entities/$entityUri/attrs/$temporalEntityAttributeName/$attributeInstanceId") + .uri("/ngsi-ld/v1/temporal/entities/$entityUri/attrs/$attributeName/$attributeInstanceId") .header("Link", APIC_HEADER_LINK) .exchange() .expectStatus().isNotFound @@ -1531,29 +1316,28 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { ) coVerify { - entityPayloadService.checkEntityExistence(entityUri) + temporalService.deleteAttributeInstance( + entityUri, + NGSILD_DEFAULT_VOCAB + attributeName, + attributeInstanceId, + sub.value + ) } - confirmVerified(entityPayloadService) } @Test fun `delete attribute instance temporal should return 404 if attributeInstanceId or attribute name is not found`() { - val expandedAttr = JsonLdUtils.expandJsonLdTerm(temporalEntityAttributeName, NGSILD_TEST_CORE_CONTEXT) + val expandedAttr = JsonLdUtils.expandJsonLdTerm(attributeName, NGSILD_TEST_CORE_CONTEXT) coEvery { - entityPayloadService.checkEntityExistence(any()) - } returns Unit.right() - coEvery { entityPayloadService.getTypes(any()) } returns listOf(BEEHIVE_TYPE).right() - coEvery { authorizationService.userCanUpdateEntity(any(), any()) } returns Unit.right() - coEvery { - attributeInstanceService.deleteInstance(any(), any(), any()) + temporalService.deleteAttributeInstance(any(), any(), any(), any()) } returns ResourceNotFoundException( attributeOrInstanceNotFoundMessage(expandedAttr, attributeInstanceId.toString()) ).left() webClient .method(HttpMethod.DELETE) - .uri("/ngsi-ld/v1/temporal/entities/$entityUri/attrs/$temporalEntityAttributeName/$attributeInstanceId") + .uri("/ngsi-ld/v1/temporal/entities/$entityUri/attrs/$attributeName/$attributeInstanceId") .header("Link", APIC_HEADER_LINK) .exchange() .expectStatus().isNotFound @@ -1567,31 +1351,20 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { """.trimIndent() ) - coVerify { entityPayloadService.checkEntityExistence(entityUri) } coVerify { - attributeInstanceService.deleteInstance(entityUri, expandedAttr, attributeInstanceId) - } - coVerify { - authorizationService.userCanUpdateEntity( - eq(entityUri), - eq(Some(MOCK_USER_SUB)) - ) + temporalService.deleteAttributeInstance(entityUri, expandedAttr, attributeInstanceId, sub.value) } } @Test fun `delete attribute instance temporal should return 403 if user is not allowed`() { coEvery { - entityPayloadService.checkEntityExistence(any()) - } returns Unit.right() - coEvery { entityPayloadService.getTypes(any()) } returns listOf(BEEHIVE_TYPE).right() - coEvery { - authorizationService.userCanUpdateEntity(any(), any()) + temporalService.deleteAttributeInstance(any(), any(), any(), any()) } returns AccessDeniedException("User forbidden write access to entity $entityUri").left() webClient .method(HttpMethod.DELETE) - .uri("/ngsi-ld/v1/temporal/entities/$entityUri/attrs/$temporalEntityAttributeName/$attributeInstanceId") + .uri("/ngsi-ld/v1/temporal/entities/$entityUri/attrs/$attributeName/$attributeInstanceId") .header("Link", APIC_HEADER_LINK) .exchange() .expectStatus().isForbidden @@ -1604,14 +1377,6 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { } """.trimIndent() ) - - coVerify { entityPayloadService.checkEntityExistence(entityUri) } - coVerify { - authorizationService.userCanUpdateEntity( - eq(entityUri), - eq(Some(MOCK_USER_SUB)) - ) - } } @Test diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityOperationsHandlerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityOperationsHandlerTests.kt similarity index 87% rename from search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityOperationsHandlerTests.kt rename to search-service/src/test/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityOperationsHandlerTests.kt index efcbfd000..a128f2794 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityOperationsHandlerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityOperationsHandlerTests.kt @@ -1,11 +1,10 @@ -package com.egm.stellio.search.web +package com.egm.stellio.search.temporal.web import arrow.core.Either -import com.egm.stellio.search.authorization.AuthorizationService -import com.egm.stellio.search.config.SearchProperties -import com.egm.stellio.search.model.TemporalQuery -import com.egm.stellio.search.service.QueryService +import com.egm.stellio.search.common.config.SearchProperties import com.egm.stellio.search.support.buildDefaultTestTemporalQuery +import com.egm.stellio.search.temporal.model.TemporalQuery +import com.egm.stellio.search.temporal.service.TemporalQueryService import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.util.* import com.ninjasquad.springmockk.MockkBean @@ -32,10 +31,7 @@ class TemporalEntityOperationsHandlerTests { private lateinit var webClient: WebTestClient @MockkBean(relaxed = true) - private lateinit var queryService: QueryService - - @MockkBean - private lateinit var authorizationService: AuthorizationService + private lateinit var temporalQueryService: TemporalQueryService @BeforeAll fun configureWebClientDefaults() { @@ -57,8 +53,9 @@ class TemporalEntityOperationsHandlerTests { endTimeAt = ZonedDateTime.parse("2019-10-18T07:31:39Z") ) - coEvery { authorizationService.computeAccessRightFilter(any()) } returns { null } - coEvery { queryService.queryTemporalEntities(any(), any()) } returns Either.Right(Triple(emptyList(), 2, null)) + coEvery { + temporalQueryService.queryTemporalEntities(any(), any()) + } returns Either.Right(Triple(emptyList(), 2, null)) val query = """ { @@ -83,7 +80,7 @@ class TemporalEntityOperationsHandlerTests { .expectStatus().isOk coVerify { - queryService.queryTemporalEntities( + temporalQueryService.queryTemporalEntities( match { temporalEntitiesQuery -> temporalEntitiesQuery.entitiesQuery.paginationQuery.limit == 30 && temporalEntitiesQuery.entitiesQuery.paginationQuery.offset == 0 && @@ -106,8 +103,9 @@ class TemporalEntityOperationsHandlerTests { endTimeAt = ZonedDateTime.parse("2019-10-18T07:31:39Z") ) - coEvery { authorizationService.computeAccessRightFilter(any()) } returns { null } - coEvery { queryService.queryTemporalEntities(any(), any()) } returns Either.Right(Triple(emptyList(), 2, null)) + coEvery { + temporalQueryService.queryTemporalEntities(any(), any()) + } returns Either.Right(Triple(emptyList(), 2, null)) val query = """ { @@ -133,7 +131,7 @@ class TemporalEntityOperationsHandlerTests { .expectHeader().valueEquals(RESULTS_COUNT_HEADER, "2") coVerify { - queryService.queryTemporalEntities( + temporalQueryService.queryTemporalEntities( match { temporalEntitiesQuery -> temporalEntitiesQuery.entitiesQuery.paginationQuery.limit == 30 && temporalEntitiesQuery.entitiesQuery.paginationQuery.offset == 0 && @@ -148,7 +146,7 @@ class TemporalEntityOperationsHandlerTests { ) } - confirmVerified(queryService) + confirmVerified(temporalQueryService) } @Test diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityOperationHandlerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityOperationHandlerTests.kt deleted file mode 100644 index 1ee376b73..000000000 --- a/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityOperationHandlerTests.kt +++ /dev/null @@ -1,1088 +0,0 @@ -package com.egm.stellio.search.web - -import arrow.core.left -import arrow.core.right -import com.egm.stellio.search.authorization.AuthorizationService -import com.egm.stellio.search.config.SearchProperties -import com.egm.stellio.search.model.EMPTY_UPDATE_RESULT -import com.egm.stellio.search.model.EntityPayload -import com.egm.stellio.search.model.UpdateResult -import com.egm.stellio.search.service.EntityOperationService -import com.egm.stellio.search.service.EntityPayloadService -import com.egm.stellio.search.service.QueryService -import com.egm.stellio.shared.config.ApplicationProperties -import com.egm.stellio.shared.model.AccessDeniedException -import com.egm.stellio.shared.model.ExpandedEntity -import com.egm.stellio.shared.model.NgsiLdEntity -import com.egm.stellio.shared.util.* -import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_DEFAULT_VOCAB -import com.ninjasquad.springmockk.MockkBean -import io.mockk.* -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.Test -import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.context.properties.EnableConfigurationProperties -import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient -import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest -import org.springframework.core.io.ClassPathResource -import org.springframework.http.HttpStatus -import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf -import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockJwt -import org.springframework.test.context.ActiveProfiles -import org.springframework.test.web.reactive.server.WebTestClient -import java.net.URI - -@AutoConfigureWebTestClient -@ActiveProfiles("test") -@WebFluxTest(EntityOperationHandler::class) -@EnableConfigurationProperties(ApplicationProperties::class, SearchProperties::class) -class EntityOperationHandlerTests { - - @Autowired - private lateinit var webClient: WebTestClient - - @MockkBean - private lateinit var entityOperationService: EntityOperationService - - @MockkBean - private lateinit var entityPayloadService: EntityPayloadService - - @MockkBean - private lateinit var queryService: QueryService - - @MockkBean - private lateinit var authorizationService: AuthorizationService - - private val logger = LoggerFactory.getLogger(javaClass) - - private lateinit var mockedTemperatureSensorEntity: NgsiLdEntity - private lateinit var mockedDissolvedOxygenSensorEntity: NgsiLdEntity - private lateinit var mockedDeviceEntity: NgsiLdEntity - private lateinit var mockedTemperatureSensorExpandedEntity: ExpandedEntity - private lateinit var mockedDissolvedOxygenSensorExpandedEntity: ExpandedEntity - private lateinit var mockedDeviceExpandedEntity: ExpandedEntity - - @BeforeAll - fun configureWebClientDefaults() { - webClient = webClient.mutate() - .apply(mockJwt().jwt { it.subject(MOCK_USER_SUB) }) - .apply(csrf()) - .defaultHeaders { - it.accept = listOf(JSON_LD_MEDIA_TYPE) - it.contentType = JSON_LD_MEDIA_TYPE - } - .entityExchangeResultConsumer { - logger.warn(String(it.responseBody as? ByteArray ?: "Empty response body".toByteArray())) - } - .build() - - mockedTemperatureSensorEntity = mockkClass(NgsiLdEntity::class) { - every { id } returns temperatureSensorUri - every { types } returns listOf(SENSOR_TYPE) - } - mockedTemperatureSensorExpandedEntity = mockkClass(ExpandedEntity::class) { - every { id } returns temperatureSensorUri.toString() - every { members } returns emptyMap() - } - mockedDissolvedOxygenSensorEntity = mockkClass(NgsiLdEntity::class) { - every { id } returns dissolvedOxygenSensorUri - every { types } returns listOf(SENSOR_TYPE) - } - mockedDissolvedOxygenSensorExpandedEntity = mockkClass(ExpandedEntity::class) { - every { id } returns dissolvedOxygenSensorUri.toString() - every { members } returns emptyMap() - } - mockedDeviceEntity = mockkClass(NgsiLdEntity::class) { - every { id } returns deviceUri - every { types } returns listOf(DEVICE_TYPE) - } - mockedDeviceExpandedEntity = mockkClass(ExpandedEntity::class) { - every { id } returns deviceUri.toString() - every { members } returns emptyMap() - } - } - - private val temperatureSensorUri = "urn:ngsi-ld:Sensor:HCMR-AQUABOX1temperature".toUri() - private val dissolvedOxygenSensorUri = "urn:ngsi-ld:Sensor:HCMR-AQUABOX1dissolvedOxygen".toUri() - private val deviceUri = "urn:ngsi-ld:Device:HCMR-AQUABOX1".toUri() - private val allEntitiesUris = listOf(temperatureSensorUri, dissolvedOxygenSensorUri, deviceUri) - - private val batchCreateEndpoint = "/ngsi-ld/v1/entityOperations/create" - private val batchUpsertEndpoint = "/ngsi-ld/v1/entityOperations/upsert" - private val batchUpsertWithUpdateEndpoint = "/ngsi-ld/v1/entityOperations/upsert?options=update" - private val batchUpdateEndpoint = "/ngsi-ld/v1/entityOperations/update" - private val batchUpdateEndpointWithNoOverwriteOption = "/ngsi-ld/v1/entityOperations/update?options=noOverwrite" - private val batchDeleteEndpoint = "/ngsi-ld/v1/entityOperations/delete" - private val queryEntitiesEndpoint = "/ngsi-ld/v1/entityOperations/query" - private val batchMergeEndpoint = "/ngsi-ld/v1/entityOperations/merge" - - private val validJsonFile = ClassPathResource("/ngsild/hcmr/HCMR_test_file.json") - private val twoEntityOneInvalidJsonLDFile = ClassPathResource("/ngsild/two_sensors_one_invalid.jsonld") - private val missingContextJsonFile = ClassPathResource("/ngsild/hcmr/HCMR_test_file_missing_context.json") - private val oneEntityMissingContextJsonFile = - ClassPathResource("/ngsild/hcmr/HCMR_test_file_one_entity_missing_context.json") - private val deleteAllJsonFile = ClassPathResource("/ngsild/hcmr/HCMR_test_delete_all_entities.json") - - @Test - fun `update batch entity should return a 204 if JSON-LD payload is correct`() = runTest { - val jsonLdFile = validJsonFile - - coEvery { entityOperationService.splitEntitiesByExistence(any()) } returns Pair( - listOf( - mockedTemperatureSensorExpandedEntity to mockedTemperatureSensorEntity, - mockedDissolvedOxygenSensorExpandedEntity to mockedDissolvedOxygenSensorEntity, - mockedDeviceExpandedEntity to mockedDeviceEntity - ), - emptyList() - ) - coEvery { authorizationService.userCanUpdateEntity(any(), sub) } returns Unit.right() - coEvery { - entityOperationService.update(any(), any(), any()) - } returns BatchOperationResult(success = mutableListOf(), errors = mutableListOf()) - - webClient.post() - .uri(batchUpdateEndpoint) - .bodyValue(jsonLdFile) - .exchange() - .expectStatus().isNoContent - .expectBody().isEmpty - - coVerify { - entityOperationService.update(any(), false, eq("60AAEBA3-C0C7-42B6-8CB0-0D30857F210E")) - } - } - - @Test - fun `update batch entity should return a 207 if JSON-LD payload contains update errors`() = runTest { - val jsonLdFile = validJsonFile - val errors = arrayListOf( - BatchEntityError(temperatureSensorUri, arrayListOf("Update unexpectedly failed.")), - BatchEntityError(dissolvedOxygenSensorUri, arrayListOf("Update unexpectedly failed.")) - ) - - coEvery { entityOperationService.splitEntitiesByExistence(any()) } returns Pair( - listOf( - Pair(mockedTemperatureSensorExpandedEntity, mockedTemperatureSensorEntity), - Pair(mockedDissolvedOxygenSensorExpandedEntity, mockedDissolvedOxygenSensorEntity), - ), - emptyList() - ) - coEvery { authorizationService.userCanUpdateEntity(any(), sub) } returns Unit.right() - coEvery { entityOperationService.update(any(), any(), any()) } returns BatchOperationResult( - mutableListOf(), - errors - ) - - webClient.post() - .uri(batchUpdateEndpoint) - .bodyValue(jsonLdFile) - .exchange() - .expectStatus().isEqualTo(HttpStatus.MULTI_STATUS) - .expectBody().json( - """ - { - "errors": [ - { - "entityId": "urn:ngsi-ld:Sensor:HCMR-AQUABOX1temperature", - "error": [ "Update unexpectedly failed." ] - }, - { - "entityId": "urn:ngsi-ld:Sensor:HCMR-AQUABOX1dissolvedOxygen", - "error": [ "Update unexpectedly failed." ] - } - ], - "success": [] - } - """.trimIndent() - ) - } - - @Test - fun `update batch entity should return a 207 if one entity is an invalid NGSI-LD payload`() = runTest { - val jsonLdFile = twoEntityOneInvalidJsonLDFile - - coEvery { entityOperationService.splitEntitiesByExistence(any()) } returns Pair( - listOf(Pair(mockedTemperatureSensorExpandedEntity, mockedTemperatureSensorEntity)), - emptyList() - ) - coEvery { authorizationService.userCanUpdateEntity(any(), sub) } returns Unit.right() - coEvery { entityOperationService.update(any(), any(), any()) } returns BatchOperationResult( - mutableListOf(BatchEntitySuccess(temperatureSensorUri, EMPTY_UPDATE_RESULT)), - mutableListOf() - ) - - webClient.post() - .uri(batchUpdateEndpoint) - .bodyValue(jsonLdFile) - .exchange() - .expectStatus().isEqualTo(HttpStatus.MULTI_STATUS) - .expectBody().json( - """ - { - "errors": [ - { - "entityId": "urn:ngsi-ld:Sensor:HCMR-AQUABOX2temperature", - "error": [ "Unable to expand input payload" ] - } - ], - "success": [ - "urn:ngsi-ld:Sensor:HCMR-AQUABOX1temperature" - ] - } - """.trimIndent() - ) - } - - @Test - fun `update batch entity should return a 400 if JSON-LD payload is not correct`() { - shouldReturn400WithBadPayload("update") - } - - @Test - fun `update batch entity should return 204 if JSON-LD payload is correct and noOverwrite is asked`() = runTest { - val jsonLdFile = validJsonFile - - coEvery { entityOperationService.splitEntitiesByExistence(any()) } returns Pair( - listOf( - Pair(mockedTemperatureSensorExpandedEntity, mockedTemperatureSensorEntity), - Pair(mockedDissolvedOxygenSensorExpandedEntity, mockedDissolvedOxygenSensorEntity), - ), - emptyList() - ) - coEvery { authorizationService.userCanUpdateEntity(any(), sub) } returns Unit.right() - coEvery { - entityOperationService.update(any(), any(), any()) - } returns BatchOperationResult(success = mutableListOf(), errors = mutableListOf()) - - webClient.post() - .uri(batchUpdateEndpointWithNoOverwriteOption) - .bodyValue(jsonLdFile) - .exchange() - .expectStatus().isNoContent - .expectBody().isEmpty - - coVerify { - entityOperationService.update(any(), true, eq("60AAEBA3-C0C7-42B6-8CB0-0D30857F210E")) - } - } - - @Test - fun `update batch entity should return 207 if there is a non existing entity in the payload`() = runTest { - val jsonLdFile = validJsonFile - - coEvery { entityOperationService.splitEntitiesByExistence(any()) } returns Pair( - listOf(Pair(mockedTemperatureSensorExpandedEntity, mockedTemperatureSensorEntity)), - listOf(Pair(mockedDeviceExpandedEntity, mockedDeviceEntity)) - ) - coEvery { authorizationService.userCanUpdateEntity(any(), sub) } returns Unit.right() - coEvery { entityOperationService.update(any(), any(), any()) } returns BatchOperationResult( - success = mutableListOf(BatchEntitySuccess(temperatureSensorUri, mockkClass(UpdateResult::class))), - errors = mutableListOf() - ) - - webClient.post() - .uri(batchUpdateEndpoint) - .bodyValue(jsonLdFile) - .exchange() - .expectStatus().isEqualTo(HttpStatus.MULTI_STATUS) - .expectBody().json( - """ - { - "errors": [ - { - "entityId": "urn:ngsi-ld:Device:HCMR-AQUABOX1", - "error": [ "Entity does not exist" ] - } - ], - "success": [ "urn:ngsi-ld:Sensor:HCMR-AQUABOX1temperature" ] - } - """.trimIndent() - ) - } - - @Test - fun `create batch entity should return a 201 if JSON-LD payload is correct`() = runTest { - val jsonLdFile = validJsonFile - val capturedExpandedEntities = slot>() - - coEvery { - entityOperationService.splitEntitiesByUniqueness(capture(capturedExpandedEntities)) - } answers { Pair(capturedExpandedEntities.captured, emptyList()) } - coEvery { - entityOperationService.splitEntitiesByExistence(capture(capturedExpandedEntities)) - } answers { Pair(emptyList(), capturedExpandedEntities.captured) } - coEvery { authorizationService.userCanCreateEntities(sub) } returns Unit.right() - coEvery { entityOperationService.create(any(), any()) } returns BatchOperationResult( - allEntitiesUris.map { BatchEntitySuccess(it) }.toMutableList(), - arrayListOf() - ) - coEvery { authorizationService.createOwnerRights(any(), eq(sub)) } returns Unit.right() - - webClient.post() - .uri(batchCreateEndpoint) - .bodyValue(jsonLdFile) - .exchange() - .expectStatus().isCreated - .expectBody() - .jsonPath("$").isArray - .jsonPath("$[*]").isEqualTo(allEntitiesUris.map { it.toString() }) - - assertEquals(allEntitiesUris, capturedExpandedEntities.captured.map { it.entityId() }) - - coVerify { authorizationService.createOwnerRights(allEntitiesUris, sub) } - } - - @Test - fun `create batch entity should return a 207 when some entities already exist`() = runTest { - val jsonLdFile = validJsonFile - val capturedExpandedEntities = slot>() - val createdEntitiesIds = arrayListOf(dissolvedOxygenSensorUri, deviceUri) - - coEvery { - entityOperationService.splitEntitiesByUniqueness(capture(capturedExpandedEntities)) - } answers { - Pair(capturedExpandedEntities.captured, emptyList()) - } - coEvery { - entityOperationService.splitEntitiesByExistence(any()) - } answers { - Pair( - listOf(Pair(mockedTemperatureSensorExpandedEntity, mockedTemperatureSensorEntity)), - listOf( - Pair(mockedDissolvedOxygenSensorExpandedEntity, mockedDissolvedOxygenSensorEntity), - Pair(mockedDeviceExpandedEntity, mockedDeviceEntity) - ) - ) - } - coEvery { authorizationService.userCanCreateEntities(sub) } returns Unit.right() - coEvery { entityOperationService.create(any(), any()) } returns BatchOperationResult( - createdEntitiesIds.map { BatchEntitySuccess(it) }.toMutableList(), - arrayListOf() - ) - coEvery { authorizationService.createOwnerRights(any(), eq(sub)) } returns Unit.right() - - webClient.post() - .uri(batchCreateEndpoint) - .bodyValue(jsonLdFile) - .exchange() - .expectStatus().isEqualTo(HttpStatus.MULTI_STATUS) - .expectBody().json( - """ - { - "errors": [ - { - "entityId": "urn:ngsi-ld:Sensor:HCMR-AQUABOX1temperature", - "error": [ "Entity already exists" ] - } - ], - "success": [ - "urn:ngsi-ld:Sensor:HCMR-AQUABOX1dissolvedOxygen", - "urn:ngsi-ld:Device:HCMR-AQUABOX1" - ] - } - """.trimIndent() - ) - - coVerify { authorizationService.createOwnerRights(createdEntitiesIds, sub) } - } - - @Test - fun `create batch entity should return a 207 when some entities have the same id`() = runTest { - val jsonLdFile = validJsonFile - val capturedExpandedEntities = slot>() - val createdEntitiesIds = arrayListOf(dissolvedOxygenSensorUri, deviceUri) - - coEvery { - entityOperationService.splitEntitiesByUniqueness(any()) - } answers { - Pair( - listOf( - Pair(mockedTemperatureSensorExpandedEntity, mockedTemperatureSensorEntity), - Pair(mockedDeviceExpandedEntity, mockedDeviceEntity) - ), - listOf(Pair(mockedDissolvedOxygenSensorExpandedEntity, mockedDissolvedOxygenSensorEntity)) - ) - } - coEvery { - entityOperationService.splitEntitiesByExistence(capture(capturedExpandedEntities)) - } answers { Pair(emptyList(), capturedExpandedEntities.captured) } - coEvery { authorizationService.userCanCreateEntities(sub) } returns Unit.right() - coEvery { entityOperationService.create(any(), any()) } returns BatchOperationResult( - createdEntitiesIds.map { BatchEntitySuccess(it) }.toMutableList(), - arrayListOf() - ) - coEvery { authorizationService.createOwnerRights(any(), eq(sub)) } returns Unit.right() - - webClient.post() - .uri(batchCreateEndpoint) - .bodyValue(jsonLdFile) - .exchange() - .expectStatus().isEqualTo(HttpStatus.MULTI_STATUS) - .expectBody().json( - """ - { - "errors": [ - { - "entityId": "urn:ngsi-ld:Sensor:HCMR-AQUABOX1dissolvedOxygen", - "error": [ "Entity already exists" ] - } - ], - "success": [ - "urn:ngsi-ld:Sensor:HCMR-AQUABOX1dissolvedOxygen", - "urn:ngsi-ld:Device:HCMR-AQUABOX1" - ] - } - """.trimIndent() - ) - } - - @Test - fun `create batch entity should not authorize user without creator role`() = runTest { - val jsonLdFile = validJsonFile - - coEvery { - entityOperationService.splitEntitiesByUniqueness(any()) - } returns Pair(listOf(Pair(mockedDeviceExpandedEntity, mockedDeviceEntity)), emptyList()) - coEvery { - entityOperationService.splitEntitiesByExistence(any()) - } returns Pair(emptyList(), listOf(Pair(mockedDeviceExpandedEntity, mockedDeviceEntity))) - coEvery { - authorizationService.userCanCreateEntities(sub) - } returns AccessDeniedException(ENTITIY_CREATION_FORBIDDEN_MESSAGE).left() - - webClient.post() - .uri(batchCreateEndpoint) - .bodyValue(jsonLdFile) - .exchange() - .expectStatus().isEqualTo(HttpStatus.MULTI_STATUS) - .expectBody().json( - """ - { - "success": [], - "errors": [ - { - "entityId":"urn:ngsi-ld:Device:HCMR-AQUABOX1", - "error":["User forbidden to create entity"] - } - ] - } - """.trimIndent() - ) - } - - @Test - fun `create batch entity should return a 400 if JSON-LD payload is not correct`() = runTest { - shouldReturn400WithBadPayload("create") - } - - @Test - fun `create batch entity should return a 400 if one JSON-LD entity misses a context`() = runTest { - val jsonLdFile = oneEntityMissingContextJsonFile - webClient.post() - .uri(batchCreateEndpoint) - .bodyValue(jsonLdFile) - .exchange() - .expectStatus().isBadRequest - .expectBody().json( - """ - { - "type":"https://uri.etsi.org/ngsi-ld/errors/BadRequestData", - "title":"The request includes input data which does not meet the requirements of the operation", - "detail": - "Request payload must contain @context term for a request having an application/ld+json content type" - } - """.trimIndent() - ) - } - - @Test - fun `upsert batch entity should return a 201 if JSON-LD payload is correct`() = runTest { - val jsonLdFile = validJsonFile - val capturedExpandedEntities = slot>() - val createdEntitiesIds = arrayListOf(temperatureSensorUri) - val updatedEntitiesIds = arrayListOf(dissolvedOxygenSensorUri, deviceUri) - val createdBatchResult = BatchOperationResult( - createdEntitiesIds.map { BatchEntitySuccess(it) }.toMutableList(), - arrayListOf() - ) - val updatedBatchResult = BatchOperationResult( - updatedEntitiesIds.map { BatchEntitySuccess(it, mockkClass(UpdateResult::class)) }.toMutableList() - ) - - coEvery { - entityOperationService.splitEntitiesByUniqueness(capture(capturedExpandedEntities)) - } answers { Pair(capturedExpandedEntities.captured, emptyList()) } - coEvery { entityOperationService.splitEntitiesByExistence(any()) } returns Pair( - listOf( - Pair(mockedDissolvedOxygenSensorExpandedEntity, mockedDissolvedOxygenSensorEntity), - Pair(mockedDeviceExpandedEntity, mockedDeviceEntity) - ), - listOf(Pair(mockedTemperatureSensorExpandedEntity, mockedTemperatureSensorEntity)) - ) - coEvery { authorizationService.userCanCreateEntities(any()) } returns Unit.right() - coEvery { entityOperationService.create(any(), any()) } returns createdBatchResult - coEvery { authorizationService.createOwnerRights(any(), eq(sub)) } returns Unit.right() - - coEvery { authorizationService.userCanUpdateEntity(any(), sub) } returns Unit.right() - coEvery { entityOperationService.update(any(), any(), any()) } returns updatedBatchResult - - webClient.post() - .uri(batchUpsertWithUpdateEndpoint) - .bodyValue(jsonLdFile).exchange() - .expectStatus().isCreated - .expectBody() - .jsonPath("$").isArray - .jsonPath("$[*]").isEqualTo(createdEntitiesIds.map { it.toString() }) - - coVerify { authorizationService.createOwnerRights(createdEntitiesIds, sub) } - } - - @Test - fun `upsert batch entity should return a 204 if it has only updated existing entities`() = runTest { - val jsonLdFile = validJsonFile - val capturedExpandedEntities = slot>() - - coEvery { - entityOperationService.splitEntitiesByUniqueness(capture(capturedExpandedEntities)) - } answers { - Pair(capturedExpandedEntities.captured, emptyList()) - } - coEvery { entityOperationService.splitEntitiesByExistence(any()) } returns Pair( - listOf(Pair(mockedTemperatureSensorExpandedEntity, mockedTemperatureSensorEntity)), - emptyList() - ) - coEvery { authorizationService.userCanUpdateEntity(any(), sub) } returns Unit.right() - coEvery { - entityOperationService.update(any(), any(), any()) - } returns BatchOperationResult(success = mutableListOf(), errors = mutableListOf()) - - webClient.post() - .uri(batchUpsertWithUpdateEndpoint) - .bodyValue(jsonLdFile) - .exchange() - .expectStatus().isNoContent - .expectBody().isEmpty - - coVerify { entityOperationService.replace(any(), any()) wasNot Called } - coVerify { entityOperationService.update(any(), false, sub.getOrNull()) } - } - - @Test - fun `upsert batch entity should return a 207 if JSON-LD payload contains update errors`() = runTest { - val jsonLdFile = validJsonFile - val capturedExpandedEntities = slot>() - val errors = arrayListOf( - BatchEntityError(temperatureSensorUri, arrayListOf("Update unexpectedly failed.")), - BatchEntityError(dissolvedOxygenSensorUri, arrayListOf("Update unexpectedly failed.")) - ) - - coEvery { entityOperationService.splitEntitiesByUniqueness(capture(capturedExpandedEntities)) } answers { - Pair(capturedExpandedEntities.captured, emptyList()) - } - coEvery { entityOperationService.splitEntitiesByExistence(any()) } returns Pair( - listOf( - Pair(mockedTemperatureSensorExpandedEntity, mockedTemperatureSensorEntity), - Pair(mockedDissolvedOxygenSensorExpandedEntity, mockedDissolvedOxygenSensorEntity) - ), - listOf(Pair(mockedDeviceExpandedEntity, mockedDeviceEntity)) - ) - coEvery { authorizationService.userCanCreateEntities(sub) } returns Unit.right() - coEvery { entityOperationService.create(any(), any()) } returns BatchOperationResult( - arrayListOf(BatchEntitySuccess(deviceUri, mockkClass(UpdateResult::class))), - arrayListOf() - ) - coEvery { authorizationService.createOwnerRights(any(), eq(sub)) } returns Unit.right() - - coEvery { authorizationService.userCanUpdateEntity(any(), sub) } returns Unit.right() - coEvery { entityOperationService.update(any(), any(), any()) } returns BatchOperationResult( - arrayListOf(), - errors - ) - - webClient.post() - .uri(batchUpsertWithUpdateEndpoint) - .bodyValue(jsonLdFile) - .exchange() - .expectStatus().isEqualTo(HttpStatus.MULTI_STATUS) - .expectBody().json( - """ - { - "errors": [ - { - "entityId": "urn:ngsi-ld:Sensor:HCMR-AQUABOX1temperature", - "error": [ "Update unexpectedly failed." ] - }, - { - "entityId": "urn:ngsi-ld:Sensor:HCMR-AQUABOX1dissolvedOxygen", - "error": [ "Update unexpectedly failed." ] - } - ], - "success": [ "urn:ngsi-ld:Device:HCMR-AQUABOX1" ] - } - """.trimIndent() - ) - - coVerify { authorizationService.createOwnerRights(listOf(deviceUri), sub) } - } - - @Test - fun `upsert batch entity without option should replace existing entities`() = runTest { - val jsonLdFile = validJsonFile - val capturedExpandedEntities = slot>() - val entitiesIds = arrayListOf(temperatureSensorUri, dissolvedOxygenSensorUri) - - coEvery { entityOperationService.splitEntitiesByUniqueness(capture(capturedExpandedEntities)) } answers { - Pair(capturedExpandedEntities.captured, emptyList()) - } - coEvery { entityOperationService.splitEntitiesByExistence(any()) } returns Pair( - listOf( - Pair(mockedTemperatureSensorExpandedEntity, mockedTemperatureSensorEntity), - Pair(mockedDissolvedOxygenSensorExpandedEntity, mockedDissolvedOxygenSensorEntity) - ), - emptyList() - ) - coEvery { authorizationService.userCanUpdateEntity(any(), sub) } returns Unit.right() - coEvery { entityOperationService.replace(any(), any()) } returns BatchOperationResult( - entitiesIds.map { BatchEntitySuccess(it) }.toMutableList(), - arrayListOf() - ) - - webClient.post() - .uri(batchUpsertEndpoint) - .bodyValue(jsonLdFile) - .exchange() - .expectStatus().isNoContent - - coVerify { entityOperationService.create(any(), any()) wasNot Called } - coVerify { entityOperationService.replace(any(), sub.getOrNull()) } - coVerify { entityOperationService.update(any(), any(), any()) wasNot Called } - } - - @Test - fun `upsert batch should not authorize user to create entities without creator role`() = runTest { - val jsonLdFile = validJsonFile - val capturedExpandedEntities = slot>() - - coEvery { - entityOperationService.splitEntitiesByUniqueness(capture(capturedExpandedEntities)) - } answers { - Pair(capturedExpandedEntities.captured, emptyList()) - } - coEvery { entityOperationService.splitEntitiesByExistence(any()) } returns Pair( - emptyList(), - listOf(Pair(mockedTemperatureSensorExpandedEntity, mockedTemperatureSensorEntity)) - ) - coEvery { - authorizationService.userCanCreateEntities(sub) - } returns AccessDeniedException(ENTITIY_CREATION_FORBIDDEN_MESSAGE).left() - - webClient.post() - .uri(batchUpsertEndpoint) - .bodyValue(jsonLdFile) - .exchange() - .expectStatus().isEqualTo(HttpStatus.MULTI_STATUS) - .expectBody().json( - """ - { - "errors": [ - { - "entityId": "urn:ngsi-ld:Sensor:HCMR-AQUABOX1temperature", - "error": [ "User forbidden to create entity" ] - } - ], - "success": [] - } - """.trimIndent() - ) - - coVerify { entityOperationService.replace(any(), any()) wasNot Called } - } - - @Test - fun `upsert batch should not authorize user to updates entities without write right`() = runTest { - val jsonLdFile = validJsonFile - val capturedExpandedEntities = slot>() - - coEvery { - entityOperationService.splitEntitiesByUniqueness(capture(capturedExpandedEntities)) - } answers { - Pair(capturedExpandedEntities.captured, emptyList()) - } - coEvery { entityOperationService.splitEntitiesByExistence(any()) } returns Pair( - listOf( - Pair(mockedTemperatureSensorExpandedEntity, mockedTemperatureSensorEntity), - Pair(mockedDissolvedOxygenSensorExpandedEntity, mockedDissolvedOxygenSensorEntity) - ), - emptyList() - ) - coEvery { - authorizationService.userCanUpdateEntity(match { it == temperatureSensorUri }, sub) - } returns Unit.right() - coEvery { - authorizationService.userCanUpdateEntity(match { it == dissolvedOxygenSensorUri }, sub) - } returns AccessDeniedException(ENTITY_UPDATE_FORBIDDEN_MESSAGE).left() - coEvery { entityOperationService.replace(any(), any()) } returns BatchOperationResult( - mutableListOf(BatchEntitySuccess(temperatureSensorUri)), - arrayListOf() - ) - - webClient.post() - .uri(batchUpsertEndpoint) - .bodyValue(jsonLdFile) - .exchange() - .expectStatus().isEqualTo(HttpStatus.MULTI_STATUS) - .expectBody().json( - """ - { - "errors": [ - { - "entityId": "urn:ngsi-ld:Sensor:HCMR-AQUABOX1dissolvedOxygen", - "error": [ "User forbidden to modify entity" ] - } - ], - "success": [ "urn:ngsi-ld:Sensor:HCMR-AQUABOX1temperature" ] - } - """.trimIndent() - ) - - coVerify { entityOperationService.replace(any(), sub.getOrNull()) } - } - - @Test - fun `upsert batch entity should return a 400 if JSON-LD payload is not correct`() { - shouldReturn400WithBadPayload("upsert") - } - - private fun shouldReturn400WithBadPayload(method: String) { - val jsonLdFile = missingContextJsonFile - webClient.post() - .uri("/ngsi-ld/v1/entityOperations/$method") - .bodyValue(jsonLdFile) - .exchange() - .expectStatus().isBadRequest - .expectBody().json( - """ - { - "type":"https://uri.etsi.org/ngsi-ld/errors/BadRequestData", - "title":"The request includes input data which does not meet the requirements of the operation", - "detail": - "Request payload must contain @context term for a request having an application/ld+json content type" - } - """.trimIndent() - ) - } - - @Test - fun `delete batch for correct entities should return a 204`() = runTest { - val jsonLdFile = deleteAllJsonFile - - coEvery { entityOperationService.splitEntitiesIdsByUniqueness(any()) } answers { - Pair(allEntitiesUris, emptyList()) - } - coEvery { entityOperationService.splitEntitiesIdsByExistence(any()) } answers { - Pair(allEntitiesUris, emptyList()) - } - coEvery { authorizationService.userCanAdminEntity(any(), any()) } returns Unit.right() - coEvery { entityOperationService.delete(any(), any()) } returns - BatchOperationResult( - allEntitiesUris.map { BatchEntitySuccess(it) }.toMutableList(), - mutableListOf() - ) - - coEvery { entityPayloadService.retrieve(any>()) } returns - listOf( - mockkClass(EntityPayload::class, relaxed = true) { - every { entityId } returns dissolvedOxygenSensorUri - every { types } returns listOf(SENSOR_TYPE) - }, - mockkClass(EntityPayload::class, relaxed = true) { - every { entityId } returns temperatureSensorUri - every { types } returns listOf(SENSOR_TYPE) - }, - mockkClass(EntityPayload::class, relaxed = true) { - every { entityId } returns deviceUri - every { types } returns listOf(DEVICE_TYPE) - } - ) - - webClient.post() - .uri(batchDeleteEndpoint) - .bodyValue(jsonLdFile) - .exchange() - .expectStatus().isNoContent - } - - @Test - fun `delete batch for unknown entities should return a 207 with explicit error messages`() = runTest { - coEvery { entityOperationService.splitEntitiesIdsByUniqueness(any()) } answers { - Pair(allEntitiesUris, emptyList()) - } - coEvery { entityOperationService.splitEntitiesIdsByExistence(any()) } answers { - Pair(emptyList(), allEntitiesUris) - } - coEvery { authorizationService.userCanAdminEntity(any(), any()) } answers { Unit.right() } - - performBatchDeleteAndCheck207Response(ENTITY_DOES_NOT_EXIST_MESSAGE) - - coVerify { entityOperationService.splitEntitiesIdsByExistence(allEntitiesUris) } - coVerify { entityPayloadService wasNot called } - coVerify { entityOperationService.delete(any(), any()) wasNot Called } - } - - @Test - fun `delete batch for duplicate entities should return a 207 with explicit error messages`() = runTest { - coEvery { entityOperationService.splitEntitiesIdsByUniqueness(any()) } answers { - Pair(listOf(temperatureSensorUri, dissolvedOxygenSensorUri), listOf(deviceUri)) - } - coEvery { entityOperationService.splitEntitiesIdsByExistence(any()) } answers { - Pair(emptyList(), listOf(temperatureSensorUri, dissolvedOxygenSensorUri)) - } - coEvery { authorizationService.userCanAdminEntity(any(), any()) } answers { Unit.right() } - - performBatchDeleteAndCheck207Response(ENTITY_DOES_NOT_EXIST_MESSAGE) - - coVerify { entityOperationService.splitEntitiesIdsByUniqueness(allEntitiesUris) } - } - - @Test - fun `delete batch for unauthorized entities should return a 207 with explicit error messages`() = runTest { - coEvery { entityOperationService.splitEntitiesIdsByUniqueness(any()) } answers { - Pair(allEntitiesUris, emptyList()) - } - coEvery { entityOperationService.splitEntitiesIdsByExistence(any()) } answers { - Pair(allEntitiesUris, emptyList()) - } - coEvery { entityPayloadService.retrieve(any>()) } returns - listOf( - mockkClass(EntityPayload::class, relaxed = true) { - every { entityId } returns dissolvedOxygenSensorUri - every { types } returns listOf(SENSOR_TYPE) - }, - mockkClass(EntityPayload::class, relaxed = true) { - every { entityId } returns temperatureSensorUri - every { types } returns listOf(SENSOR_TYPE) - }, - mockkClass(EntityPayload::class, relaxed = true) { - every { entityId } returns deviceUri - every { types } returns listOf(DEVICE_TYPE) - } - ) - coEvery { - authorizationService.userCanAdminEntity(any(), any()) - } returns AccessDeniedException(ENTITY_DELETE_FORBIDDEN_MESSAGE).left() - - performBatchDeleteAndCheck207Response(ENTITY_DELETE_FORBIDDEN_MESSAGE) - - coVerify { entityOperationService.delete(any(), any()) wasNot Called } - } - - private fun performBatchDeleteAndCheck207Response(expectedErrorMessage: String) { - val jsonLdFile = deleteAllJsonFile - webClient.post() - .uri(batchDeleteEndpoint) - .bodyValue(jsonLdFile) - .exchange() - .expectStatus().isEqualTo(HttpStatus.MULTI_STATUS) - .expectBody().json( - """ - { - "success": [], - "errors": [ - {"entityId":"urn:ngsi-ld:Sensor:HCMR-AQUABOX1temperature", - "error":["$expectedErrorMessage"]}, - {"entityId":"urn:ngsi-ld:Sensor:HCMR-AQUABOX1dissolvedOxygen", - "error":["$expectedErrorMessage"]}, - {"entityId":"urn:ngsi-ld:Device:HCMR-AQUABOX1", - "error":["$expectedErrorMessage"]} - ] - } - """.trimIndent() - ) - } - - @Test - fun `query entities should return a 200 if the query is correct`() = runTest { - coEvery { authorizationService.computeAccessRightFilter(any()) } returns { null } - coEvery { queryService.queryEntities(any(), any()) } returns Pair(emptyList(), 0).right() - - val query = """ - { - "type": "Query", - "entities": [{ - "type": "$BEEHIVE_TYPE" - }], - "attrs": ["attr1", "attr2"] - } - """.trimIndent() - - webClient.post() - .uri("$queryEntitiesEndpoint?limit=10&offset=20") - .bodyValue(query) - .exchange() - .expectStatus().isOk - - coVerify { - queryService.queryEntities( - match { - it.paginationQuery.limit == 10 && - it.paginationQuery.offset == 20 && - it.typeSelection == BEEHIVE_TYPE && - it.attrs == setOf("${NGSILD_DEFAULT_VOCAB}attr1", "${NGSILD_DEFAULT_VOCAB}attr2") - }, - any() - ) - } - } - - @Test - fun `merge batch entity should return a 204 if JSON-LD payload is correct`() = runTest { - val jsonLdFile = validJsonFile - - coEvery { entityOperationService.splitEntitiesByExistence(any()) } returns ( - listOf( - mockedTemperatureSensorExpandedEntity to mockedTemperatureSensorEntity, - mockedDissolvedOxygenSensorExpandedEntity to mockedDissolvedOxygenSensorEntity, - mockedDeviceExpandedEntity to mockedDeviceEntity, - ) to emptyList() - ) - - coEvery { authorizationService.userCanUpdateEntity(any(), sub) } returns Unit.right() - coEvery { - entityOperationService.merge(any(), any()) - } returns BatchOperationResult(success = mutableListOf(), errors = mutableListOf()) - - webClient.post() - .uri(batchMergeEndpoint) - .bodyValue(jsonLdFile) - .exchange() - .expectStatus().isNoContent - .expectBody().isEmpty - - coVerify { - entityOperationService.merge(any(), eq("60AAEBA3-C0C7-42B6-8CB0-0D30857F210E")) - } - } - - @Test - fun `merge batch entity should return a 207 if JSON-LD payload contains update errors`() = runTest { - val jsonLdFile = validJsonFile - val errors = arrayListOf( - BatchEntityError(temperatureSensorUri, arrayListOf("Update unexpectedly failed.")), - BatchEntityError(dissolvedOxygenSensorUri, arrayListOf("Update unexpectedly failed.")) - ) - - coEvery { entityOperationService.splitEntitiesByExistence(any()) } returns ( - listOf( - mockedTemperatureSensorExpandedEntity to mockedTemperatureSensorEntity, - mockedDissolvedOxygenSensorExpandedEntity to mockedDissolvedOxygenSensorEntity, - ) to - emptyList() - ) - coEvery { authorizationService.userCanUpdateEntity(any(), sub) } returns Unit.right() - coEvery { entityOperationService.merge(any(), any()) } returns BatchOperationResult( - mutableListOf(), - errors - ) - - webClient.post() - .uri(batchMergeEndpoint) - .bodyValue(jsonLdFile) - .exchange() - .expectStatus().isEqualTo(HttpStatus.MULTI_STATUS) - .expectBody().json( - """ - { - "errors": [ - { - "entityId": "urn:ngsi-ld:Sensor:HCMR-AQUABOX1temperature", - "error": [ "Update unexpectedly failed." ] - }, - { - "entityId": "urn:ngsi-ld:Sensor:HCMR-AQUABOX1dissolvedOxygen", - "error": [ "Update unexpectedly failed." ] - } - ], - "success": [] - } - """.trimIndent() - ) - } - - @Test - fun `merge batch entity should return a 207 if one entity is an invalid NGSI-LD payload`() = runTest { - val jsonLdFile = twoEntityOneInvalidJsonLDFile - - coEvery { entityOperationService.splitEntitiesByExistence(any()) } returns ( - listOf(mockedTemperatureSensorExpandedEntity to mockedTemperatureSensorEntity) to emptyList() - ) - coEvery { authorizationService.userCanUpdateEntity(any(), sub) } returns Unit.right() - coEvery { entityOperationService.merge(any(), any()) } returns BatchOperationResult( - mutableListOf(BatchEntitySuccess(temperatureSensorUri, EMPTY_UPDATE_RESULT)), - mutableListOf() - ) - - webClient.post() - .uri(batchMergeEndpoint) - .bodyValue(jsonLdFile) - .exchange() - .expectStatus().isEqualTo(HttpStatus.MULTI_STATUS) - .expectBody().json( - """ - { - "errors": [ - { - "entityId": "urn:ngsi-ld:Sensor:HCMR-AQUABOX2temperature", - "error": [ "Unable to expand input payload" ] - } - ], - "success": [ - "urn:ngsi-ld:Sensor:HCMR-AQUABOX1temperature" - ] - } - """.trimIndent() - ) - } - - @Test - fun `merge batch entity should return a 400 if JSON-LD payload is not correct`() { - shouldReturn400WithBadPayload("merge") - } - - @Test - fun `merge batch entity should return 207 if there is a non existing entity in the payload`() = runTest { - val jsonLdFile = validJsonFile - - coEvery { entityOperationService.splitEntitiesByExistence(any()) } returns ( - listOf(mockedTemperatureSensorExpandedEntity to mockedTemperatureSensorEntity) - to - listOf(mockedDeviceExpandedEntity to mockedDeviceEntity) - ) - coEvery { authorizationService.userCanUpdateEntity(any(), sub) } returns Unit.right() - coEvery { entityOperationService.merge(any(), any()) } returns BatchOperationResult( - success = mutableListOf(BatchEntitySuccess(temperatureSensorUri, mockkClass(UpdateResult::class))), - errors = mutableListOf() - ) - - webClient.post() - .uri(batchMergeEndpoint) - .bodyValue(jsonLdFile) - .exchange() - .expectStatus().isEqualTo(HttpStatus.MULTI_STATUS) - .expectBody().json( - """ - { - "errors": [ - { - "entityId": "urn:ngsi-ld:Device:HCMR-AQUABOX1", - "error": [ "Entity does not exist" ] - } - ], - "success": [ "urn:ngsi-ld:Sensor:HCMR-AQUABOX1temperature" ] - } - """.trimIndent() - ) - } -}