Skip to content

Commit

Permalink
feat(core): handle more than one instance of the same Entity in an En…
Browse files Browse the repository at this point in the history
…tity array - 5.5.11
  • Loading branch information
bobeal committed Jan 20, 2024
1 parent 9479a67 commit 8d6dafc
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 20 deletions.
1 change: 1 addition & 0 deletions search-service/config/detekt/baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<ID>Filename:V0_29__JsonLd_migration.kt$db.migration.V0_29__JsonLd_migration.kt</ID>
<ID>LongMethod:AttributeInstanceService.kt$AttributeInstanceService$@Transactional suspend fun create(attributeInstance: AttributeInstance): Either&lt;APIException, Unit&gt;</ID>
<ID>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&lt;String&gt; ): ResponseEntity&lt;*&gt;</ID>
<ID>LongMethod:EntityOperationHandlerTests.kt$EntityOperationHandlerTests$@Test fun `create batch entity should return a 207 when some entities already exist`()</ID>
<ID>LongMethod:QueryServiceTests.kt$QueryServiceTests$@Test fun `it should query temporal entities as requested by query params`()</ID>
<ID>LongMethod:QueryServiceTests.kt$QueryServiceTests$@Test fun `it should return an empty list for an attribute if it has no temporal values`()</ID>
<ID>LongMethod:TemporalEntityBuilderTests.kt$TemporalEntityBuilderTests$@Test fun `it should return a temporal entity with values aggregated`()</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,28 @@ class EntityOperationService(
return entities.partition { existingEntitiesIds.contains(extractIdFunc.invoke(it)) }
}

fun splitEntitiesByUniqueness(
entities: List<JsonLdNgsiLdEntity>
): Pair<List<JsonLdNgsiLdEntity>, List<JsonLdNgsiLdEntity>> {
val extractIdFunc: (JsonLdNgsiLdEntity) -> URI = { it.entityId() }
return splitEntitiesByUniquenessGeneric(entities, extractIdFunc)
}

fun splitEntitiesIdsByUniqueness(entityIds: List<URI>): Pair<List<URI>, List<URI>> {
val identityFunc: (URI) -> URI = { it }
return splitEntitiesByUniquenessGeneric(entityIds, identityFunc)
}

fun <T> splitEntitiesByUniquenessGeneric(
entities: List<T>,
extractIdFunc: (T) -> URI
): Pair<List<T>, List<T>> =
entities.fold(Pair(emptyList(), emptyList())) { acc, current ->
if (acc.first.any { extractIdFunc(it) == extractIdFunc(current) })
Pair(acc.first, acc.second.plus(current))
else Pair(acc.first.plus(current), acc.second)
}

/**
* Creates a batch of [entities].
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,18 @@ class EntityOperationHandler(
checkBatchRequestBody(body).bind()
checkContentType(httpHeaders, body).bind()
val context = getContextFromLinkHeader(httpHeaders.getOrEmpty(HttpHeaders.LINK)).bind()

val (parsedEntities, unparsableEntities) =
expandAndPrepareBatchOfEntities(body, context, httpHeaders.contentType).bind()
val (existingEntities, newEntities) = entityOperationService.splitEntitiesByExistence(parsedEntities)
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)
}
Expand Down Expand Up @@ -109,10 +113,16 @@ class EntityOperationHandler(
addEntitiesToErrors(newUnauthorizedEntities.extractNgsiLdEntities(), ENTITIY_CREATION_FORBIDDEN_MESSAGE)
}

doBatchCreation(newAuthorizedEntities, batchOperationResult, sub)
val (newUniqueEntities, duplicatedEntities) =
entityOperationService.splitEntitiesByUniqueness(newAuthorizedEntities)
val existingOrDuplicatedEntities = existingEntities.plus(duplicatedEntities)

doBatchCreation(newUniqueEntities, batchOperationResult, sub)

val (existingEntitiesUnauthorized, existingEntitiesAuthorized) =
existingEntities.partition { authorizationService.userCanUpdateEntity(it.entityId(), sub).isLeft() }
existingOrDuplicatedEntities.partition {
authorizationService.userCanUpdateEntity(it.entityId(), sub).isLeft()
}
batchOperationResult.addEntitiesToErrors(
existingEntitiesUnauthorized.extractNgsiLdEntities(),
ENTITY_UPDATE_FORBIDDEN_MESSAGE
Expand All @@ -133,8 +143,8 @@ class EntityOperationHandler(
batchOperationResult.success.addAll(updateOperationResult.success)
}

if (batchOperationResult.errors.isEmpty() && newEntities.isNotEmpty())
ResponseEntity.status(HttpStatus.CREATED).body(newEntities.map { it.entityId() })
if (batchOperationResult.errors.isEmpty() && newUniqueEntities.isNotEmpty())
ResponseEntity.status(HttpStatus.CREATED).body(newUniqueEntities.map { it.entityId() })
else if (batchOperationResult.errors.isEmpty())
ResponseEntity.status(HttpStatus.NO_CONTENT).build<String>()
else
Expand Down Expand Up @@ -165,14 +175,14 @@ class EntityOperationHandler(

val (parsedEntities, unparsableEntities) =
expandAndPrepareBatchOfEntities(body, context, httpHeaders.contentType).bind()
val (existingEntities, newEntities) = entityOperationService.splitEntitiesByExistence(parsedEntities)
val (existingEntities, unknownEntities) = entityOperationService.splitEntitiesByExistence(parsedEntities)

val (existingEntitiesUnauthorized, existingEntitiesAuthorized) =
existingEntities.partition { authorizationService.userCanUpdateEntity(it.entityId(), sub).isLeft() }

val batchOperationResult = BatchOperationResult().apply {
addEntitiesToErrors(unparsableEntities)
addEntitiesToErrors(newEntities.extractNgsiLdEntities(), ENTITY_DOES_NOT_EXIST_MESSAGE)
addEntitiesToErrors(unknownEntities.extractNgsiLdEntities(), ENTITY_DOES_NOT_EXIST_MESSAGE)
addEntitiesToErrors(existingEntitiesUnauthorized.extractNgsiLdEntities(), ENTITY_UPDATE_FORBIDDEN_MESSAGE)
}

Expand All @@ -186,7 +196,7 @@ class EntityOperationHandler(
batchOperationResult.success.addAll(updateOperationResult.success)
}

if (batchOperationResult.errors.isEmpty() && newEntities.isEmpty())
if (batchOperationResult.errors.isEmpty() && unknownEntities.isEmpty())
ResponseEntity.status(HttpStatus.NO_CONTENT).build<String>()
else
ResponseEntity.status(HttpStatus.MULTI_STATUS).body(batchOperationResult)
Expand All @@ -205,27 +215,30 @@ class EntityOperationHandler(
val body = requestBody.awaitFirst()
checkBatchRequestBody(body).bind()

val (uniqueEntitiesId, duplicateEntitiesId) =
entityOperationService.splitEntitiesIdsByUniqueness(body.toListOfUri())

val (existingEntities, unknownEntities) =
entityOperationService.splitEntitiesIdsByExistence(body.toListOfUri())
entityOperationService.splitEntitiesIdsByExistence(uniqueEntitiesId)

val (entitiesUserCannotDelete, entitiesUserCanDelete) =
existingEntities.partition {
authorizationService.userCanAdminEntity(it, sub).isLeft()
}

val entitiesIdsToDelete = existingEntities.toSet()
val entitiesBeforeDelete =
if (entitiesIdsToDelete.isNotEmpty())
entityPayloadService.retrieve(entitiesIdsToDelete.toList())
if (entitiesUserCanDelete.isNotEmpty())
entityPayloadService.retrieve(entitiesUserCanDelete.toList())
else emptyList()

val (entitiesUserCannotAdmin, entitiesUserCanAdmin) =
entitiesBeforeDelete.partition {
authorizationService.userCanAdminEntity(it.entityId, sub).isLeft()
}

val batchOperationResult = BatchOperationResult().apply {
addIdsToErrors(duplicateEntitiesId, ENTITY_DOES_NOT_EXIST_MESSAGE)
addIdsToErrors(unknownEntities, ENTITY_DOES_NOT_EXIST_MESSAGE)
addIdsToErrors(entitiesUserCannotAdmin.map { it.entityId }, ENTITY_DELETE_FORBIDDEN_MESSAGE)
addIdsToErrors(entitiesUserCannotDelete, ENTITY_DELETE_FORBIDDEN_MESSAGE)
}

if (entitiesUserCanAdmin.isNotEmpty()) {
val deleteOperationResult = entityOperationService.delete(entitiesUserCanAdmin.map { it.entityId }.toSet())
if (entitiesUserCanDelete.isNotEmpty()) {
val deleteOperationResult = entityOperationService.delete(entitiesUserCanDelete.toSet())

deleteOperationResult.success.map { it.entityId }.forEach { uri ->
val entity = entitiesBeforeDelete.find { it.entityId == uri }!!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,29 @@ class EntityOperationServiceTests {
assertEquals(listOf(secondEntityURI), doNotExist)
}

@Test
fun `it should split entities per uniqueness`() = runTest {
val (unique, duplicates) = entityOperationService.splitEntitiesByUniqueness(
listOf(
Pair(firstExpandedEntity, firstEntity),
Pair(secondExpandedEntity, secondEntity),
Pair(firstExpandedEntity, firstEntity),
)
)

assertEquals(listOf(Pair(firstExpandedEntity, firstEntity), Pair(secondExpandedEntity, secondEntity)), unique)
assertEquals(listOf(Pair(firstExpandedEntity, firstEntity)), duplicates)
}

@Test
fun `it should split entities per uniqueness with ids`() = runTest {
val (unique, duplicates) =
entityOperationService.splitEntitiesIdsByUniqueness(listOf(firstEntityURI, secondEntityURI, firstEntityURI))

assertEquals(listOf(firstEntityURI, secondEntityURI), unique)
assertEquals(listOf(firstEntityURI), duplicates)
}

@Test
fun `it should ask to create all provided entities`() = runTest {
coEvery { entityPayloadService.createEntity(any<NgsiLdEntity>(), any(), any()) } returns Unit.right()
Expand Down
Loading

0 comments on commit 8d6dafc

Please sign in to comment.