1
0

update dependend relations when updating partner person (#162)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/162
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
Michael Hoennig
2025-03-10 12:04:54 +01:00
parent e3b11972e5
commit a2b81f009b
26 changed files with 949 additions and 241 deletions

View File

@@ -13,15 +13,13 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import jakarta.annotation.PostConstruct;
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
public class HsOfficeContactController implements HsOfficeContactsApi {
@Autowired
@@ -30,9 +28,20 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
@Autowired
private StrictMapper mapper;
@Autowired
private HsOfficeContactFromResourceConverter<HsOfficeContactRbacEntity> contactFromResourceConverter;
@Autowired
private HsOfficeContactRbacRepository contactRepo;
@PostConstruct
public void init() {
// HOWTO: add a ModelMapper converter for a generic entity class to a ModelMapper to be used in a certain context
// This @PostConstruct could be implemented in the converter, but only without generics.
// But this converter is for HsOfficeContactRbacEntity and HsOfficeContactRealEntity.
mapper.addConverter(contactFromResourceConverter, HsOfficeContactInsertResource.class, HsOfficeContactRbacEntity.class);
}
@Override
@Transactional(readOnly = true)
@Timed("app.office.contacts.api.getListOfContacts")
@@ -62,7 +71,7 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
context.define(currentSubject, assumedRoles);
final var entityToSave = mapper.map(body, HsOfficeContactRbacEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
final var entityToSave = mapper.map(body, HsOfficeContactRbacEntity.class);
final var saved = contactRepo.save(entityToSave);
@@ -128,11 +137,4 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
final var mapped = mapper.map(saved, HsOfficeContactResource.class);
return ResponseEntity.ok(mapped);
}
@SuppressWarnings("unchecked")
final BiConsumer<HsOfficeContactInsertResource, HsOfficeContactRbacEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
entity.putPostalAddress(from(resource.getPostalAddress()));
entity.putEmailAddresses(from(resource.getEmailAddresses()));
entity.putPhoneNumbers(from(resource.getPhoneNumbers()));
};
}

View File

@@ -0,0 +1,27 @@
package net.hostsharing.hsadminng.hs.office.contact;
import lombok.SneakyThrows;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactInsertResource;
import org.modelmapper.Converter;
import org.modelmapper.spi.MappingContext;
import org.springframework.stereotype.Component;
import static net.hostsharing.hsadminng.mapper.KeyValueMap.from;
// HOWTO: implement a ModelMapper converter which converts from a (JSON) resource instance to a generic entity instance (RBAC vs. REAL)
@Component
public class HsOfficeContactFromResourceConverter<E extends HsOfficeContact>
implements Converter<HsOfficeContactInsertResource, E> {
@Override
@SneakyThrows
public E convert(final MappingContext<HsOfficeContactInsertResource, E> context) {
final var resource = context.getSource();
final var entity = context.getDestinationType().getDeclaredConstructor().newInstance();
entity.setCaption(resource.getCaption());
entity.putPostalAddress(from(resource.getPostalAddress()));
entity.putEmailAddresses(from(resource.getEmailAddresses()));
entity.putPhoneNumbers(from(resource.getPhoneNumbers()));
return entity;
}
}

View File

@@ -3,8 +3,10 @@ package net.hostsharing.hsadminng.hs.office.partner;
import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.errors.ReferenceNotFoundException;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactFromResourceConverter;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficePartnersApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerPatchResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerResource;
@@ -22,6 +24,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import jakarta.annotation.PostConstruct;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.util.List;
@@ -42,14 +45,22 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
private StrictMapper mapper;
@Autowired
private HsOfficePartnerRbacRepository partnerRepo;
private HsOfficeContactFromResourceConverter<HsOfficeContactRealEntity> contactFromResourceConverter;
@Autowired
private HsOfficeRelationRealRepository relationRepo;
private HsOfficePartnerRbacRepository rbacPartnerRepo;
@Autowired
private HsOfficeRelationRealRepository realRelationRepo;
@PersistenceContext
private EntityManager em;
@PostConstruct
public void init() {
mapper.addConverter(contactFromResourceConverter, HsOfficeContactInsertResource.class, HsOfficeContactRealEntity.class);
}
@Override
@Transactional(readOnly = true)
@Timed("app.office.partners.api.getListOfPartners")
@@ -59,7 +70,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
final String name) {
context.define(currentSubject, assumedRoles);
final var entities = partnerRepo.findPartnerByOptionalNameLike(name);
final var entities = rbacPartnerRepo.findPartnerByOptionalNameLike(name);
final var resources = mapper.mapList(entities, HsOfficePartnerResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(resources);
@@ -77,7 +88,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
final var entityToSave = createPartnerEntity(body);
final var saved = partnerRepo.save(entityToSave);
final var saved = rbacPartnerRepo.save(entityToSave);
final var uri =
MvcUriComponentsBuilder.fromController(getClass())
@@ -98,7 +109,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
context.define(currentSubject, assumedRoles);
final var result = partnerRepo.findByUuid(partnerUuid);
final var result = rbacPartnerRepo.findByUuid(partnerUuid);
if (result.isEmpty()) {
return ResponseEntity.notFound().build();
}
@@ -116,7 +127,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
context.define(currentSubject, assumedRoles);
final var result = partnerRepo.findPartnerByPartnerNumber(partnerNumber);
final var result = rbacPartnerRepo.findPartnerByPartnerNumber(partnerNumber);
if (result.isEmpty()) {
return ResponseEntity.notFound().build();
}
@@ -133,12 +144,12 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
final UUID partnerUuid) {
context.define(currentSubject, assumedRoles);
final var partnerToDelete = partnerRepo.findByUuid(partnerUuid);
final var partnerToDelete = rbacPartnerRepo.findByUuid(partnerUuid);
if (partnerToDelete.isEmpty()) {
return ResponseEntity.notFound().build();
}
if (partnerRepo.deleteByUuid(partnerUuid) != 1) {
if (rbacPartnerRepo.deleteByUuid(partnerUuid) != 1) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
@@ -156,22 +167,55 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
context.define(currentSubject, assumedRoles);
final var current = partnerRepo.findByUuid(partnerUuid).orElseThrow();
final var previousPartnerRel = current.getPartnerRel();
final var current = rbacPartnerRepo.findByUuid(partnerUuid).orElseThrow();
final var previousPartnerPerson = current.getPartnerRel().getHolder();
new HsOfficePartnerEntityPatcher(em, current).apply(body);
new HsOfficePartnerEntityPatcher(mapper, em, current).apply(body);
final var saved = partnerRepo.save(current);
optionallyCreateExPartnerRelation(saved, previousPartnerRel);
final var saved = rbacPartnerRepo.save(current);
optionallyCreateExPartnerRelation(saved, previousPartnerPerson);
optionallyUpdateRelatedRelations(saved, previousPartnerPerson);
final var mapped = mapper.map(saved, HsOfficePartnerResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(mapped);
}
private void optionallyCreateExPartnerRelation(final HsOfficePartnerRbacEntity saved, final HsOfficeRelationRealEntity previousPartnerRel) {
if (!saved.getPartnerRel().getUuid().equals(previousPartnerRel.getUuid())) {
// TODO.impl: we also need to use the new partner-person as the anchor
relationRepo.save(previousPartnerRel.toBuilder().uuid(null).type(EX_PARTNER).build());
private void optionallyCreateExPartnerRelation(final HsOfficePartnerRbacEntity saved, final HsOfficePersonRealEntity previousPartnerPerson) {
final var partnerPersonHasChanged = !saved.getPartnerRel().getHolder().getUuid().equals(previousPartnerPerson.getUuid());
if (partnerPersonHasChanged) {
realRelationRepo.save(saved.getPartnerRel().toBuilder()
.uuid(null)
.type(EX_PARTNER)
.anchor(saved.getPartnerRel().getHolder())
.holder(previousPartnerPerson)
.build());
}
}
private void optionallyUpdateRelatedRelations(final HsOfficePartnerRbacEntity saved, final HsOfficePersonRealEntity previousPartnerPerson) {
final var partnerPersonHasChanged = !saved.getPartnerRel().getHolder().getUuid().equals(previousPartnerPerson.getUuid());
if (partnerPersonHasChanged) {
// self-debitors of the old partner-person become self-debitors of the new partner person
em.createNativeQuery("""
UPDATE hs_office.relation
SET holderUuid = :newPartnerPersonUuid
WHERE type = 'DEBITOR' AND
holderUuid = :oldPartnerPersonUuid AND anchorUuid = :oldPartnerPersonUuid
""")
.setParameter("oldPartnerPersonUuid", previousPartnerPerson.getUuid())
.setParameter("newPartnerPersonUuid", saved.getPartnerRel().getHolder().getUuid())
.executeUpdate();
// re-anchor all relations from the old partner person to the new partner persion
em.createNativeQuery("""
UPDATE hs_office.relation
SET anchorUuid = :newPartnerPersonUuid
WHERE anchorUuid = :oldPartnerPersonUuid
""")
.setParameter("oldPartnerPersonUuid", previousPartnerPerson.getUuid())
.setParameter("newPartnerPersonUuid", saved.getPartnerRel().getHolder().getUuid())
.executeUpdate();
}
}

View File

@@ -1,35 +1,36 @@
package net.hostsharing.hsadminng.hs.office.partner;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerPatchResource;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationPatcher;
import net.hostsharing.hsadminng.mapper.EntityPatcher;
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import jakarta.persistence.EntityManager;
class HsOfficePartnerEntityPatcher implements EntityPatcher<HsOfficePartnerPatchResource> {
private final StrictMapper mapper;
private final EntityManager em;
private final HsOfficePartnerRbacEntity entity;
HsOfficePartnerEntityPatcher(
final StrictMapper mapper,
final EntityManager em,
final HsOfficePartnerRbacEntity entity) {
this.mapper = mapper;
this.em = em;
this.entity = entity;
}
@Override
public void apply(final HsOfficePartnerPatchResource resource) {
OptionalFromJson.of(resource.getPartnerRelUuid()).ifPresent(newValue -> {
verifyNotNull(newValue, "partnerRel");
entity.setPartnerRel(em.getReference(HsOfficeRelationRealEntity.class, newValue));
});
new HsOfficePartnerDetailsEntityPatcher(em, entity.getDetails()).apply(resource.getDetails());
}
if (resource.getPartnerRel() != null) {
new HsOfficeRelationPatcher(mapper, em, entity.getPartnerRel()).apply(resource.getPartnerRel());
}
private void verifyNotNull(final Object newValue, final String propertyName) {
if (newValue == null) {
throw new IllegalArgumentException("property '" + propertyName + "' must not be null");
if (resource.getDetails() != null) {
new HsOfficePartnerDetailsEntityPatcher(em, entity.getDetails()).apply(resource.getDetails());
}
}
}

View File

@@ -163,13 +163,13 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
final String currentSubject,
final String assumedRoles,
final UUID relationUuid,
final HsOfficeRelationPatchResource body) {
final HsOfficeRelationContactPatchResource body) {
context.define(currentSubject, assumedRoles);
final var current = rbacRelationRepo.findByUuid(relationUuid).orElseThrow();
new HsOfficeRelationEntityPatcher(em, current).apply(body);
new HsOfficeRelationEntityContactPatcher(em, current).apply(body);
final var saved = rbacRelationRepo.save(current);
final var mapped = mapper.map(saved, HsOfficeRelationResource.class);

View File

@@ -1,25 +1,25 @@
package net.hostsharing.hsadminng.hs.office.relation;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeRelationPatchResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeRelationContactPatchResource;
import net.hostsharing.hsadminng.mapper.EntityPatcher;
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
import jakarta.persistence.EntityManager;
import java.util.UUID;
class HsOfficeRelationEntityPatcher implements EntityPatcher<HsOfficeRelationPatchResource> {
public class HsOfficeRelationEntityContactPatcher implements EntityPatcher<HsOfficeRelationContactPatchResource> {
private final EntityManager em;
private final HsOfficeRelation entity;
HsOfficeRelationEntityPatcher(final EntityManager em, final HsOfficeRelation entity) {
public HsOfficeRelationEntityContactPatcher(final EntityManager em, final HsOfficeRelation entity) {
this.em = em;
this.entity = entity;
}
@Override
public void apply(final HsOfficeRelationPatchResource resource) {
public void apply(final HsOfficeRelationContactPatchResource resource) {
OptionalFromJson.of(resource.getContactUuid()).ifPresent(newValue -> {
verifyNotNull(newValue, "contact");
entity.setContact(em.getReference(HsOfficeContactRealEntity.class, newValue));

View File

@@ -0,0 +1,50 @@
package net.hostsharing.hsadminng.hs.office.relation;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeRelationPatchResource;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
import net.hostsharing.hsadminng.mapper.EntityPatcher;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import jakarta.persistence.EntityManager;
import jakarta.validation.ValidationException;
public class HsOfficeRelationPatcher implements EntityPatcher<HsOfficeRelationPatchResource> {
private final StrictMapper mapper;
private final EntityManager em;
private final HsOfficeRelation entity;
public HsOfficeRelationPatcher(final StrictMapper mapper, final EntityManager em, final HsOfficeRelation entity) {
this.mapper = mapper;
this.em = em;
this.entity = entity;
}
@Override
public void apply(final HsOfficeRelationPatchResource resource) {
if (resource.getHolder() != null && resource.getHolderUuid() != null) {
throw new ValidationException("either \"holder\" or \"holder.uuid\" can be given, not both");
} else {
if (resource.getHolder() != null) {
final var newHolder = mapper.map(resource.getHolder(), HsOfficePersonRealEntity.class);
em.persist(newHolder);
entity.setHolder(newHolder);
} else if (resource.getHolderUuid() != null) {
entity.setHolder(em.getReference(HsOfficePersonRealEntity.class, resource.getHolderUuid().get()));
}
}
if (resource.getContact() != null && resource.getContactUuid() != null) {
throw new ValidationException("either \"contact\" or \"contact.uuid\" can be given, not both");
} else {
if (resource.getContact() != null) {
final var newContact = mapper.map(resource.getContact(), HsOfficeContactRealEntity.class);
em.persist(newContact);
entity.setContact(newContact);
} else if (resource.getContactUuid() != null) {
entity.setContact(em.getReference(HsOfficeContactRealEntity.class, resource.getContactUuid().get()));
}
}
}
}

View File

@@ -51,7 +51,7 @@ public class HsOfficeRelationRbacEntity extends HsOfficeRelation {
"""))
.withRestrictedViewOrderBy(SQL.expression(
"(select idName from hs_office.person_iv p where p.uuid = target.holderUuid)"))
.withUpdatableColumns("contactUuid")
.withUpdatableColumns("anchorUuid", "holderUuid", "contactUuid") // BEWARE: additional checks at API-level
.importEntityAlias("anchorPerson", HsOfficePersonRbacEntity.class, usingDefaultCase(),
dependsOnColumn("anchorUuid"),
directlyFetchedByDependsOnColumn(),

View File

@@ -48,12 +48,11 @@ components:
HsOfficePartnerPatch:
type: object
properties:
partnerRel.uuid:
type: string
format: uuid
nullable: true
partnerRel:
$ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationPatch'
details:
$ref: '#/components/schemas/HsOfficePartnerDetailsPatch'
additionalProperties: false
HsOfficePartnerDetailsPatch:
type: object

View File

@@ -34,7 +34,7 @@ components:
contact:
$ref: 'hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContact'
HsOfficeRelationPatch:
HsOfficeRelationContactPatch:
type: object
properties:
contact.uuid:
@@ -42,6 +42,27 @@ components:
format: uuid
nullable: true
HsOfficeRelationPatch:
type: object
properties:
anchor.uuid:
type: string
format: uuid
nullable: true
holder.uuid:
type: string
format: uuid
nullable: true
holder:
$ref: 'hs-office-person-schemas.yaml#/components/schemas/HsOfficePersonInsert'
contact.uuid:
type: string
format: uuid
nullable: true
contact:
$ref: 'hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContactInsert'
additionalProperties: false
# arbitrary relation with explicit type
HsOfficeRelationInsert:
type: object

View File

@@ -44,7 +44,7 @@ patch:
content:
'application/json':
schema:
$ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationPatch'
$ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationContactPatch'
responses:
"200":
description: OK

View File

@@ -124,7 +124,9 @@ create or replace procedure hs_office.relation_update_rbac_system(
language plpgsql as $$
begin
if NEW.contactUuid is distinct from OLD.contactUuid then
if NEW.holderUuid is distinct from OLD.holderUuid
or NEW.anchorUuid is distinct from OLD.anchorUuid
or NEW.contactUuid is distinct from OLD.contactUuid then
delete from rbac.grant g where g.grantedbytriggerof = OLD.uuid;
call hs_office.relation_build_rbac_system(NEW);
end if;
@@ -248,6 +250,8 @@ call rbac.generateRbacRestrictedView('hs_office.relation',
(select idName from hs_office.person_iv p where p.uuid = target.holderUuid)
$orderBy$,
$updates$
anchorUuid = new.anchorUuid,
holderUuid = new.holderUuid,
contactUuid = new.contactUuid
$updates$);
--//