1
0

create relation with holder- and contact-data, and search for contact emailAddress + relation mark (#136)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/136
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
Michael Hoennig
2024-12-13 14:09:01 +01:00
parent 19fac6b5e1
commit 20fa27194b
18 changed files with 354 additions and 119 deletions

View File

@@ -13,8 +13,16 @@ public class Validate {
return new Validate(variableNames);
}
public final void atMaxOneNonNull(final Object var1, final Object var2) {
public final void atMaxOne(final Object var1, final Object var2) {
if (var1 != null && var2 != null) {
throw new ValidationException(
"At maximum one of (" + variableNames + ") must be non-null, " +
"but are (" + var1 + ", " + var2 + ")");
}
}
public final void exactlyOne(final Object var1, final Object var2) {
if ((var1 != null) == (var2 != null)) {
throw new ValidationException(
"Exactly one of (" + variableNames + ") must be non-null, " +
"but are (" + var1 + ", " + var2 + ")");

View File

@@ -17,6 +17,7 @@ import java.util.List;
import java.util.UUID;
import java.util.function.BiConsumer;
import static net.hostsharing.hsadminng.errors.Validate.validate;
import static net.hostsharing.hsadminng.mapper.KeyValueMap.from;
@RestController
@@ -38,10 +39,14 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
public ResponseEntity<List<HsOfficeContactResource>> getListOfContacts(
final String currentSubject,
final String assumedRoles,
final String caption) {
final String caption,
final String emailAddress) {
context.define(currentSubject, assumedRoles);
final var entities = contactRepo.findContactByOptionalCaptionLike(caption);
validate("caption, emailAddress").atMaxOne(caption, emailAddress);
final var entities = emailAddress != null
? contactRepo.findContactByEmailAddress(emailAddress)
: contactRepo.findContactByOptionalCaptionLike(caption);
final var resources = mapper.mapList(entities, HsOfficeContactResource.class);
return ResponseEntity.ok(resources);

View File

@@ -21,6 +21,16 @@ public interface HsOfficeContactRbacRepository extends Repository<HsOfficeContac
@Timed("app.office.contacts.repo.findContactByOptionalCaptionLike.rbac")
List<HsOfficeContactRbacEntity> findContactByOptionalCaptionLike(String caption);
@Query(value = """
select c.* from hs_office.contact_rv c
where exists (
SELECT 1 FROM jsonb_each_text(c.emailAddresses) AS kv(key, value)
WHERE kv.value LIKE :emailAddressRegEx
)
""", nativeQuery = true)
@Timed("app.office.contacts.repo.findContactByEmailAddress.rbac")
List<HsOfficeContactRbacEntity> findContactByEmailAddress(final String emailAddressRegEx);
@Timed("app.office.contacts.repo.save.rbac")
HsOfficeContactRbacEntity save(final HsOfficeContactRbacEntity entity);

View File

@@ -43,7 +43,7 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
final String partnerNumber) {
context.define(currentSubject, assumedRoles);
validate("partnerUuid, partnerNumber").atMaxOneNonNull(partnerUuid, partnerNumber);
validate("partnerUuid, partnerNumber").atMaxOne(partnerUuid, partnerNumber);
final var entities = partnerNumber != null
? membershipRepo.findMembershipsByPartnerNumber(

View File

@@ -35,10 +35,10 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
public ResponseEntity<List<HsOfficePersonResource>> getListOfPersons(
final String currentSubject,
final String assumedRoles,
final String caption) {
final String name) {
context.define(currentSubject, assumedRoles);
final var entities = personRepo.findPersonByOptionalNameLike(caption);
final var entities = personRepo.findPersonByOptionalNameLike(name);
final var resources = mapper.mapList(entities, HsOfficePersonResource.class);
return ResponseEntity.ok(resources);

View File

@@ -2,9 +2,12 @@ package net.hostsharing.hsadminng.hs.office.relation;
import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.errors.Validate;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealRepository;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeRelationsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealRepository;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import org.springframework.beans.factory.annotation.Autowired;
@@ -20,6 +23,7 @@ import java.util.NoSuchElementException;
import java.util.UUID;
import java.util.function.BiConsumer;
import static net.hostsharing.hsadminng.mapper.KeyValueMap.from;
@RestController
public class HsOfficeRelationController implements HsOfficeRelationsApi {
@@ -31,10 +35,10 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
private StandardMapper mapper;
@Autowired
private HsOfficeRelationRbacRepository relationRbacRepo;
private HsOfficeRelationRbacRepository rbacRelationRepo;
@Autowired
private HsOfficePersonRealRepository personRepo;
private HsOfficePersonRealRepository realPersonRepo;
@Autowired
private HsOfficeContactRealRepository realContactRepo;
@@ -50,15 +54,16 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
final String assumedRoles,
final UUID personUuid,
final HsOfficeRelationTypeResource relationType,
final String mark,
final String personData,
final String contactData) {
context.define(currentSubject, assumedRoles);
final List<HsOfficeRelationRbacEntity> entities =
relationRbacRepo.findRelationRelatedToPersonUuidRelationTypePersonAndContactData(
rbacRelationRepo.findRelationRelatedToPersonUuidRelationTypeMarkPersonAndContactData(
personUuid,
relationType == null ? null : HsOfficeRelationType.valueOf(relationType.name()),
personData, contactData);
mark, personData, contactData);
final var resources = mapper.mapList(entities, HsOfficeRelationResource.class,
RELATION_ENTITY_TO_RESOURCE_POSTMAPPER);
@@ -78,17 +83,34 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
final var entityToSave = new HsOfficeRelationRbacEntity();
entityToSave.setType(HsOfficeRelationType.valueOf(body.getType()));
entityToSave.setMark(body.getMark());
entityToSave.setAnchor(personRepo.findByUuid(body.getAnchorUuid()).orElseThrow(
entityToSave.setAnchor(realPersonRepo.findByUuid(body.getAnchorUuid()).orElseThrow(
() -> new NoSuchElementException("cannot find Person by anchorUuid: " + body.getAnchorUuid())
));
entityToSave.setHolder(personRepo.findByUuid(body.getHolderUuid()).orElseThrow(
() -> new NoSuchElementException("cannot find Person by holderUuid: " + body.getHolderUuid())
));
entityToSave.setContact(realContactRepo.findByUuid(body.getContactUuid()).orElseThrow(
() -> new NoSuchElementException("cannot find Contact by contactUuid: " + body.getContactUuid())
));
final var saved = relationRbacRepo.save(entityToSave);
Validate.validate("anchor, anchor.uuid").exactlyOne(body.getHolder(), body.getHolderUuid());
if ( body.getHolderUuid() != null) {
entityToSave.setHolder(realPersonRepo.findByUuid(body.getHolderUuid()).orElseThrow(
() -> new NoSuchElementException("cannot find Person by holderUuid: " + body.getHolderUuid())
));
} else {
entityToSave.setHolder(realPersonRepo.save(
mapper.map(body.getHolder(), HsOfficePersonRealEntity.class)
) );
}
Validate.validate("contact, contact.uuid").exactlyOne(body.getContact(), body.getContactUuid());
if ( body.getContactUuid() != null) {
entityToSave.setContact(realContactRepo.findByUuid(body.getContactUuid()).orElseThrow(
() -> new NoSuchElementException("cannot find Contact by contactUuid: " + body.getContactUuid())
));
} else {
entityToSave.setContact(realContactRepo.save(
mapper.map(body.getContact(), HsOfficeContactRealEntity.class, CONTACT_RESOURCE_TO_ENTITY_POSTMAPPER)
) );
}
final var saved = rbacRelationRepo.save(entityToSave);
final var uri =
MvcUriComponentsBuilder.fromController(getClass())
@@ -110,7 +132,7 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
context.define(currentSubject, assumedRoles);
final var result = relationRbacRepo.findByUuid(relationUuid);
final var result = rbacRelationRepo.findByUuid(relationUuid);
if (result.isEmpty()) {
return ResponseEntity.notFound().build();
}
@@ -126,7 +148,7 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
final UUID relationUuid) {
context.define(currentSubject, assumedRoles);
final var result = relationRbacRepo.deleteByUuid(relationUuid);
final var result = rbacRelationRepo.deleteByUuid(relationUuid);
if (result == 0) {
return ResponseEntity.notFound().build();
}
@@ -145,11 +167,11 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
context.define(currentSubject, assumedRoles);
final var current = relationRbacRepo.findByUuid(relationUuid).orElseThrow();
final var current = rbacRelationRepo.findByUuid(relationUuid).orElseThrow();
new HsOfficeRelationEntityPatcher(em, current).apply(body);
final var saved = relationRbacRepo.save(current);
final var saved = rbacRelationRepo.save(current);
final var mapped = mapper.map(saved, HsOfficeRelationResource.class);
return ResponseEntity.ok(mapped);
}
@@ -159,4 +181,11 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
resource.setHolder(mapper.map(entity.getHolder(), HsOfficePersonResource.class));
resource.setContact(mapper.map(entity.getContact(), HsOfficeContactResource.class));
};
@SuppressWarnings("unchecked")
final BiConsumer<HsOfficeContactInsertResource, HsOfficeContactRealEntity> CONTACT_RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
entity.putEmailAddresses(from(resource.getEmailAddresses()));
entity.putPhoneNumbers(from(resource.getPhoneNumbers()));
};
}

View File

@@ -26,24 +26,29 @@ public interface HsOfficeRelationRbacRepository extends Repository<HsOfficeRelat
* *
* @param personUuid the optional UUID of the anchorPerson or holderPerson
* @param relationType the type of the relation
* @param mark the mark (use '%' for wildcard), case ignored
* @param personData a string to match the persons tradeName, familyName or givenName (use '%' for wildcard), case ignored
* @param contactData a string to match the contacts caption, postalAddress, emailAddresses or phoneNumbers (use '%' for wildcard), case ignored
* @return a list of (accessible) relations which match all given criteria
*/
default List<HsOfficeRelationRbacEntity> findRelationRelatedToPersonUuidRelationTypePersonAndContactData(
default List<HsOfficeRelationRbacEntity> findRelationRelatedToPersonUuidRelationTypeMarkPersonAndContactData(
final UUID personUuid,
final HsOfficeRelationType relationType,
final String mark,
final String personData,
final String contactData) {
return findRelationRelatedToPersonUuidRelationTypePersonAndContactDataImpl(
personUuid, toStringOrNull(relationType), toSqlLikeOperand(personData), toSqlLikeOperand(contactData));
return findRelationRelatedToPersonUuidRelationByTypeMarkPersonAndContactDataImpl(
personUuid, toStringOrNull(relationType),
toSqlLikeOperand(mark), toSqlLikeOperand(personData), toSqlLikeOperand(contactData));
}
// TODO: use ELIKE instead of lower(...) LIKE ...? Or use jsonb_path with RegEx like emailAddressRegEx in ContactRepo?
@Query(value = """
SELECT rel FROM HsOfficeRelationRbacEntity AS rel
WHERE (:relationType IS NULL OR CAST(rel.type AS String) = :relationType)
AND ( :personUuid IS NULL
OR rel.anchor.uuid = :personUuid OR rel.holder.uuid = :personUuid )
AND ( :mark IS NULL OR lower(rel.mark) LIKE :mark )
AND ( :personData IS NULL
OR lower(rel.anchor.tradeName) LIKE :personData OR lower(rel.holder.tradeName) LIKE :personData
OR lower(rel.anchor.familyName) LIKE :personData OR lower(rel.holder.familyName) LIKE :personData
@@ -54,10 +59,11 @@ public interface HsOfficeRelationRbacRepository extends Repository<HsOfficeRelat
OR lower(CAST(rel.contact.emailAddresses AS String)) LIKE :contactData
OR lower(CAST(rel.contact.phoneNumbers AS String)) LIKE :contactData )
""")
@Timed("app.office.relations.repo.findRelationRelatedToPersonUuidRelationTypePersonAndContactDataImpl.rbac")
List<HsOfficeRelationRbacEntity> findRelationRelatedToPersonUuidRelationTypePersonAndContactDataImpl(
@Timed("app.office.relations.repo.findRelationRelatedToPersonUuidRelationByTypeMarkPersonAndContactDataImpl.rbac")
List<HsOfficeRelationRbacEntity> findRelationRelatedToPersonUuidRelationByTypeMarkPersonAndContactDataImpl(
final UUID personUuid,
final String relationType,
final String mark,
final String personData,
final String contactData);

View File

@@ -7,12 +7,19 @@ get:
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: name
- name: caption
in: query
required: false
schema:
type: string
description: Prefix of caption to filter the results.
description: Beginning of caption to filter the results.
- name: emailAddress
in: query
required: false
schema:
type: string
description:
Email-address to filter the results, use '%' as wildcard.
responses:
"200":
description: OK

View File

@@ -52,6 +52,8 @@ components:
holder.uuid:
type: string
format: uuid
holder:
$ref: 'hs-office-person-schemas.yaml#/components/schemas/HsOfficePersonInsert'
type:
type: string
nullable: true
@@ -61,11 +63,17 @@ components:
contact.uuid:
type: string
format: uuid
contact:
$ref: 'hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContactInsert'
required:
- anchor.uuid
- holder.uuid
- type
- contact.uuid
- anchor.uuid
- type
# soon we might need to be able to use this:
# https://community.smartbear.com/discussions/swaggerostools/defining-conditional-attributes-in-openapi/222410
# For now we just describe the conditionally required properties:
description:
Additionally to `type` and `anchor.uuid`, either `anchor.uuid` or `anchor`
and either `contact` or `contact.uuid` need to be given.
# relation created as a sub-element with implicitly known type
HsOfficeRelationSubInsert:

View File

@@ -21,7 +21,13 @@ get:
required: false
schema:
$ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationType'
description: Prefix of name properties from holder or contact to filter the results.
description: Beginning of name properties from holder or contact to filter the results.
- name: mark
in: query
required: false
schema:
type: string
description:
- name: personData
in: query
required: false