Compare commits
4 Commits
master
...
office-rba
Author | SHA1 | Date | |
---|---|---|---|
|
04138fe991 | ||
|
5c2aec1c37 | ||
|
97059c75a1 | ||
|
5aec875680 |
27
.aliases
27
.aliases
@ -8,20 +8,12 @@ gradleWrapper () {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if command -v unbuffer >/dev/null 2>&1; then
|
||||
# if `unbuffer` is available in PATH, use it to print report file-URIs at the end
|
||||
TEMPFILE=$(mktemp /tmp/gw.XXXXXX)
|
||||
unbuffer ./gradlew "$@" | tee $TEMPFILE
|
||||
echo
|
||||
grep --color=never "Report:" $TEMPFILE
|
||||
rm $TEMPFILE
|
||||
else
|
||||
# if `unbuffer` is not in PATH, simply run gradle
|
||||
./gradlew "$@"
|
||||
echo "HINT: it's suggested to install 'unbuffer' to print report URIs at the end of a gradle run"
|
||||
fi
|
||||
|
||||
TEMPFILE=$(mktemp /tmp/gw.XXXXXX)
|
||||
unbuffer ./gradlew "$@" | tee $TEMPFILE
|
||||
|
||||
echo
|
||||
grep --color=never "Report:" $TEMPFILE
|
||||
rm $TEMPFILE
|
||||
}
|
||||
|
||||
postgresAutodoc () {
|
||||
@ -38,13 +30,13 @@ postgresAutodoc () {
|
||||
fi
|
||||
postgresql_autodoc -d postgres -f build/postgres-autodoc -h localhost -u postgres --password=password \
|
||||
-m '(rbacobject|hs).*' \
|
||||
-l /usr/share/postgresql-autodoc -t neato &&
|
||||
-l /usr/share/postgresql-autodoc -t neato &&
|
||||
dot -Tsvg build/postgres-autodoc.neato >build/postgres-autodoc-hs.svg && \
|
||||
echo "generated: $PWD/build/postgres-autodoc-hs.svg"
|
||||
|
||||
postgresql_autodoc -d postgres -f build/postgres-autodoc -h localhost -u postgres --password=password \
|
||||
-m '(global|rbac).*' \
|
||||
-l /usr/share/postgresql-autodoc -t neato &&
|
||||
-l /usr/share/postgresql-autodoc -t neato &&
|
||||
dot -Tsvg build/postgres-autodoc.neato >build/postgres-autodoc-rbac.svg && \
|
||||
echo "generated $PWD/build/postgres-autodoc-rbac.svg"
|
||||
}
|
||||
@ -91,7 +83,7 @@ alias fp='grep -r '@Accepts' src | sed -e 's/^.*@/@/g' | sort -u | wc -l'
|
||||
|
||||
alias gw-spotless='./gradlew spotlessApply -x pitest -x test -x :processResources'
|
||||
alias gw-test='. .aliases; ./gradlew test'
|
||||
alias gw-check='. .aliases; gw test check -x pitest'
|
||||
alias gw-check='. .aliases; gw test importOfficeData check -x pitest -x :dependencyCheckAnalyze'
|
||||
|
||||
# etc/docker-compose.yml limits CPUs+MEM and includes a PostgreSQL config for analysing slow queries
|
||||
alias gw-importOfficeData-in-docker-compose='
|
||||
@ -103,6 +95,3 @@ if [ ! -f .environment ]; then
|
||||
cp .tc-environment .environment
|
||||
fi
|
||||
source .environment
|
||||
|
||||
alias scenario-reports-upload='./gradlew scenarioTests convertMarkdownToHtml && ssh hsh03-hsngdev@h50.hostsharing.net "rm -f doms/hsngdev.hs-example.de/htdocs-ssl/scenarios/office/*.html" && scp build/doc/scenarios/*.html hsh03-hsngdev@h50.hostsharing.net:doms/hsngdev.hs-example.de/htdocs-ssl/scenarios/office'
|
||||
alias scenario-reports-open='open https://hsngdev.hs-example.de/scenarios/office'
|
||||
|
88
Jenkinsfile
vendored
88
Jenkinsfile
vendored
@ -1,88 +0,0 @@
|
||||
pipeline {
|
||||
agent {
|
||||
dockerfile {
|
||||
filename 'etc/jenkinsAgent.Dockerfile'
|
||||
// additionalBuildArgs ...
|
||||
args '--network=bridge --user root -v $PWD:$PWD \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock --group-add 984 \
|
||||
--memory=6g --cpus=3'
|
||||
}
|
||||
}
|
||||
|
||||
environment {
|
||||
DOCKER_HOST = 'unix:///var/run/docker.sock'
|
||||
HSADMINNG_POSTGRES_ADMIN_USERNAME = 'admin'
|
||||
HSADMINNG_POSTGRES_RESTRICTED_USERNAME = 'restricted'
|
||||
HSADMINNG_MIGRATION_DATA_PATH = 'migration'
|
||||
}
|
||||
|
||||
triggers {
|
||||
pollSCM('H/1 * * * *')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout scm
|
||||
}
|
||||
}
|
||||
|
||||
stage ('Compile') {
|
||||
steps {
|
||||
sh './gradlew clean processSpring compileJava compileTestJava --no-daemon'
|
||||
}
|
||||
}
|
||||
|
||||
stage ('Tests') {
|
||||
parallel {
|
||||
stage('Unit-/Integration/Acceptance-Tests') {
|
||||
steps {
|
||||
sh './gradlew check --no-daemon -x pitest -x dependencyCheckAnalyze -x importOfficeData -x importHostingAssets'
|
||||
}
|
||||
}
|
||||
stage('Import-Tests') {
|
||||
steps {
|
||||
sh './gradlew importOfficeData importHostingAssets --no-daemon'
|
||||
}
|
||||
}
|
||||
stage ('Scenario-Tests') {
|
||||
steps {
|
||||
sh './gradlew scenarioTests --no-daemon'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage ('Check') {
|
||||
steps {
|
||||
sh './gradlew check -x pitest -x dependencyCheckAnalyze --no-daemon'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
always {
|
||||
// archive test results
|
||||
junit 'build/test-results/test/*.xml'
|
||||
|
||||
// archive the JaCoCo coverage report in XML and HTML format
|
||||
jacoco(
|
||||
execPattern: 'build/jacoco/*.exec',
|
||||
classPattern: 'build/classes/java/main',
|
||||
sourcePattern: 'src/main/java'
|
||||
)
|
||||
|
||||
// archive scenario-test reports in HTML format
|
||||
sh '''
|
||||
./gradlew convertMarkdownToHtml
|
||||
'''
|
||||
archiveArtifacts artifacts:
|
||||
'build/doc/scenarios/*.html, ' +
|
||||
'build/reports/dependency-license/dependencies-without-allowed-license.json',
|
||||
allowEmptyArchive: true
|
||||
|
||||
// cleanup workspace
|
||||
cleanWs()
|
||||
}
|
||||
}
|
||||
}
|
79
README.md
79
README.md
@ -60,40 +60,36 @@ If you have at least Docker and the Java JDK installed in appropriate versions a
|
||||
|
||||
cd your-hsadmin-ng-directory
|
||||
|
||||
source .aliases # creates some comfortable bash aliases, e.g. 'gw'='./gradlew'
|
||||
gw # initially downloads the configured Gradle version into the project
|
||||
source .aliases # creates some comfortable bash aliases, e.g. 'gw'='./gradlew'
|
||||
gw # initially downloads the configured Gradle version into the project
|
||||
|
||||
gw test # compiles and runs unit- and integration-tests - takes >10min even on a fast machine
|
||||
gw scenarioTests # compiles and scenario-tests - takes ~1min on a decent machine
|
||||
gw test # compiles and runs unit- and integration-tests
|
||||
|
||||
# if the container has not been built yet, run this:
|
||||
pg-sql-run # downloads + runs PostgreSQL in a Docker container on localhost:5432
|
||||
# if the container has been built already and you want to keep the data, run this:
|
||||
# if the container has been built already, run this:
|
||||
pg-sql-start
|
||||
|
||||
gw bootRun # compiles and runs the application on localhost:8080
|
||||
|
||||
# the following command should reply with "pong":
|
||||
curl -f http://localhost:8080/api/ping
|
||||
curl http://localhost:8080/api/ping
|
||||
|
||||
# the following command should return a JSON array with just all customers:
|
||||
curl -f\
|
||||
curl \
|
||||
-H 'current-subject: superuser-alex@hostsharing.net' \
|
||||
http://localhost:8080/api/test/customers \
|
||||
| jq # just if `jq` is installed, to prettyprint the output
|
||||
http://localhost:8080/api/test/customers
|
||||
|
||||
# the following command should return a JSON array with just all packages visible for the admin of the customer yyy:
|
||||
curl -f\
|
||||
-H 'current-subject: superuser-alex@hostsharing.net' -H 'assumed-roles: rbactest.customer#yyy:ADMIN' \
|
||||
http://localhost:8080/api/test/packages \
|
||||
| jq
|
||||
curl \
|
||||
-H 'current-subject: superuser-alex@hostsharing.net' -H 'assumed-roles: test_customer#yyy:ADMIN' \
|
||||
http://localhost:8080/api/test/packages
|
||||
|
||||
# add a new customer
|
||||
curl -f\
|
||||
curl \
|
||||
-H 'current-subject: superuser-alex@hostsharing.net' -H "Content-Type: application/json" \
|
||||
-d '{ "prefix":"ttt", "reference":80001, "adminUserName":"admin@ttt.example.com" }' \
|
||||
-X POST http://localhost:8080/api/test/customers \
|
||||
| jq
|
||||
-X POST http://localhost:8080/api/test/customers
|
||||
|
||||
If you wonder who 'superuser-alex@hostsharing.net' and 'superuser-fran@hostsharing.net' are and where the data comes from:
|
||||
Mike and Sven are just example global admin accounts as part of the example data which is automatically inserted in Testcontainers and Development environments.
|
||||
@ -501,19 +497,9 @@ We'll see if this changes when the project progresses and more validations are a
|
||||
|
||||
### OWASP Security Vulnerability Check
|
||||
|
||||
An OWASP security vulnerability is configured, but you need an API key.
|
||||
Fetch it from https://nvd.nist.gov/developers/request-an-api-key.
|
||||
|
||||
Then add it to your `~/.gradle/gradle.properties` file:
|
||||
|
||||
```
|
||||
OWASP_API_KEY=........-....-....-....-............
|
||||
```
|
||||
|
||||
Now you can run the dependency vulnerability check:
|
||||
An OWASP security vulnerability is configured and can be utilized by running:
|
||||
|
||||
```shell
|
||||
gw dependencyCheckUpdate
|
||||
gw dependencyCheckAnalyze
|
||||
```
|
||||
|
||||
@ -564,51 +550,14 @@ Dependency versions can be automatically upgraded to the latest available versio
|
||||
gw useLatestVersions
|
||||
```
|
||||
|
||||
Afterward, `gw check` is automatically started.
|
||||
Afterwards, `gw check` is automatically started.
|
||||
Please only commit+push to master if the check run shows no errors.
|
||||
|
||||
More infos, e.g. on blacklists see on the [project's website](https://github.com/patrikerdes/gradle-use-latest-versions-plugin).
|
||||
|
||||
|
||||
## Biggest Flaws in our Architecture
|
||||
|
||||
### The RBAC System is too Complicated
|
||||
|
||||
Now, where we have a better experience with what we really need from the RBAC system, we have learned
|
||||
that and creates too many (grant- and role-) rows and too even tables which could be avoided completely.
|
||||
|
||||
The basic idea is always to always have a fixed set of ordered role-types which apply for all DB-tables under RBAC,
|
||||
e.g. OWNER>ADMIN>AGENT\[>PROXY?\]>TENENT>REFERRER.
|
||||
Grants between these for the same DB-row would be implicit by order comparison.
|
||||
This way we would get rid of all explicit grants within the same DB-row
|
||||
and would not need the `rbac.role` table anymore.
|
||||
We would also reduce the depth of the expensive recursive CTE-query.
|
||||
|
||||
This has to be explored further.
|
||||
For now, we just keep it in mind and
|
||||
|
||||
### The Mapper is Error-Prone
|
||||
|
||||
Where `org.modelmapper.ModelMapper` reduces bloat-code a lot and has some nice features about recursive data-structure mappings,
|
||||
it often causes strange errors which are hard to fix.
|
||||
E.g. the uuid of the target main object is often taken from an uuid of a sub-subject.
|
||||
(For now, use `StrictMapper` to avoid this, for the case it happens.)
|
||||
|
||||
|
||||
### Too Many Business-Rules Implemented in Controllers
|
||||
|
||||
Some REST-Controllers implement too much code for business-roles.
|
||||
This should be extracted to services.
|
||||
|
||||
|
||||
## How To ...
|
||||
|
||||
Besides the following *How Tos* you can also find several *How Tos* in the source code:
|
||||
|
||||
```sh
|
||||
grep -r HOWTO src
|
||||
```
|
||||
|
||||
### How to Configure .pgpass for the Default PostgreSQL Database?
|
||||
|
||||
To access the default database schema as used during development, add this line to your `.pgpass` file in your users home directory:
|
||||
|
@ -1,38 +1,36 @@
|
||||
#!/bin/bash
|
||||
# waits for commits on any branch on origin, checks it out and builds it
|
||||
|
||||
. .aliases
|
||||
# get the current branch name
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
while true; do
|
||||
git fetch origin >/dev/null
|
||||
branch_with_new_commits=`git fetch origin >/dev/null; git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads | grep '\[behind' | cut -d' ' -f1 | head -n1`
|
||||
|
||||
if [ -n "$branch_with_new_commits" ]; then
|
||||
echo "checking out branch: $branch_with_new_commits"
|
||||
if git show-ref --quiet --heads "$branch_with_new_commits"; then
|
||||
echo "Branch $branch_with_new_commits already exists. Checking it out and pulling latest changes."
|
||||
git checkout "$branch_with_new_commits"
|
||||
git pull origin "$branch_with_new_commits"
|
||||
else
|
||||
echo "Creating and checking out new branch: $branch_with_new_commits"
|
||||
git checkout -b "$branch_with_new_commits" "origin/$branch_with_new_commits"
|
||||
fi
|
||||
# get the latest commit hashes from origin and local
|
||||
git fetch origin
|
||||
LOCAL=$(git rev-parse HEAD)
|
||||
REMOTE=$(git rev-parse origin/$BRANCH)
|
||||
|
||||
echo "building ..."
|
||||
./gradlew gw clean test check -x pitest
|
||||
fi
|
||||
# check if the local branch differs from the remote branch
|
||||
if [ "$LOCAL" != "$REMOTE" ]; then
|
||||
echo "local $LOCAL differs from remote $REMOTE => pulling changes from origin"
|
||||
git pull origin $BRANCH
|
||||
|
||||
# wait 10s with a little animation
|
||||
echo -e -n "\r\033[K waiting for changes (/) ..."
|
||||
sleep 2
|
||||
echo -e -n "\r\033[K waiting for changes (-) ..."
|
||||
sleep 2
|
||||
echo -e -n "\r\033[K waiting for changes (\) ..."
|
||||
sleep 2
|
||||
echo -e -n "\r\033[K waiting for changes (|) ..."
|
||||
sleep 2
|
||||
echo -e -n "\r\033[K waiting for changes ( ) ... "
|
||||
sleep 2
|
||||
echo -e -n "\r\033[K checking for changes"
|
||||
# run the command
|
||||
echo "Running ./gradlew test"
|
||||
source .aliases # only variables, aliases are not expanded in scripts
|
||||
./gradlew test
|
||||
fi
|
||||
|
||||
# wait 10s with a little animation
|
||||
echo -e -n " waiting for changes (/) ..."
|
||||
sleep 2
|
||||
echo -e -n "\r\033[K waiting for changes (-) ..."
|
||||
sleep 2
|
||||
echo -e -n "\r\033[K waiting for changes (\) ..."
|
||||
sleep 2
|
||||
echo -e -n "\r\033[K waiting for changes (|) ..."
|
||||
sleep 2
|
||||
echo -e -n "\r\033[K waiting for changes ( ) ... "
|
||||
sleep 2
|
||||
echo -e -n "\r\033[K"
|
||||
done
|
||||
|
||||
|
119
build.gradle
119
build.gradle
@ -1,10 +1,10 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '3.3.4'
|
||||
id 'io.spring.dependency-management' version '1.1.6'
|
||||
id 'org.springframework.boot' version '3.2.4'
|
||||
id 'io.spring.dependency-management' version '1.1.4'
|
||||
id 'io.openapiprocessor.openapi-processor' version '2023.2'
|
||||
id 'com.github.jk1.dependency-license-report' version '2.9'
|
||||
id "org.owasp.dependencycheck" version "10.0.4"
|
||||
id 'com.github.jk1.dependency-license-report' version '2.6'
|
||||
id "org.owasp.dependencycheck" version "9.0.10"
|
||||
id "com.diffplug.spotless" version "6.25.0"
|
||||
id 'jacoco'
|
||||
id 'info.solidsoft.pitest' version '1.15.0'
|
||||
@ -58,22 +58,19 @@ dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||
implementation 'com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2'
|
||||
implementation 'org.springdoc:springdoc-openapi:2.6.0'
|
||||
implementation 'org.postgresql:postgresql:42.7.4'
|
||||
implementation 'org.liquibase:liquibase-core:4.29.2'
|
||||
implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.8.3'
|
||||
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.0'
|
||||
implementation 'com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.1'
|
||||
implementation 'org.springdoc:springdoc-openapi:2.4.0'
|
||||
implementation 'org.postgresql:postgresql:42.7.3'
|
||||
implementation 'org.liquibase:liquibase-core:4.27.0'
|
||||
implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.7.3'
|
||||
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.0'
|
||||
implementation 'org.openapitools:jackson-databind-nullable:0.2.6'
|
||||
implementation 'org.apache.commons:commons-text:1.12.0'
|
||||
implementation 'net.java.dev.jna:jna:5.15.0'
|
||||
implementation 'org.modelmapper:modelmapper:3.2.1'
|
||||
implementation 'org.iban4j:iban4j:3.2.10-RELEASE'
|
||||
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'
|
||||
implementation 'org.webjars:swagger-ui:5.17.14'
|
||||
implementation 'org.reflections:reflections:0.10.2'
|
||||
implementation 'org.apache.commons:commons-text:1.11.0'
|
||||
implementation 'net.java.dev.jna:jna:5.8.0'
|
||||
implementation 'org.modelmapper:modelmapper:3.2.0'
|
||||
implementation 'org.iban4j:iban4j:3.2.7-RELEASE'
|
||||
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.4.0'
|
||||
implementation 'org.reflections:reflections:0.9.12'
|
||||
|
||||
compileOnly 'org.projectlombok:lombok'
|
||||
testCompileOnly 'org.projectlombok:lombok'
|
||||
@ -88,9 +85,9 @@ dependencies {
|
||||
testImplementation 'org.testcontainers:junit-jupiter'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||
testImplementation 'org.testcontainers:postgresql'
|
||||
testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
|
||||
testImplementation 'com.tngtech.archunit:archunit-junit5:1.2.1'
|
||||
testImplementation 'io.rest-assured:spring-mock-mvc'
|
||||
testImplementation 'org.hamcrest:hamcrest-core:3.0'
|
||||
testImplementation 'org.hamcrest:hamcrest-core:2.2'
|
||||
testImplementation 'org.pitest:pitest-junit5-plugin:1.2.1'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api'
|
||||
}
|
||||
@ -120,8 +117,8 @@ openapiProcessor {
|
||||
springRoot {
|
||||
processorName 'spring'
|
||||
processor 'io.openapiprocessor:openapi-processor-spring:2022.5'
|
||||
apiPath "$projectDir/src/main/resources/api-definition/api-definition.yaml"
|
||||
mapping "$projectDir/src/main/resources/api-definition/api-mappings.yaml"
|
||||
apiPath "$projectDir/src/main/resources/api-definition.yaml"
|
||||
mapping "$projectDir/src/main/resources/api-mappings.yaml"
|
||||
targetDir "$buildDir/generated/sources/openapi-javax"
|
||||
showWarnings true
|
||||
openApiNullable true
|
||||
@ -257,7 +254,7 @@ test {
|
||||
'net.hostsharing.hsadminng.**.generated.**',
|
||||
]
|
||||
useJUnitPlatform {
|
||||
excludeTags 'importOfficeData', 'importHostingData', 'scenarioTest'
|
||||
excludeTags 'import'
|
||||
}
|
||||
}
|
||||
jacocoTestReport {
|
||||
@ -279,7 +276,7 @@ jacocoTestCoverageVerification {
|
||||
violationRules {
|
||||
rule {
|
||||
limit {
|
||||
minimum = 0.80 // TODO.test: improve instruction coverage
|
||||
minimum = 0.92
|
||||
}
|
||||
}
|
||||
|
||||
@ -291,20 +288,15 @@ jacocoTestCoverageVerification {
|
||||
element = 'CLASS'
|
||||
excludes = [
|
||||
'net.hostsharing.hsadminng.**.generated.**',
|
||||
'net.hostsharing.hsadminng.rbac.test.dom.TestDomainEntity',
|
||||
'net.hostsharing.hsadminng.HsadminNgApplication',
|
||||
'net.hostsharing.hsadminng.ping.PingController',
|
||||
'net.hostsharing.hsadminng.rbac.generator.*',
|
||||
'net.hostsharing.hsadminng.rbac.grant.RbacGrantsDiagramService',
|
||||
'net.hostsharing.hsadminng.rbac.grant.RbacGrantsDiagramService.Node',
|
||||
'net.hostsharing.hsadminng.**.*Repository',
|
||||
'net.hostsharing.hsadminng.mapper.Mapper'
|
||||
]
|
||||
|
||||
limit {
|
||||
counter = 'LINE'
|
||||
value = 'COVEREDRATIO'
|
||||
minimum = 0.75 // TODO.test: improve line coverage
|
||||
minimum = 0.98
|
||||
}
|
||||
}
|
||||
rule {
|
||||
@ -318,7 +310,7 @@ jacocoTestCoverageVerification {
|
||||
limit {
|
||||
counter = 'BRANCH'
|
||||
value = 'COVEREDRATIO'
|
||||
minimum = 0.00 // TODO.test: improve branch coverage
|
||||
minimum = 1.00
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -346,30 +338,19 @@ tasks.register('importHostingAssets', Test) {
|
||||
mustRunAfter spotlessJava
|
||||
}
|
||||
|
||||
tasks.register('scenarioTests', Test) {
|
||||
useJUnitPlatform {
|
||||
includeTags 'scenarioTest'
|
||||
}
|
||||
|
||||
group 'verification'
|
||||
description 'run the import jobs as tests'
|
||||
|
||||
mustRunAfter spotlessJava
|
||||
}
|
||||
|
||||
// pitest mutation testing
|
||||
pitest {
|
||||
targetClasses = ['net.hostsharing.hsadminng.**']
|
||||
excludedClasses = [
|
||||
'net.hostsharing.hsadminng.config.**',
|
||||
// 'net.hostsharing.hsadminng.**.*Controller',
|
||||
'net.hostsharing.hsadminng.**.*Controller',
|
||||
'net.hostsharing.hsadminng.**.generated.**'
|
||||
]
|
||||
|
||||
targetTests = ['net.hostsharing.hsadminng.**.*UnitTest', 'net.hostsharing.hsadminng.**.*RestTest']
|
||||
excludedTestClasses = ['**AcceptanceTest*', '**IntegrationTest*']
|
||||
|
||||
pitestVersion = '1.17.0'
|
||||
pitestVersion = '1.15.3'
|
||||
junit5PluginVersion = '1.1.0'
|
||||
|
||||
threads = 4
|
||||
@ -404,51 +385,3 @@ tasks.named("dependencyUpdates").configure {
|
||||
isNonStable(it.candidate.version)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Generate HTML from Markdown scenario-test-reports using Pandoc:
|
||||
tasks.register('convertMarkdownToHtml') {
|
||||
description = 'Generates HTML from Markdown scenario-test-reports using Pandoc.'
|
||||
group = 'Conversion'
|
||||
|
||||
// Define the template file and input directory
|
||||
def templateFile = file('doc/scenarios/.template.html')
|
||||
|
||||
// Task configuration and execution
|
||||
doFirst {
|
||||
// Check if pandoc is installed
|
||||
try {
|
||||
exec {
|
||||
commandLine 'pandoc', '--version'
|
||||
}
|
||||
} catch (Exception) {
|
||||
throw new GradleException("Pandoc is not installed or not found in the system path.")
|
||||
}
|
||||
|
||||
// Check if the template file exists
|
||||
if (!templateFile.exists()) {
|
||||
throw new GradleException("Template file 'doc/scenarios/.template.html' not found.")
|
||||
}
|
||||
}
|
||||
|
||||
doLast {
|
||||
// Gather all Markdown files in the current directory
|
||||
fileTree(dir: '.', include: 'build/doc/scenarios/*.md').each { file ->
|
||||
// Corrected way to create the output file path
|
||||
def outputFile = new File(file.parent, file.name.replaceAll(/\.md$/, '.html'))
|
||||
|
||||
// Execute pandoc for each markdown file
|
||||
exec {
|
||||
commandLine 'pandoc', file.absolutePath, '--template', templateFile.absolutePath, '-o', outputFile.absolutePath
|
||||
}
|
||||
|
||||
println "Converted ${file.name} to ${outputFile.name}"
|
||||
}
|
||||
}
|
||||
}
|
||||
convertMarkdownToHtml.dependsOn scenarioTests
|
||||
|
||||
// shortcut for compiling all files
|
||||
tasks.register('compile') {
|
||||
dependsOn 'compileJava', 'compileTestJava'
|
||||
}
|
||||
|
@ -1,119 +0,0 @@
|
||||
# Handling Automatic Creation of Hosting Assets for New Booking Items
|
||||
|
||||
**Status:**
|
||||
- [x] proposed by (Michael Hönnig)
|
||||
- [ ] accepted by (Participants)
|
||||
- [ ] rejected by (Participants)
|
||||
- [ ] superseded by (superseding ADR)
|
||||
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
When a customer creates a new booking item (e.g., `MANAGED_WEBSPACE`), the system must automatically create the related hosting asset.
|
||||
This process can sometimes fail or require additional data from the user, e.g. installing a DNS verification key, or a hostmaster, e.g. the target server to use.
|
||||
|
||||
The challenge is how to handle this automatic creation process while dealing with missing data, asynchronicity and failures while ensuring system consistency and proper user notification.
|
||||
|
||||
|
||||
### Technical Background
|
||||
|
||||
The creation of hosting assets can occur synchronously (in simple cases) or asynchronously (when additional steps like manual verification are needed).
|
||||
For example, a `DOMAIN_SETUP` hosting asset may require DNS verification from the user, and until this is provided, the related domain cannot be fully set up.
|
||||
|
||||
Additionally, not all data needed for creating the hosting asset is stored in the booking item.
|
||||
It's part of the HTTP request and later stored in the hosting asset, but we also need to store it before the hosting asset can be created asynchronously.
|
||||
|
||||
Current system behavior involves returning HTTP 201 upon booking item creation, but the automatic hosting asset creation might fail due to missing information.
|
||||
The system needs to manage the creation process in a way that ensures valid hosting assets are created and informs the user of any actions required while still returning a 201 HTTP code, not an error code.
|
||||
|
||||
|
||||
## Considered Options
|
||||
|
||||
For storing the data needed for the hosting-asset creation:
|
||||
|
||||
* STORAGE-1: Store temporary asset data in the `BookingItemEntity`, e.g. a JSON column.
|
||||
And delete the value of that column, once the hosting assets got successfully created.
|
||||
* STORAGE-2: Create hosting assets immediately, even if invalid, but mark them as "inactive" until completed and fully validated.
|
||||
* STORAGE-3: Store the asset data in a kind of event- or job-queue, which get deleted once the hosting-asset got successfully created.
|
||||
|
||||
For the user-notification status:
|
||||
|
||||
* STATUS-1: Introduce a status field in the booking-items.
|
||||
* STATUS-2: Store the status in the event-/job-queue entries.
|
||||
|
||||
### STORAGE-1: Temporary Data Storage in `BookingItemEntity`
|
||||
|
||||
Store asset-related data (e.g., domain name) in a temporary column or JSON field in the `BookingItemEntity` until the hosting assets are successfully created.
|
||||
Once assets are created, the temporary data is deleted to avoid inconsistencies.
|
||||
|
||||
#### Advantages
|
||||
- Easy to implement.
|
||||
|
||||
#### Disadvantages
|
||||
- Needs either a separate map of properties in the booking-item.
|
||||
- Or, if stored as a JSON field in the booking-item-resources, these are misused.
|
||||
- Requires additional cleanup logic to remove stale data.
|
||||
|
||||
### STORAGE-2: Inactive Hosting Assets Until Validation
|
||||
|
||||
Create the hosting assets immediately upon booking item creation but mark them as "inactive" until all required information (e.g., verification code) is provided and validation is complete.
|
||||
|
||||
#### Advantages
|
||||
- Avoids temporary external data storage for the hosting-assets.
|
||||
|
||||
#### Disadvantages
|
||||
- Validation becomes more complex as some properties need to be validated, others not.
|
||||
And some properties even need special treatment for new entities, which then becomes vague.
|
||||
- Inactive assets have to be filtered from operational assets.
|
||||
- Potential risk of incomplete or inconsistent assets being created, which may require correction.
|
||||
- Difficult to write tests for all possible combinations of validations.
|
||||
|
||||
### STORAGE-3: Event-Based Approach
|
||||
|
||||
The hosting asset data required for creation us passed to the API and stored in a `BookingItemCreatedEvent`.
|
||||
If hosting asset creation cannot happen synchronously, the event is stored and processed asynchronously in batches, retrying failed asset creation as needed.
|
||||
|
||||
#### Advantages
|
||||
- Clean-data-structure (separation of concerns).
|
||||
- Clear separation between booking item creation and hosting asset creation.
|
||||
- Only valid assets in the database.
|
||||
- Can handle complex asynchronous processes (like waiting for external verification) in a clean and structured manner.
|
||||
- Easier to manage retries and failures in asset creation without complicating the booking item structure.
|
||||
|
||||
#### Disadvantages
|
||||
- At the Spring controller level, the whole JSON is already converted into Java objects,
|
||||
but for storing the asset data in the even, we need JSON again.
|
||||
This could is not just a performance-overhead but could also lead to inconsistencies.
|
||||
|
||||
### STATUS-1: Store hosting-asset-creation-status in the `BookingItemEntity`
|
||||
|
||||
A status field would be added to booking-items to track the creation state of related hosting assets.
|
||||
The users could check their booking-items for the status of the hosting-asset creation, error messages and further instructions.
|
||||
|
||||
#### Advantages
|
||||
- Easy to implement.
|
||||
|
||||
#### Disadvantages
|
||||
- Adds a field to the booking-item which is makes no sense anymore once the related hosting asset is created.
|
||||
|
||||
|
||||
### Status-2: Store hosting-asset-creation-status in the `BookingItemCreateEvent`
|
||||
|
||||
A status field would be added to the booking-item-created event and get updated with the latest messages any time we try to create the hosting-asset.
|
||||
|
||||
#### Advantages
|
||||
- Clean-data-structure (separation of concerns)
|
||||
|
||||
#### Disadvantages
|
||||
- Accessing the status requires querying the event queue.
|
||||
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
**Chosen Option: STORAGE-3 with STATUS-2 (Event-Based Approach with `BookingItemCreatedEvent`)**
|
||||
|
||||
The event-based approach was selected as the best solution for handling automatic hosting asset creation. This option provides a clear separation between booking item creation and hosting asset creation, ensuring that no invalid or incomplete assets are created. The asynchronous nature of the event system allows for retries and external validation steps (such as user-entered verification codes) without disrupting the overall flow.
|
||||
|
||||
By using `BookingItemCreatedEvent` to store the hosting-asset data and the status,
|
||||
we don't need to misuse other data structures for temporary data
|
||||
and therefore hava a clean separation of concerns.
|
@ -1,124 +0,0 @@
|
||||
### hsadminNg fachliches Glossar
|
||||
|
||||
<!--
|
||||
Currently, this business glossary is only available in German because in many cases,
|
||||
the German terms are important for comprehensibility for those using this software.
|
||||
-->
|
||||
|
||||
Dieses ist eine Sammlung von Fachbegriffen, die in diesem Projekt benutzt werden.
|
||||
Ebenfalls aufgenommen sind technische Begriffe, die für Benutzer für das Verständnis der Schnittstellen nötig sind.
|
||||
|
||||
Falls etwas fehlt, bitte Bescheid geben.
|
||||
|
||||
|
||||
#### Partner
|
||||
|
||||
In diesem System ist ein _Partner_ grundsätzlich jeglicher Geschäftspartner der _Hostsharing eG_.
|
||||
Dies können grundsätzlich Kunden, siehe [Debitor](#Debitor), wie Lieferanten sein.
|
||||
Derzeit sind aber nur Debitoren implementiert.
|
||||
|
||||
Des Weiteren gibt es für jeden _Partner_ eine fünfstellige Partnernummer mit dem Prefix 'P-' (z.B. `P-123454`)
|
||||
sowie Zusatzinformationen (z.B. Registergerichtnummer oder Geburtsdatum), die zur genauen Identifikation benötigt werden.
|
||||
|
||||
Für einen _Partner_ kann es gleichzeitig mehrere [Debitoren](#Debitor)
|
||||
und zeitlich nacheinander mehrere [Mitgliedschaften](#Mitgliedschaft) geben.
|
||||
|
||||
Partner sind grundsätzlich als ist [Relation](#Relation) der Vertragsperson mit der Person _Hostsharing eG_ implementiert.
|
||||
|
||||
|
||||
### Debitor
|
||||
|
||||
Ein `Debitor` ist quasi ein Rechnungsempfänger für einen [Partner](#Partner).
|
||||
|
||||
Für einen _Partner_ kann es gleichzeitig mehrere [Debitoren](#Debitor) geben,
|
||||
z.B. für spezielle Projekte des Kunden oder verbundene Organisationen.
|
||||
|
||||
Des Weiteren gibt es für jeden _Partner_ eine fünfstellige Partnernummer mit dem Prefix 'P-' (z.B. `P-123454`)
|
||||
sowie Zusatzinformationen (z.B. Registergerichtsnummer oder Geburtsdatum), die zur genauen Identifikation benötigt werden.
|
||||
|
||||
Debitoren sind grundsätzlich als ist [Relation](#Relation) der Vertragsperson mit der Person des Vertragspartners implementiert.
|
||||
|
||||
|
||||
#### Relation
|
||||
|
||||
Eine _Relation_ ist eine typisierte und mit Kontaktdaten versehene Beziehung einer (_Holder_)-Person zu einer _Anchor_-Person.
|
||||
|
||||
Eine Relation ist eine Art Geschäftsrolle, wir haben hier aber keinen Begriff mit 'Rolle' verwendet,
|
||||
weil 'Role' (engl.) zu leicht mit der [RBAC-Rolle](#RBAC-Role) verwechselt werden könnte.
|
||||
|
||||
Die _Relation_ ist auch ein technisches Konzept und gehört nicht zur Domänensprache.
|
||||
Dieses Konzept ist jedoch für das Verständnis der ([API](#API)) notwendig.
|
||||
|
||||
|
||||
#### Ex-Partner
|
||||
|
||||
Ex-Partner bilden [Personen](#Person) ab, die vormals [Partner](#Partner) waren.
|
||||
Diese bleiben dadurch informationshalber im System verfügbar.
|
||||
|
||||
Implementiert ist der _Ex-Partner_ als eine besondere Form der [Relation](#Relation)
|
||||
der Person des Ex-Partner (_Holder_) zum neuen Partner (_Anchor_) dargestellt.
|
||||
Dieses kann zu einer Kettenbildung führen.
|
||||
|
||||
|
||||
#### Representative-Contact (ehemals _contractual_)
|
||||
|
||||
Ein _Representative_ ist eine natürliche Person, die für eine nicht-natürliche Person vertretungsberechtigt ist.
|
||||
|
||||
Implementiert ist der _Representative_ als eine besondere Form der [Relation](#Relation)
|
||||
der Person des Repräsentanten (_Holder_) zur repräsentierten Person (_Anchor_) dargestellt.
|
||||
|
||||
|
||||
### VIP-Contact
|
||||
|
||||
Ein _VIP-Contact_ ist eine natürliche Person, die für einen Geschäftspartner eine wichtige Funktion übernimmt,
|
||||
nicht aber deren offizieller Repräsentant ist.
|
||||
|
||||
Implementiert ist der _VIP-Contact_ als eine besondere Form der [Relation](#Relation)
|
||||
der Person des VIP-Contact (_Holder_) zur repräsentierten Person (_Anchor_) dargestellt.
|
||||
|
||||
|
||||
### Operations-Contact
|
||||
|
||||
Ein _Operations-_Contact_ ist_ eine natürliche Person, die für einen Geschäftspartner technischer Ansprechpartner ist.
|
||||
|
||||
Ein Seiteneffekt ist, dass diese Person im Ticketsystem Znuny direkt dem Geschäftspartner zugeordnet werden kann.
|
||||
|
||||
Im Legacy System waren das die Kontakte mit der Rolle `operation` und `silent`.
|
||||
|
||||
Implementiert ist der _Operations-Contact_ als eine besondere Form der [Relation](#Relation)
|
||||
der Person des _Operations-Contact_ (_Holder_) zur repräsentierten Person (_Anchor_) dargestellt.
|
||||
|
||||
|
||||
### OperationsAlert-Contact
|
||||
|
||||
Ein _OperationsAlert-_Contact_ ist_ eine natürliche Person, die für einen Geschäftspartner bei technischen Probleme kontaktiert werden soll.
|
||||
|
||||
Im Legacy System waren das die Kontakte mit der Rolle `operation`.
|
||||
|
||||
Implementiert ist der _OperationsAlert-Contact_ als eine besondere Form der [Relation](#Relation)
|
||||
der Person des _OperationsAlert-Contact_ (_Holder_) zur repräsentierten Person (_Anchor_) dargestellt.
|
||||
|
||||
|
||||
### Subscriber-Contact
|
||||
|
||||
Ein _Subscriber-_Contact_ ist_ eine natürliche Person, die für einen Geschäftspartner eine bestimmte Mailingliste abonniert.
|
||||
|
||||
Implementiert ist der _Subscriber-Contact_ als eine besondere Form der [Relation](#Relation)
|
||||
der Person des _Subscriber-Contact_ (_Holder_) zur repräsentierten Person (_Anchor_) dargestellt.
|
||||
Zusätzlich wird diese Relation mit dem Kurznamen der abonnierten Mailingliste markiert.
|
||||
|
||||
|
||||
#### Anchor / Relation-Anchor
|
||||
|
||||
siehe [Relation](#Relation)
|
||||
|
||||
|
||||
#### Holder / Relation-Holder
|
||||
|
||||
siehe [Relation](#Relation)
|
||||
|
||||
|
||||
#### API
|
||||
|
||||
Und API (Application-Programming-Interface) verstehen wir eine über HTTPS angesprochene programmatisch bedienbare Schnittstell
|
||||
zur Funktionalität des hsAdmin-NG-Systems.
|
@ -64,7 +64,7 @@ classDiagram
|
||||
}
|
||||
|
||||
class partner-MeierGmbH {
|
||||
+Numeric partnerNumber: P-12345
|
||||
+Numeric partnerNumber: 12345
|
||||
+Relation partnerRel
|
||||
}
|
||||
partner-MeierGmbH *-- rel-MeierGmbH
|
||||
@ -115,7 +115,7 @@ classDiagram
|
||||
+BankAccount refundBankAccount
|
||||
+String defaultPrefix: mei
|
||||
}
|
||||
debitor-MeierGmbH o-- partner-MeierGmbH
|
||||
debitor-MeierGmbH o-- rel-MeierGmbH
|
||||
debitor-MeierGmbH *-- rel-MeierGmbH-Buha
|
||||
|
||||
class contactData-MeierGmbH-Buha {
|
||||
|
@ -199,21 +199,21 @@ Limit (cost=6549.08..6549.35 rows=54 width=16)
|
||||
Group Key: grants.descendantuuid
|
||||
-> CTE Scan on grants (cost=0.00..22.06 rows=1103 width=16)
|
||||
-> Index Only Scan using rbacobject_objecttable_uuid_key on rbacobject obj (cost=0.28..0.31 rows=1 width=16)
|
||||
Index Cond: ((objecttable = 'hs_hosting.asset'::text) AND (uuid = perm.objectuuid))
|
||||
Index Cond: ((objecttable = 'hs_hosting_asset'::text) AND (uuid = perm.objectuuid))
|
||||
```
|
||||
|
||||
### Office-Relation-Query
|
||||
|
||||
```SQL
|
||||
SELECT hore1_0.uuid,a1_0.uuid,a1_0.familyname,a1_0.givenname,a1_0.persontype,a1_0.salutation,a1_0.title,a1_0.tradename,a1_0.version,c1_0.uuid,c1_0.caption,c1_0.emailaddresses,c1_0.phonenumbers,c1_0.postaladdress,c1_0.version,h1_0.uuid,h1_0.familyname,h1_0.givenname,h1_0.persontype,h1_0.salutation,h1_0.title,h1_0.tradename,h1_0.version,hore1_0.mark,hore1_0.type,hore1_0.version
|
||||
FROM hs_office.relation_rv hore1_0
|
||||
LEFT JOIN hs_office.person_rv a1_0 ON a1_0.uuid=hore1_0.anchoruuid
|
||||
LEFT JOIN hs_office.contact_rv c1_0 ON c1_0.uuid=hore1_0.contactuuid
|
||||
LEFT JOIN hs_office.person_rv h1_0 ON h1_0.uuid=hore1_0.holderuuid
|
||||
FROM hs_office_relation_rv hore1_0
|
||||
LEFT JOIN hs_office_person_rv a1_0 ON a1_0.uuid=hore1_0.anchoruuid
|
||||
LEFT JOIN hs_office_contact_rv c1_0 ON c1_0.uuid=hore1_0.contactuuid
|
||||
LEFT JOIN hs_office_person_rv h1_0 ON h1_0.uuid=hore1_0.holderuuid
|
||||
WHERE hore1_0.uuid=$1
|
||||
```
|
||||
|
||||
That query on the `hs_office.relation_rv`-table joins the three references anchor-person, holder-person and contact.
|
||||
That query on the `hs_office_relation_rv`-table joins the three references anchor-person, holder-person and contact.
|
||||
|
||||
|
||||
### Total-Query-Time > Total-Import-Runtime
|
||||
@ -270,21 +270,21 @@ At this point, the import took 21mins with these statistics:
|
||||
|
||||
| query | calls | total_m | mean_ms |
|
||||
|-------|-------|---------|---------|
|
||||
| select hore1_0.uuid,a1_0.uuid,a1_0.familyname,a1_0.givenname,a1_0.persontype,a1_0.salutation,a1_0.title,a1_0.tradename,a1_0.version,c1_0.uuid,c1_0.caption,c1_0.emailaddresses,c1_0.phonenumbers,c1_0.postaladdress, c1_0.version,h1_0.uuid,h1_0.familyname,h1_0.givenname,h1_0.persontype,h1_0.salutation,h1_0.title,h1_0.tradename,h1_0.version,hore1_0.mark,hore1_0.type,hore1_0.version from public.hs_office.relation_rv hore1_0 left join public.hs_office.person_rv a1_0 on a1_0.uuid=hore1_0.anchoruuid left join public.hs_office.contact_rv c1_0 on c1_0.uuid=hore1_0.contactuuid left join public.hs_office.person_rv h1_0 on h1_0.uuid=hore1_0.holderuuid where hore1_0.uuid=$1 | 517 | 11 | 1282 |
|
||||
| select hope1_0.uuid,hope1_0.familyname,hope1_0.givenname,hope1_0.persontype,hope1_0.salutation,hope1_0.title,hope1_0.tradename,hope1_0.version from public.hs_office.person_rv hope1_0 where hope1_0.uuid=$1 | 973 | 4 | 254 |
|
||||
| select hoce1_0.uuid,hoce1_0.caption,hoce1_0.emailaddresses,hoce1_0.phonenumbers,hoce1_0.postaladdress,hoce1_0.version from public.hs_office.contact_rv hoce1_0 where hoce1_0.uuid=$1 | 973 | 4 | 253 |
|
||||
| select hore1_0.uuid,a1_0.uuid,a1_0.familyname,a1_0.givenname,a1_0.persontype,a1_0.salutation,a1_0.title,a1_0.tradename,a1_0.version,c1_0.uuid,c1_0.caption,c1_0.emailaddresses,c1_0.phonenumbers,c1_0.postaladdress, c1_0.version,h1_0.uuid,h1_0.familyname,h1_0.givenname,h1_0.persontype,h1_0.salutation,h1_0.title,h1_0.tradename,h1_0.version,hore1_0.mark,hore1_0.type,hore1_0.version from public.hs_office_relation_rv hore1_0 left join public.hs_office_person_rv a1_0 on a1_0.uuid=hore1_0.anchoruuid left join public.hs_office_contact_rv c1_0 on c1_0.uuid=hore1_0.contactuuid left join public.hs_office_person_rv h1_0 on h1_0.uuid=hore1_0.holderuuid where hore1_0.uuid=$1 | 517 | 11 | 1282 |
|
||||
| select hope1_0.uuid,hope1_0.familyname,hope1_0.givenname,hope1_0.persontype,hope1_0.salutation,hope1_0.title,hope1_0.tradename,hope1_0.version from public.hs_office_person_rv hope1_0 where hope1_0.uuid=$1 | 973 | 4 | 254 |
|
||||
| select hoce1_0.uuid,hoce1_0.caption,hoce1_0.emailaddresses,hoce1_0.phonenumbers,hoce1_0.postaladdress,hoce1_0.version from public.hs_office_contact_rv hoce1_0 where hoce1_0.uuid=$1 | 973 | 4 | 253 |
|
||||
| call rbac.grantRoleToRole(roleUuid, superRoleUuid, superRoleDesc.assumed) | 31316 | 0 | 1 |
|
||||
| call buildRbacSystemForHsHostingAsset(NEW) | 2258 | 0 | 7 |
|
||||
| select * from rbac.isGranted(array[granteeId], grantedId) | 44613 | 0 | 0 |
|
||||
| insert into public.hs_hosting.asset_rv (alarmcontactuuid,assignedtoassetuuid,bookingitemuuid,caption,config,identifier,parentassetuuid,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) | 2207 | 0 | 7 |
|
||||
| insert into hs_hosting.asset (alarmcontactuuid, version, bookingitemuuid, type, parentassetuuid, assignedtoassetuuid, config, uuid, identifier, caption) values (new.alarmcontactuuid, new. version, new. bookingitemuuid, new. type, new. parentassetuuid, new. assignedtoassetuuid, new. config, new. uuid, new. identifier, new. caption) returning * | 2207 | 0 | 7 |
|
||||
| insert into public.hs_office.relation_rv (anchoruuid,contactuuid,holderuuid,mark,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7) | 1261 | 0 | 9 |
|
||||
| insert into hs_office.relation (uuid, version, anchoruuid, holderuuid, contactuuid, type, mark) values (new.uuid, new. version, new. anchoruuid, new. holderuuid, new. contactuuid, new. type, new. mark) returning * | 1261 | 0 | 9 |
|
||||
| insert into public.hs_hosting_asset_rv (alarmcontactuuid,assignedtoassetuuid,bookingitemuuid,caption,config,identifier,parentassetuuid,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) | 2207 | 0 | 7 |
|
||||
| insert into hs_hosting_asset (alarmcontactuuid, version, bookingitemuuid, type, parentassetuuid, assignedtoassetuuid, config, uuid, identifier, caption) values (new.alarmcontactuuid, new. version, new. bookingitemuuid, new. type, new. parentassetuuid, new. assignedtoassetuuid, new. config, new. uuid, new. identifier, new. caption) returning * | 2207 | 0 | 7 |
|
||||
| insert into public.hs_office_relation_rv (anchoruuid,contactuuid,holderuuid,mark,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7) | 1261 | 0 | 9 |
|
||||
| insert into hs_office_relation (uuid, version, anchoruuid, holderuuid, contactuuid, type, mark) values (new.uuid, new. version, new. anchoruuid, new. holderuuid, new. contactuuid, new. type, new. mark) returning * | 1261 | 0 | 9 |
|
||||
| call buildRbacSystemForHsOfficeRelation(NEW) | 1276 | 0 | 8 |
|
||||
| with recursive grants as ( select descendantUuid, ascendantUuid from RbacGrants where descendantUuid = grantedId union all select ""grant"".descendantUuid, ""grant"".ascendantUuid from RbacGrants ""grant"" inner join grants recur on recur.ascendantUuid = ""grant"".descendantUuid ) select exists ( select $3 from grants where ascendantUuid = any(granteeIds) ) or grantedId = any(granteeIds) | 47540 | 0 | 0 |
|
||||
| insert into RbacGrants (grantedByTriggerOf, ascendantuuid, descendantUuid, assumed) values (currentTriggerObjectUuid(), superRoleId, subRoleId, doAssume) on conflict do nothing" | 40472 | 0 | 0 |
|
||||
| insert into public.hs_booking.item_rv (caption,parentitemuuid,projectuuid,resources,type,validity,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8) | 926 | 0 | 7 |
|
||||
| insert into hs_booking.item (resources, version, projectuuid, type, parentitemuuid, validity, uuid, caption) values (new.resources, new. version, new. projectuuid, new. type, new. parentitemuuid, new. validity, new. uuid, new. caption) returning * | 926 | 0 | 7 |
|
||||
| insert into public.hs_booking_item_rv (caption,parentitemuuid,projectuuid,resources,type,validity,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8) | 926 | 0 | 7 |
|
||||
| insert into hs_booking_item (resources, version, projectuuid, type, parentitemuuid, validity, uuid, caption) values (new.resources, new. version, new. projectuuid, new. type, new. parentitemuuid, new. validity, new. uuid, new. caption) returning * | 926 | 0 | 7 |
|
||||
|
||||
|
||||
The slowest query now was fetching Relations joined with Contact, Anchor-Person and Holder-Person, for all tables using the restricted (RBAC) views (_rv).
|
||||
@ -294,20 +294,20 @@ We changed these mappings from `EAGER` (default) to `LAZY` to `@ManyToOne(fetch
|
||||
:::small
|
||||
| query | calls | total (min) | mean (ms) |
|
||||
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------|-------------|----------|
|
||||
| select hope1_0.uuid,hope1_0.familyname,hope1_0.givenname,hope1_0.persontype,hope1_0.salutation,hope1_0.title,hope1_0.tradename,hope1_0.version from public.hs_office.person_rv hope1_0 where hope1_0.uuid=$1 | 1015 | 4 | 238 |
|
||||
| select hore1_0.uuid,hore1_0.anchoruuid,hore1_0.contactuuid,hore1_0.holderuuid,hore1_0.mark,hore1_0.type,hore1_0.version from public.hs_office.relation_rv hore1_0 where hore1_0.uuid=$1 | 517 | 4 | 439 |
|
||||
| select hoce1_0.uuid,hoce1_0.caption,hoce1_0.emailaddresses,hoce1_0.phonenumbers,hoce1_0.postaladdress,hoce1_0.version from public.hs_office.contact_rv hoce1_0 where hoce1_0.uuid=$1 | 497 | 2 | 213 |
|
||||
| select hope1_0.uuid,hope1_0.familyname,hope1_0.givenname,hope1_0.persontype,hope1_0.salutation,hope1_0.title,hope1_0.tradename,hope1_0.version from public.hs_office_person_rv hope1_0 where hope1_0.uuid=$1 | 1015 | 4 | 238 |
|
||||
| select hore1_0.uuid,hore1_0.anchoruuid,hore1_0.contactuuid,hore1_0.holderuuid,hore1_0.mark,hore1_0.type,hore1_0.version from public.hs_office_relation_rv hore1_0 where hore1_0.uuid=$1 | 517 | 4 | 439 |
|
||||
| select hoce1_0.uuid,hoce1_0.caption,hoce1_0.emailaddresses,hoce1_0.phonenumbers,hoce1_0.postaladdress,hoce1_0.version from public.hs_office_contact_rv hoce1_0 where hoce1_0.uuid=$1 | 497 | 2 | 213 |
|
||||
| call rbac.grantRoleToRole(roleUuid, superRoleUuid, superRoleDesc.assumed) | 31316 | 0 | 1 |
|
||||
| select * from rbac.isGranted(array[granteeId], grantedId) | 44613 | 0 | 0 |
|
||||
| call buildRbacSystemForHsHostingAsset(NEW) | 2258 | 0 | 7 |
|
||||
| insert into public.hs_hosting.asset_rv (alarmcontactuuid,assignedtoassetuuid,bookingitemuuid,caption,config,identifier,parentassetuuid,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) | 2207 | 0 | 7 |
|
||||
| insert into hs_hosting.asset (alarmcontactuuid, version, bookingitemuuid, type, parentassetuuid, assignedtoassetuuid, config, uuid, identifier, caption) values (new.alarmcontactuuid, new. version, new. bookingitemuuid, new. type, new. parentassetuuid, new. assignedtoassetuuid, new. config, new. uuid, new. identifier, new. caption) returning * | 2207 | 0 | 7 |
|
||||
| insert into public.hs_hosting_asset_rv (alarmcontactuuid,assignedtoassetuuid,bookingitemuuid,caption,config,identifier,parentassetuuid,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) | 2207 | 0 | 7 |
|
||||
| insert into hs_hosting_asset (alarmcontactuuid, version, bookingitemuuid, type, parentassetuuid, assignedtoassetuuid, config, uuid, identifier, caption) values (new.alarmcontactuuid, new. version, new. bookingitemuuid, new. type, new. parentassetuuid, new. assignedtoassetuuid, new. config, new. uuid, new. identifier, new. caption) returning * | 2207 | 0 | 7 |
|
||||
| with recursive grants as ( select descendantUuid, ascendantUuid from RbacGrants where descendantUuid = grantedId union all select ""grant"".descendantUuid, ""grant"".ascendantUuid from RbacGrants ""grant"" inner join grants recur on recur.ascendantUuid = ""grant"".descendantUuid ) select exists ( select $3 from grants where ascendantUuid = any(granteeIds) ) or grantedId = any(granteeIds) | 47538 | 0 | 0 |
|
||||
insert into public.hs_office.relation_rv (anchoruuid,contactuuid,holderuuid,mark,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7) | 1261 | 0 | 8 |
|
||||
| insert into hs_office.relation (uuid, version, anchoruuid, holderuuid, contactuuid, type, mark) values (new.uuid, new. version, new. anchoruuid, new. holderuuid, new. contactuuid, new. type, new. mark) returning * | 1261 | 0 | 8 |
|
||||
insert into public.hs_office_relation_rv (anchoruuid,contactuuid,holderuuid,mark,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7) | 1261 | 0 | 8 |
|
||||
| insert into hs_office_relation (uuid, version, anchoruuid, holderuuid, contactuuid, type, mark) values (new.uuid, new. version, new. anchoruuid, new. holderuuid, new. contactuuid, new. type, new. mark) returning * | 1261 | 0 | 8 |
|
||||
| call buildRbacSystemForHsOfficeRelation(NEW) | 1276 | 0 | 7 |
|
||||
| insert into public.hs_booking.item_rv (caption,parentitemuuid,projectuuid,resources,type,validity,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8) | 926 | 0 | 7 |
|
||||
| insert into hs_booking.item (resources, version, projectuuid, type, parentitemuuid, validity, uuid, caption) values (new.resources, new. version, new. projectuuid, new. type, new. parentitemuuid, new. validity, new. uuid, new. caption) returning * | 926 | 0 | 7 |
|
||||
| insert into public.hs_booking_item_rv (caption,parentitemuuid,projectuuid,resources,type,validity,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8) | 926 | 0 | 7 |
|
||||
| insert into hs_booking_item (resources, version, projectuuid, type, parentitemuuid, validity, uuid, caption) values (new.resources, new. version, new. projectuuid, new. type, new. parentitemuuid, new. validity, new. uuid, new. caption) returning * | 926 | 0 | 7 |
|
||||
insert into RbacGrants (grantedByTriggerOf, ascendantuuid, descendantUuid, assumed) values (currentTriggerObjectUuid(), superRoleId, subRoleId, doAssume) on conflict do nothing | 40472 | 0 | 0 |
|
||||
|
||||
Now, finally, the total runtime of the import was down to 12 minutes. This is repeatable, where originally, the import took about 25mins in most cases and just rarely - and for unknown reasons - 10min.
|
||||
@ -318,7 +318,7 @@ But once UnixUser and EmailAlias assets got added to the import, the total time
|
||||
|
||||
This was not acceptable, especially not, considering that domains, email-addresses and database-assets are almost 10 times that number and thus the import would go up to over 1100min which is 20 hours.
|
||||
|
||||
In a first step, a `HsHostingAssetRawEntity` was created, mapped to the raw table (hs_hosting.asset) not to the RBAC-view (hs_hosting.asset_rv). Unfortunately we did not keep measurements, but that was only part of the problem anyway.
|
||||
In a first step, a `HsHostingAssetRawEntity` was created, mapped to the raw table (hs_hosting_asset) not to the RBAC-view (hs_hosting_asset_rv). Unfortunately we did not keep measurements, but that was only part of the problem anyway.
|
||||
|
||||
The main problem was, that there is something strange with persisting (`EntityManager.persist`) for EmailAlias assets. Where importing UnixUsers was mostly slow due to RBAC SELECT-permission checks, persisting EmailAliases suddenly created about a million (in numbers 1.000.000) SQL UPDATE statements after the INSERT, all with the same data, just increased version number (used for optimistic locking). We were not able to figure out why this happened.
|
||||
|
||||
@ -330,22 +330,22 @@ Now, the longest running queries are these:
|
||||
|
||||
| No.| calls | total_m | mean_ms | query |
|
||||
|---:|---------|--------:|--------:|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| 1 | 13.093 | 4 | 21 | insert into hs_hosting.asset( uuid, type, bookingitemuuid, parentassetuuid, assignedtoassetuuid, alarmcontactuuid, identifier, caption, config, version) values ( $1, $2, $3, $4, $5, $6, $7, $8, cast($9 as jsonb), $10) |
|
||||
| 2 | 517 | 4 | 502 | select hore1_0.uuid,hore1_0.anchoruuid,hore1_0.contactuuid,hore1_0.holderuuid,hore1_0.mark,hore1_0.type,hore1_0.version from public.hs_office.relation_rv hore1_0 where hore1_0.uuid=$1 |
|
||||
| 1 | 13.093 | 4 | 21 | insert into hs_hosting_asset( uuid, type, bookingitemuuid, parentassetuuid, assignedtoassetuuid, alarmcontactuuid, identifier, caption, config, version) values ( $1, $2, $3, $4, $5, $6, $7, $8, cast($9 as jsonb), $10) |
|
||||
| 2 | 517 | 4 | 502 | select hore1_0.uuid,hore1_0.anchoruuid,hore1_0.contactuuid,hore1_0.holderuuid,hore1_0.mark,hore1_0.type,hore1_0.version from public.hs_office_relation_rv hore1_0 where hore1_0.uuid=$1 |
|
||||
| 3 | 13.144 | 4 | 21 | call buildRbacSystemForHsHostingAsset(NEW) |
|
||||
| 4 | 96.632 | 3 | 2 | call rbac.grantRoleToRole(roleUuid, superRoleUuid, superRoleDesc.assumed) |
|
||||
| 5 | 120.815 | 3 | 2 | select * from rbac.isGranted(array[granteeId], grantedId) |
|
||||
| 6 | 123.740 | 3 | 2 | with recursive grants as ( select descendantUuid, ascendantUuid from RbacGrants where descendantUuid = grantedId union all select "grant".descendantUuid, "grant".ascendantUuid from RbacGrants "grant" inner join grants recur on recur.ascendantUuid = "grant".descendantUuid ) select exists ( select $3 from grants where ascendantUuid = any(granteeIds) ) or grantedId = any(granteeIds) |
|
||||
| 7 | 497 | 2 | 259 | select hoce1_0.uuid,hoce1_0.caption,hoce1_0.emailaddresses,hoce1_0.phonenumbers,hoce1_0.postaladdress,hoce1_0.version from public.hs_office.contact_rv hoce1_0 where hoce1_0.uuid=$1 |
|
||||
| 8 | 497 | 2 | 255 | select hope1_0.uuid,hope1_0.familyname,hope1_0.givenname,hope1_0.persontype,hope1_0.salutation,hope1_0.title,hope1_0.tradename,hope1_0.version from public.hs_office.person_rv hope1_0 where hope1_0.uuid=$1 |
|
||||
| 9 | 13.144 | 1 | 8 | SELECT createRoleWithGrants( hs_hosting.asset_TENANT(NEW), permissions => array[$7], incomingSuperRoles => array[ hs_hosting.asset_AGENT(NEW), hs_office.contact_ADMIN(newAlarmContact)], outgoingSubRoles => array[ hs_booking.item_TENANT(newBookingItem), hs_hosting.asset_TENANT(newParentAsset)] ) |
|
||||
| 10 | 13.144 | 1 | 5 | SELECT createRoleWithGrants( hs_hosting.asset_ADMIN(NEW), permissions => array[$7], incomingSuperRoles => array[ hs_booking.item_AGENT(newBookingItem), hs_hosting.asset_AGENT(newParentAsset), hs_hosting.asset_OWNER(NEW)] ) |
|
||||
| 7 | 497 | 2 | 259 | select hoce1_0.uuid,hoce1_0.caption,hoce1_0.emailaddresses,hoce1_0.phonenumbers,hoce1_0.postaladdress,hoce1_0.version from public.hs_office_contact_rv hoce1_0 where hoce1_0.uuid=$1 |
|
||||
| 8 | 497 | 2 | 255 | select hope1_0.uuid,hope1_0.familyname,hope1_0.givenname,hope1_0.persontype,hope1_0.salutation,hope1_0.title,hope1_0.tradename,hope1_0.version from public.hs_office_person_rv hope1_0 where hope1_0.uuid=$1 |
|
||||
| 9 | 13.144 | 1 | 8 | SELECT createRoleWithGrants( hsHostingAssetTENANT(NEW), permissions => array[$7], incomingSuperRoles => array[ hsHostingAssetAGENT(NEW), hsOfficeContactADMIN(newAlarmContact)], outgoingSubRoles => array[ hsBookingItemTENANT(newBookingItem), hsHostingAssetTENANT(newParentAsset)] ) |
|
||||
| 10 | 13.144 | 1 | 5 | SELECT createRoleWithGrants( hsHostingAssetADMIN(NEW), permissions => array[$7], incomingSuperRoles => array[ hsBookingItemAGENT(newBookingItem), hsHostingAssetAGENT(newParentAsset), hsHostingAssetOWNER(NEW)] ) |
|
||||
|
||||
That the `INSERT into hs_hosting.asset` (No. 1) takes up the most time, seems to be normal, and 21ms for each call is also fine.
|
||||
That the `INSERT into hs_hosting_asset` (No. 1) takes up the most time, seems to be normal, and 21ms for each call is also fine.
|
||||
|
||||
It seems that the trigger effects (eg. No. 3 and No. 4) are included in the measure for the causing INSERT, otherwise summing up the totals would exceed the actual total time of the whole import. And it was to be expected that building the RBAC rules for new business objects takes most of the time.
|
||||
|
||||
In production, the `SELECT ... FROM hs_office.relation_rv` (No. 2) with about 0.5 seconds could still be a problem. But once we apply the improvements from the hosting asset area also to the office area, this should not be a problem for the import anymore.
|
||||
In production, the `SELECT ... FROM hs_office_relation_rv` (No. 2) with about 0.5 seconds could still be a problem. But once we apply the improvements from the hosting asset area also to the office area, this should not be a problem for the import anymore.
|
||||
|
||||
|
||||
## Further Options To Explore
|
||||
@ -408,12 +408,12 @@ We found some solution approaches:
|
||||
This optimization idea came from Michael Hierweck and was promising.
|
||||
The idea is to reduce the size of the result of the recursive CTE query and maybe even speed up that query itself.
|
||||
|
||||
To evaluate this, I added a type column to the `rbacObject` table, initially as an enum hsHostingAssetType. Then I entered the type there for all rows from hs_hosting.asset. This means that 83,886 of 92,545 rows in `rbacobject` have a type set, leaving 8,659 without.
|
||||
To evaluate this, I added a type column to the `rbacObject` table, initially as an enum hsHostingAssetType. Then I entered the type there for all rows from hs_hosting_asset. This means that 83,886 of 92,545 rows in `rbacobject` have a type set, leaving 8,659 without.
|
||||
|
||||
If we do this for other types (we currently have 1,271 relations and 927 booking items), it gets more complicated because they are different enum types. As varchar(16), we could lose performance again due to the higher storage space requirements.
|
||||
|
||||
But the performance gained is not particularly high anyway.
|
||||
See the average seconds per recursive CTE select as role 'hs_hosting.asset:<DEBITOR>defaultproject:ADMIN',
|
||||
See the average seconds per recursive CTE select as role 'hs_hosting_asset:<DEBITOR>defaultproject:ADMIN',
|
||||
joined with business query for all `'EMAIL_ADDRESSES'`:
|
||||
|
||||
| | D-1000000-hsh | D-1000300-mih |
|
||||
|
@ -1,124 +0,0 @@
|
||||
<!doctype html>
|
||||
<html $if(lang)$ lang="$lang$" $endif$>
|
||||
<head>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<!--[if lt IE 9]>
|
||||
<script src="http://css3-mediaqueries-js.googlecode.com/svn/trunk/css3-mediaqueries.js"></script>
|
||||
<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="Content-Style-Type" content="text/css" />
|
||||
|
||||
<!-- <link rel="stylesheet" type="text/css" href="template.css" /> -->
|
||||
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/template.css" />
|
||||
|
||||
<link href="https://vjs.zencdn.net/5.4.4/video-js.css" rel="stylesheet" />
|
||||
|
||||
<script src="https://code.jquery.com/jquery-2.2.1.min.js"></script>
|
||||
<!-- <script type='text/javascript' src='menu/js/jquery.cookie.js'></script> -->
|
||||
<!-- <script type='text/javascript' src='menu/js/jquery.hoverIntent.minified.js'></script> -->
|
||||
<!-- <script type='text/javascript' src='menu/js/jquery.dcjqaccordion.2.7.min.js'></script> -->
|
||||
|
||||
<!-- <link href="menu/css/skins/blue.css" rel="stylesheet" type="text/css" /> -->
|
||||
<!-- <link href="menu/css/skins/graphite.css" rel="stylesheet" type="text/css" /> -->
|
||||
<!-- <link href="menu/css/skins/grey.css" rel="stylesheet" type="text/css" /> -->
|
||||
|
||||
<!-- <script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script> -->
|
||||
|
||||
|
||||
<!-- <script src="script.js"></script> -->
|
||||
|
||||
<!-- <script src="jquery.sticky-kit.js "></script> -->
|
||||
<script type='text/javascript' src='https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/js/jquery.cookie.js'></script>
|
||||
<script type='text/javascript' src='https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/js/jquery.hoverIntent.minified.js'></script>
|
||||
<script type='text/javascript' src='https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/js/jquery.dcjqaccordion.2.7.min.js'></script>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/css/skins/blue.css" rel="stylesheet" type="text/css" />
|
||||
<link href="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/css/skins/graphite.css" rel="stylesheet" type="text/css" />
|
||||
<link href="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/css/skins/grey.css" rel="stylesheet" type="text/css" />
|
||||
<link href="https://cdn.jsdelivr.net/gh/ryangrose/easy-pandoc-templates@948e28e5/css/elegant_bootstrap.css" rel="stylesheet" type="text/css" />
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.4/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/script.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/jquery.sticky-kit.js"></script>
|
||||
<meta name="generator" content="pandoc" />
|
||||
$for(author-meta)$
|
||||
<meta name="author" content="$author-meta$" />
|
||||
$endfor$
|
||||
$if(date-meta)$
|
||||
<meta name="date" content="$date-meta$" />
|
||||
$endif$
|
||||
<title>$if(title-prefix)$$title-prefix$ - $endif$$pagetitle$</title>
|
||||
<style type="text/css">code{white-space: pre;}</style>
|
||||
$if(quotes)$
|
||||
<style type="text/css">q { quotes: "“" "”" "‘" "’"; }</style>
|
||||
$endif$
|
||||
$if(highlighting-css)$
|
||||
<style type="text/css">
|
||||
$highlighting-css$
|
||||
</style>
|
||||
$endif$
|
||||
$for(css)$
|
||||
<link rel="stylesheet" href="$css$" $if(html5)$$else$type="text/css" $endif$/>
|
||||
$endfor$
|
||||
$if(math)$
|
||||
$math$
|
||||
$endif$
|
||||
$for(header-includes)$
|
||||
$header-includes$
|
||||
$endfor$
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
||||
$if(title)$
|
||||
<div class="navbar navbar-static-top">
|
||||
<div class="navbar-inner">
|
||||
<div class="container">
|
||||
<span class="doc-title">$title$</span>
|
||||
<ul class="nav pull-right doc-info">
|
||||
$for(author)$
|
||||
<li><p class="navbar-text">$author$</p></li>
|
||||
$endfor$
|
||||
$if(date)$
|
||||
<li><p class="navbar-text">$date$</p></li>
|
||||
$endif$
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
$endif$
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
$if(toc)$
|
||||
<div id="$idprefix$TOC" class="span3">
|
||||
<div class="well toc">
|
||||
|
||||
$toc$
|
||||
|
||||
</div>
|
||||
</div>
|
||||
$endif$
|
||||
<div class="span$if(toc)$9$else$12$endif$">
|
||||
|
||||
$if(abstract)$
|
||||
<H1>$abstract-title$</H1>
|
||||
$abstract$
|
||||
$endif$
|
||||
|
||||
$for(include-before)$
|
||||
$include-before$
|
||||
$endfor$
|
||||
$body$
|
||||
$for(include-after)$
|
||||
$include-after$
|
||||
$endfor$
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://vjs.zencdn.net/5.4.4/video.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1 +0,0 @@
|
||||
find the generated ScenarioReports in build/doc/scenarios
|
@ -90,20 +90,6 @@ Acceptance-tests, are blackbox-tests and do <u>not</u> count into test-code-cove
|
||||
TODO.test: Complete the Acceptance-Tests test concept.
|
||||
|
||||
|
||||
#### Scenario-Tests
|
||||
|
||||
Our Scenario-tests are induced by business use-cases.
|
||||
They test from the REST API all the way down to the database.
|
||||
|
||||
Most scenario-tests are positive tests, they test if business scenarios do work.
|
||||
But few might be negative tests, which test if specific forbidden data gets rejected.
|
||||
|
||||
Our scenario tests also generate test-reports which contain the REST-API calls needed for each scenario.
|
||||
These reports can be used as examples for the API usage from a business perspective.
|
||||
|
||||
There is an extra document regarding scenario-test, see [Scenario-Tests README](../src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/README.md).
|
||||
|
||||
|
||||
#### Performance-Tests
|
||||
|
||||
Performance-critical scenarios have to be identified and a special performance-test has to be implemented.
|
||||
|
@ -1,10 +1,8 @@
|
||||
{
|
||||
"allowedLicenses": [
|
||||
{ "moduleLicense": "Apache 2" },
|
||||
{ "moduleLicense": "Apache 2.0" },
|
||||
{ "moduleLicense": "Apache-2.0" },
|
||||
{ "moduleLicense": "Apache 2" },
|
||||
{ "moduleLicense": "Apache License 2.0" },
|
||||
{ "moduleLicense": "Apache License v2.0" },
|
||||
{ "moduleLicense": "Apache License, Version 2.0" },
|
||||
{ "moduleLicense": "The Apache Software License, Version 2.0" },
|
||||
|
||||
@ -13,8 +11,6 @@
|
||||
{ "moduleLicense": "BSD-3-Clause" },
|
||||
{ "moduleLicense": "The BSD License" },
|
||||
|
||||
{ "moduleLicense": "The New BSD License" },
|
||||
|
||||
{ "moduleLicense": "CDDL 1.1" },
|
||||
{ "moduleLicense": "CDDL/GPLv2+CE" },
|
||||
{ "moduleLicense": "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0" },
|
||||
@ -33,27 +29,11 @@
|
||||
{ "moduleLicense": "GNU General Public License, version 2 with the GNU Classpath Exception" },
|
||||
{ "moduleLicense": "GPL2 w/ CPE" },
|
||||
|
||||
{ "moduleLicense": "LGPL, version 2.1"},
|
||||
{ "moduleLicense": "LGPL-2.1-or-later"},
|
||||
|
||||
{ "moduleLicense": "MIT License" },
|
||||
{ "moduleLicense": "MIT" },
|
||||
{ "moduleLicense": "The MIT License (MIT)" },
|
||||
{ "moduleLicense": "The MIT License" },
|
||||
|
||||
{ "moduleLicense": "WTFPL" },
|
||||
|
||||
{
|
||||
"moduleLicense": "Public Domain, per Creative Commons CC0",
|
||||
"moduleVersion": "2.0.3"
|
||||
},
|
||||
|
||||
{
|
||||
"moduleLicense": null,
|
||||
"#moduleLicense": "Apache License 2.0, see https://github.com/springdoc/springdoc-openapi/blob/main/LICENSE",
|
||||
"moduleVersion": "2.4.0",
|
||||
"moduleName": "org.springdoc:springdoc-openapi"
|
||||
}
|
||||
|
||||
{ "moduleName": "org.springdoc:springdoc-openapi" }
|
||||
]
|
||||
}
|
||||
|
@ -1,6 +0,0 @@
|
||||
FROM eclipse-temurin:21-jdk
|
||||
RUN apt-get update && \
|
||||
apt-get install -y bind9-utils pandoc && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
@ -1,5 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd">
|
||||
<suppress>
|
||||
<notes><![CDATA[
|
||||
Cyclic references are not possible if file comes in JSON text format.
|
||||
]]></notes>
|
||||
<packageUrl regex="true">^pkg:maven/com\.fasterxml\.jackson\.core/jackson\-databind@.*$</packageUrl>
|
||||
<cpe>cpe:/a:fasterxml:jackson-databind</cpe>
|
||||
</suppress>
|
||||
<suppress>
|
||||
<notes><![CDATA[
|
||||
Internal tooling, not exposed to the Internet.
|
||||
@ -7,10 +14,4 @@
|
||||
<packageUrl regex="true">^pkg:maven/org\.pitest/pitest\-command\-line@.*$</packageUrl>
|
||||
<cpe>cpe:/a:line:line</cpe>
|
||||
</suppress>
|
||||
<suppress>
|
||||
<notes><![CDATA[
|
||||
Malicious HTTP redirect in JAXB on a REST-endpoint is not that dangerous.
|
||||
]]></notes>
|
||||
<cve>CVE-2024-9329</cve>
|
||||
</suppress>
|
||||
</suppressions>
|
||||
|
@ -6,21 +6,21 @@
|
||||
rollback;
|
||||
begin transaction;
|
||||
call defineContext('historization testing', null, 'superuser-alex@hostsharing.net',
|
||||
-- 'hs_booking.project#D-1000000-hshdefaultproject:ADMIN'); -- prod+test
|
||||
'hs_booking.project#D-1000313-D-1000313defaultproject:ADMIN'); -- prod+test
|
||||
-- 'hs_booking.project#D-1000300-mihdefaultproject:ADMIN'); -- prod
|
||||
-- 'hs_booking.project#D-1000300-mimdefaultproject:ADMIN'); -- test
|
||||
-- update hs_hosting.asset set caption='lug00 b' where identifier = 'lug00' and type = 'MANAGED_WEBSPACE'; -- prod
|
||||
-- update hs_hosting.asset set caption='hsh00 A ' || now()::text where identifier = 'hsh00' and type = 'MANAGED_WEBSPACE'; -- test
|
||||
-- update hs_hosting.asset set caption='hsh00 B ' || now()::text where identifier = 'hsh00' and type = 'MANAGED_WEBSPACE'; -- test
|
||||
-- 'hs_booking_project#D-1000000-hshdefaultproject:ADMIN'); -- prod+test
|
||||
'hs_booking_project#D-1000313-D-1000313defaultproject:ADMIN'); -- prod+test
|
||||
-- 'hs_booking_project#D-1000300-mihdefaultproject:ADMIN'); -- prod
|
||||
-- 'hs_booking_project#D-1000300-mimdefaultproject:ADMIN'); -- test
|
||||
-- update hs_hosting_asset set caption='lug00 b' where identifier = 'lug00' and type = 'MANAGED_WEBSPACE'; -- prod
|
||||
-- update hs_hosting_asset set caption='hsh00 A ' || now()::text where identifier = 'hsh00' and type = 'MANAGED_WEBSPACE'; -- test
|
||||
-- update hs_hosting_asset set caption='hsh00 B ' || now()::text where identifier = 'hsh00' and type = 'MANAGED_WEBSPACE'; -- test
|
||||
|
||||
-- insert into hs_hosting.asset
|
||||
-- insert into hs_hosting_asset
|
||||
-- (uuid, bookingitemuuid, type, parentassetuuid, assignedtoassetuuid, identifier, caption, config, alarmcontactuuid)
|
||||
-- values
|
||||
-- (uuid_generate_v4(), null, 'EMAIL_ADDRESS', 'bbda5895-0569-4e20-bb4c-34f3a38f3f63'::uuid, null,
|
||||
-- 'new@thi.example.org', 'some new E-Mail-Address', '{}'::jsonb, null);
|
||||
|
||||
delete from hs_hosting.asset where uuid='5aea68d2-3b55-464f-8362-b05c76c5a681'::uuid;
|
||||
delete from hs_hosting_asset where uuid='5aea68d2-3b55-464f-8362-b05c76c5a681'::uuid;
|
||||
commit;
|
||||
|
||||
-- single version at point in time
|
||||
@ -29,11 +29,11 @@ set hsadminng.tx_history_txid to '';
|
||||
set hsadminng.tx_history_timestamp to '2024-08-29 12:42';
|
||||
-- all versions
|
||||
select base.tx_history_txid(), txc.txtimestamp, txc.currentSubject, txc.currentTask, haex.*
|
||||
from hs_hosting.asset_ex haex
|
||||
from hs_hosting_asset_ex haex
|
||||
join base.tx_context txc on haex.txid=txc.txid
|
||||
where haex.identifier = 'test@thi.example.org';
|
||||
|
||||
select uuid, version, type, identifier, caption from hs_hosting.asset_hv p where identifier = 'test@thi.example.org';
|
||||
select uuid, version, type, identifier, caption from hs_hosting_asset_hv p where identifier = 'test@thi.example.org';
|
||||
|
||||
select pg_current_xact_id();
|
||||
|
||||
|
@ -3,14 +3,14 @@
|
||||
-- --------------------------------------------------------
|
||||
|
||||
|
||||
select rbac.isGranted(rbac.findRoleId('administrators'), rbac.findRoleId('rbactest.package#aaa00:OWNER'));
|
||||
select rbac.isGranted(rbac.findRoleId('rbactest.package#aaa00:OWNER'), rbac.findRoleId('administrators'));
|
||||
-- call rbac.grantRoleToRole(findRoleId('rbactest.package#aaa00:OWNER'), findRoleId('administrators'));
|
||||
-- call rbac.grantRoleToRole(findRoleId('administrators'), findRoleId('rbactest.package#aaa00:OWNER'));
|
||||
select rbac.isGranted(rbac.findRoleId('administrators'), rbac.findRoleId('test.package#aaa00:OWNER'));
|
||||
select rbac.isGranted(rbac.findRoleId('test.package#aaa00:OWNER'), rbac.findRoleId('administrators'));
|
||||
-- call rbac.grantRoleToRole(findRoleId('test.package#aaa00:OWNER'), findRoleId('administrators'));
|
||||
-- call rbac.grantRoleToRole(findRoleId('administrators'), findRoleId('test.package#aaa00:OWNER'));
|
||||
|
||||
select count(*)
|
||||
FROM rbac.queryAllPermissionsOfSubjectIdForObjectUuids(rbac.findRbacSubject('superuser-fran@hostsharing.net'),
|
||||
ARRAY(select uuid from rbactest.customer where reference < 1100000));
|
||||
ARRAY(select uuid from test.customer where reference < 1100000));
|
||||
select count(*)
|
||||
FROM rbac.queryAllPermissionsOfSubjectId(findRbacSubject('superuser-fran@hostsharing.net'));
|
||||
select *
|
||||
|
@ -20,7 +20,7 @@ CREATE POLICY customer_policy ON customer
|
||||
TO restricted
|
||||
USING (
|
||||
-- id=1000
|
||||
rbac.isPermissionGrantedToSubject(rbac.findEffectivePermissionId('rbactest.customer', id, 'SELECT'), rbac.currentSubjectUuid())
|
||||
rbac.isPermissionGrantedToSubject(rbac.findEffectivePermissionId('test.customer', id, 'SELECT'), rbac.currentSubjectUuid())
|
||||
);
|
||||
|
||||
SET SESSION AUTHORIZATION restricted;
|
||||
@ -31,28 +31,28 @@ SELECT * from customer;
|
||||
SET SESSION SESSION AUTHORIZATION DEFAULT;
|
||||
DROP VIEW cust_view;
|
||||
CREATE VIEW cust_view AS
|
||||
SELECT * FROM rbactest.customer;
|
||||
SELECT * FROM test.customer;
|
||||
CREATE OR REPLACE RULE "_RETURN" AS
|
||||
ON SELECT TO cust_view
|
||||
DO INSTEAD
|
||||
SELECT * FROM rbactest.customer WHERE rbac.isPermissionGrantedToSubject(rbac.findEffectivePermissionId('rbactest.customer', id, 'SELECT'), rbac.currentSubjectUuid());
|
||||
SELECT * FROM test.customer WHERE rbac.isPermissionGrantedToSubject(rbac.findEffectivePermissionId('test.customer', id, 'SELECT'), rbac.currentSubjectUuid());
|
||||
SELECT * from cust_view LIMIT 10;
|
||||
|
||||
select rbac.queryAllPermissionsOfSubjectId(findRbacSubject('superuser-alex@hostsharing.net'));
|
||||
|
||||
-- access control via view-rule with join to recursive permissions - really fast (38ms for 1 million rows)
|
||||
SET SESSION SESSION AUTHORIZATION DEFAULT;
|
||||
ALTER TABLE rbactest.customer ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE test.customer ENABLE ROW LEVEL SECURITY;
|
||||
DROP VIEW IF EXISTS cust_view;
|
||||
CREATE OR REPLACE VIEW cust_view AS
|
||||
SELECT *
|
||||
FROM rbactest.customer;
|
||||
FROM test.customer;
|
||||
CREATE OR REPLACE RULE "_RETURN" AS
|
||||
ON SELECT TO cust_view
|
||||
DO INSTEAD
|
||||
SELECT c.uuid, c.reference, c.prefix FROM rbactest.customer AS c
|
||||
SELECT c.uuid, c.reference, c.prefix FROM test.customer AS c
|
||||
JOIN rbac.queryAllPermissionsOfSubjectId(rbac.currentSubjectUuid()) AS p
|
||||
ON p.objectTable='rbactest.customer' AND p.objectUuid=c.uuid;
|
||||
ON p.objectTable='test.customer' AND p.objectUuid=c.uuid;
|
||||
GRANT ALL PRIVILEGES ON cust_view TO restricted;
|
||||
|
||||
SET SESSION SESSION AUTHORIZATION restricted;
|
||||
@ -81,9 +81,9 @@ select rr.uuid, rr.type from rbac.RbacGrants g
|
||||
join rbac.RbacReference RR on g.ascendantUuid = RR.uuid
|
||||
where g.descendantUuid in (
|
||||
select uuid from rbac.queryAllPermissionsOfSubjectId(findRbacSubject('alex@example.com'))
|
||||
where objectTable='rbactest.customer');
|
||||
where objectTable='test.customer');
|
||||
|
||||
call rbac.grantRoleToUser(rbac.findRoleId('rbactest.customer#aaa:ADMIN'), rbac.findRbacSubject('aaaaouq@example.com'));
|
||||
call rbac.grantRoleToUser(rbac.findRoleId('test.customer#aaa:ADMIN'), rbac.findRbacSubject('aaaaouq@example.com'));
|
||||
|
||||
select rbac.queryAllPermissionsOfSubjectId(findRbacSubject('aaaaouq@example.com'));
|
||||
|
||||
|
@ -6,10 +6,10 @@ select * from hs_statistics_v;
|
||||
|
||||
-- This is the extracted recursive CTE query to determine the visible object UUIDs of a single table
|
||||
-- (and optionally the hosting-asset-type) as a separate VIEW.
|
||||
-- In the generated code this is part of the hs_hosting.asset_rv VIEW.
|
||||
-- In the generated code this is part of the hs_hosting_asset_rv VIEW.
|
||||
|
||||
drop view if exists hs_hosting.asset_example_gv;
|
||||
create view hs_hosting.asset_example_gv as
|
||||
drop view if exists hs_hosting_asset_example_gv;
|
||||
create view hs_hosting_asset_example_gv as
|
||||
with recursive
|
||||
recursive_grants as (
|
||||
select distinct rbacgrants.descendantuuid,
|
||||
@ -40,7 +40,7 @@ select distinct perm.objectuuid
|
||||
join rbacpermission perm on recursive_grants.descendantuuid = perm.uuid
|
||||
join rbacobject obj on obj.uuid = perm.objectuuid
|
||||
join count_check cc on cc.valid
|
||||
where obj.objecttable::text = 'hs_hosting.asset'::text
|
||||
where obj.objecttable::text = 'hs_hosting_asset'::text
|
||||
-- with/without this type condition
|
||||
-- and obj.type = 'EMAIL_ADDRESS'::hshostingassettype
|
||||
and obj.type = 'EMAIL_ADDRESS'::hshostingassettype
|
||||
@ -53,10 +53,10 @@ select distinct perm.objectuuid
|
||||
rollback transaction;
|
||||
begin transaction;
|
||||
CALL defineContext('performance testing', null, 'superuser-alex@hostsharing.net',
|
||||
'hs_booking.project#D-1000000-hshdefaultproject:ADMIN');
|
||||
-- 'hs_booking.project#D-1000300-mihdefaultproject:ADMIN');
|
||||
'hs_booking_project#D-1000000-hshdefaultproject:ADMIN');
|
||||
-- 'hs_booking_project#D-1000300-mihdefaultproject:ADMIN');
|
||||
SET TRANSACTION READ ONLY;
|
||||
EXPLAIN ANALYZE select * from hs_hosting.asset_example_gv;
|
||||
EXPLAIN ANALYZE select * from hs_hosting_asset_example_gv;
|
||||
end transaction ;
|
||||
|
||||
-- ========================================================
|
||||
@ -64,15 +64,15 @@ end transaction ;
|
||||
-- An example for a restricted view (_rv) similar to the one generated by our RBAC system,
|
||||
-- but using the above separate VIEW to determine the visible objects.
|
||||
|
||||
drop view if exists hs_hosting.asset_example_rv;
|
||||
create view hs_hosting.asset_example_rv as
|
||||
with accessible_hs_hosting.asset_uuids as (
|
||||
select * from hs_hosting.asset_example_gv
|
||||
drop view if exists hs_hosting_asset_example_rv;
|
||||
create view hs_hosting_asset_example_rv as
|
||||
with accessible_hs_hosting_asset_uuids as (
|
||||
select * from hs_hosting_asset_example_gv
|
||||
)
|
||||
select target.*
|
||||
from hs_hosting.asset target
|
||||
where (target.uuid in (select accessible_hs_hosting.asset_uuids.objectuuid
|
||||
from accessible_hs_hosting.asset_uuids));
|
||||
from hs_hosting_asset target
|
||||
where (target.uuid in (select accessible_hs_hosting_asset_uuids.objectuuid
|
||||
from accessible_hs_hosting_asset_uuids));
|
||||
|
||||
-- -------------------------------------------------------------------------------
|
||||
|
||||
@ -89,8 +89,8 @@ BEGIN
|
||||
start_time := clock_timestamp();
|
||||
|
||||
CALL defineContext('performance testing', null, 'superuser-alex@hostsharing.net',
|
||||
'hs_booking.project#D-1000000-hshdefaultproject:ADMIN');
|
||||
-- 'hs_booking.project#D-1000300-mihdefaultproject:ADMIN');
|
||||
'hs_booking_project#D-1000000-hshdefaultproject:ADMIN');
|
||||
-- 'hs_booking_project#D-1000300-mihdefaultproject:ADMIN');
|
||||
SET TRANSACTION READ ONLY;
|
||||
|
||||
FOR i IN 0..25 LOOP
|
||||
@ -99,7 +99,7 @@ BEGIN
|
||||
|
||||
-- An example for a business query based on the view:
|
||||
select type, uuid, identifier, caption
|
||||
from hs_hosting.asset_example_rv
|
||||
from hs_hosting_asset_example_rv
|
||||
where type = 'EMAIL_ADDRESS'
|
||||
and identifier like letter || '%'
|
||||
-- end of the business query example.
|
||||
@ -115,7 +115,7 @@ BEGIN
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- average seconds per recursive CTE select as role 'hs_hosting.asset:<DEBITOR>defaultproject:ADMIN'
|
||||
-- average seconds per recursive CTE select as role 'hs_hosting_asset:<DEBITOR>defaultproject:ADMIN'
|
||||
-- joined with business query for all 'EMAIL_ADDRESSES':
|
||||
-- D-1000000-hsh D-1000300-mih
|
||||
-- - without type comparison in rbacobject: ~3.30 - ~3.49 ~0.23
|
||||
@ -128,15 +128,15 @@ $$;
|
||||
rollback transaction;
|
||||
begin transaction;
|
||||
CALL defineContext('performance testing', null, 'superuser-alex@hostsharing.net',
|
||||
'hs_booking.project#D-1000000-hshdefaultproject:ADMIN');
|
||||
-- 'hs_booking.project#D-1000300-mihdefaultproject:ADMIN');
|
||||
'hs_booking_project#D-1000000-hshdefaultproject:ADMIN');
|
||||
-- 'hs_booking_project#D-1000300-mihdefaultproject:ADMIN');
|
||||
SET TRANSACTION READ ONLY;
|
||||
|
||||
EXPLAIN SELECT * from (
|
||||
|
||||
-- An example for a business query based on the view:
|
||||
select type, uuid, identifier, caption
|
||||
from hs_hosting.asset_example_rv
|
||||
from hs_hosting_asset_example_rv
|
||||
where type = 'EMAIL_ADDRESS'
|
||||
-- and identifier like 'b%'
|
||||
-- end of the business query example.
|
||||
@ -151,17 +151,17 @@ end transaction;
|
||||
|
||||
alter table rbacobject
|
||||
-- just for performance testing, we would need a joined enum or a varchar(16) which would make it slow
|
||||
add column type hs_hosting.AssetType;
|
||||
add column type hshostingassettype;
|
||||
|
||||
-- and fill the type column with hs_hosting.asset types:
|
||||
-- and fill the type column with hs_hosting_asset types:
|
||||
|
||||
rollback transaction;
|
||||
begin transaction;
|
||||
call defineContext('setting rbacobject.type from hs_hosting.asset.type', null, 'superuser-alex@hostsharing.net');
|
||||
call defineContext('setting rbacobject.type from hs_hosting_asset.type', null, 'superuser-alex@hostsharing.net');
|
||||
|
||||
UPDATE rbacobject
|
||||
SET type = hs.type
|
||||
FROM hs_hosting.asset hs
|
||||
FROM hs_hosting_asset hs
|
||||
WHERE rbacobject.uuid = hs.uuid;
|
||||
|
||||
end transaction;
|
||||
|
@ -1,44 +0,0 @@
|
||||
package net.hostsharing.hsadminng.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
@Endpoint(id="metric-links")
|
||||
// BLOG: implement a custom Spring Actuator endpoint to view _clickable_ Spring Actuator (Micrometer) Metrics endpoints
|
||||
// HOWTO: implement a custom Spring Actuator endpoint
|
||||
public class CustomActuatorEndpoint {
|
||||
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
@ReadOperation
|
||||
public String getMetricsLinks() {
|
||||
final String baseUrl = ServletUriComponentsBuilder.fromCurrentContextPath().build().toUriString();
|
||||
final var metricsEndpoint = baseUrl + "/actuator/metrics";
|
||||
|
||||
final var response = restTemplate.getForObject(metricsEndpoint, ActuatorMetricsEndpointResource.class);
|
||||
|
||||
if (response == null || response.getNames() == null) {
|
||||
throw new IllegalStateException("no metrics available");
|
||||
}
|
||||
return generateJsonLinksToMetricEndpoints(response, metricsEndpoint);
|
||||
}
|
||||
|
||||
private static String generateJsonLinksToMetricEndpoints(final ActuatorMetricsEndpointResource response, final String metricsEndpoint) {
|
||||
final var links = response.getNames().stream()
|
||||
.map(name -> "\"" + name + "\": \"" + metricsEndpoint + "/" + name + "\"")
|
||||
.toList();
|
||||
return "{\n" + String.join(",\n", links) + "\n}";
|
||||
}
|
||||
|
||||
@Getter
|
||||
private static class ActuatorMetricsEndpointResource {
|
||||
private List<String> names;
|
||||
}
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
package net.hostsharing.hsadminng.config;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.openapitools.jackson.nullable.JsonNullableModule;
|
||||
@ -11,24 +9,15 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||
|
||||
|
||||
@Configuration
|
||||
public class JsonObjectMapperConfiguration {
|
||||
|
||||
public static ObjectMapper build() {
|
||||
return new JsonObjectMapperConfiguration().customObjectMapper().build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public Jackson2ObjectMapperBuilder customObjectMapper() {
|
||||
// HOWTO: add JSON converters and specify other JSON mapping configurations
|
||||
return new Jackson2ObjectMapperBuilder()
|
||||
.modules(new JsonNullableModule(), new JavaTimeModule())
|
||||
.featuresToEnable(
|
||||
JsonParser.Feature.ALLOW_COMMENTS,
|
||||
DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS
|
||||
)
|
||||
.featuresToEnable(JsonParser.Feature.ALLOW_COMMENTS)
|
||||
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
}
|
||||
}
|
||||
|
@ -1,27 +0,0 @@
|
||||
package net.hostsharing.hsadminng.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class WebSecurityConfig {
|
||||
|
||||
@Bean
|
||||
@Profile("!test")
|
||||
public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception {
|
||||
return http
|
||||
.authorizeHttpRequests(authorize -> authorize
|
||||
.requestMatchers("/api/**").permitAll() // TODO.impl: implement authentication
|
||||
.requestMatchers("/swagger-ui/**").permitAll()
|
||||
.requestMatchers("/v3/api-docs/**").permitAll()
|
||||
.requestMatchers("/actuator/**").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.build();
|
||||
}
|
||||
}
|
@ -46,7 +46,6 @@ public class CustomErrorResponse {
|
||||
this.path = path;
|
||||
this.statusCode = status.value();
|
||||
this.statusPhrase = status.getReasonPhrase();
|
||||
// HOWTO: debug serverside error response - set a breakpoint here
|
||||
this.message = message.startsWith("ERROR: [") ? message : "ERROR: [" + statusCode + "] " + message;
|
||||
}
|
||||
}
|
||||
|
@ -9,25 +9,15 @@ import java.lang.annotation.Target;
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface DisplayAs {
|
||||
|
||||
class DisplayName {
|
||||
|
||||
public static String of(final Class<?> clazz) {
|
||||
final var displayNameAnnot = getDisplayNameAnnotation(clazz);
|
||||
final var displayNameAnnot = clazz.getAnnotation(DisplayAs.class);
|
||||
return displayNameAnnot != null ? displayNameAnnot.value() : clazz.getSimpleName();
|
||||
}
|
||||
|
||||
public static String of(@NotNull final Object instance) {
|
||||
return of(instance.getClass());
|
||||
}
|
||||
|
||||
private static DisplayAs getDisplayNameAnnotation(final Class<?> clazz) {
|
||||
if (clazz == null) {
|
||||
return null;
|
||||
}
|
||||
final var annot = clazz.getAnnotation(DisplayAs.class);
|
||||
return annot != null ? annot : getDisplayNameAnnotation(clazz.getSuperclass());
|
||||
}
|
||||
}
|
||||
|
||||
String value() default "";
|
||||
|
@ -1,31 +0,0 @@
|
||||
package net.hostsharing.hsadminng.errors;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import jakarta.validation.ValidationException;
|
||||
|
||||
@AllArgsConstructor
|
||||
public class Validate {
|
||||
|
||||
final String variableNames;
|
||||
|
||||
public static Validate validate(final String variableNames) {
|
||||
return new Validate(variableNames);
|
||||
}
|
||||
|
||||
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 + ")");
|
||||
}
|
||||
}
|
||||
}
|
@ -27,7 +27,7 @@ public final class HashGenerator {
|
||||
"abcdefghijklmnopqrstuvwxyz" +
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
|
||||
"0123456789/.";
|
||||
private static boolean couldBeHashEnabled; // TODO.legacy: remove after legacy data is migrated
|
||||
private static boolean couldBeHashEnabled; // TODO.impl: remove after legacy data is migrated
|
||||
|
||||
public enum Algorithm {
|
||||
LINUX_SHA512(LinuxEtcShadowHashGenerator::hash, "6"),
|
||||
|
@ -5,8 +5,8 @@ import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.repr.Stringify;
|
||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
@ -14,11 +14,11 @@ import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
// a partial HsOfficeDebitorEntity to reduce the number of SQL queries to load the entity
|
||||
@Entity
|
||||
@Table(schema = "hs_booking", name = "debitor_xv")
|
||||
@Table(name = "hs_booking_debitor_xv")
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.debitor;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
import java.util.List;
|
||||
@ -9,9 +8,7 @@ import java.util.UUID;
|
||||
|
||||
public interface HsBookingDebitorRepository extends Repository<HsBookingDebitorEntity, UUID> {
|
||||
|
||||
@Timed("app.booking.debitor.repo.findByUuid")
|
||||
Optional<HsBookingDebitorEntity> findByUuid(UUID id);
|
||||
|
||||
@Timed("app.booking.debitor.repo.findByDebitorNumber")
|
||||
List<HsBookingDebitorEntity> findByDebitorNumber(int debitorNumber);
|
||||
}
|
||||
|
@ -1,20 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.item;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
@Getter
|
||||
public class BookingItemCreatedAppEvent extends ApplicationEvent {
|
||||
|
||||
private BookingItemCreatedEventEntity entity;
|
||||
|
||||
public BookingItemCreatedAppEvent(
|
||||
@NotNull final Object source,
|
||||
@NotNull final HsBookingItemRealEntity newBookingItem,
|
||||
final String assetJson) {
|
||||
super(source);
|
||||
this.entity = new BookingItemCreatedEventEntity(newBookingItem, assetJson);
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.item;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.MapsId;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Version;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.UUID;
|
||||
|
||||
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_booking", name = "item_created_event")
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
public class BookingItemCreatedEventEntity implements BaseEntity {
|
||||
@Id
|
||||
@Column(name="bookingitemuuid")
|
||||
private UUID uuid;
|
||||
|
||||
@MapsId
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "bookingitemuuid", nullable = false)
|
||||
private HsBookingItemRealEntity bookingItem;
|
||||
|
||||
@Version
|
||||
private int version;
|
||||
|
||||
@Column(name = "assetjson")
|
||||
private String assetJson;
|
||||
|
||||
@Setter
|
||||
@Column(name = "statusmessage")
|
||||
private String statusMessage;
|
||||
|
||||
public BookingItemCreatedEventEntity(
|
||||
@NotNull final HsBookingItemRealEntity newBookingItem,
|
||||
final String assetJson) {
|
||||
this.bookingItem = newBookingItem;
|
||||
this.assetJson = assetJson;
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.item;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public interface BookingItemCreatedEventRepository extends Repository<BookingItemCreatedEventEntity, UUID> {
|
||||
|
||||
@Timed("app.booking.items.repo.save")
|
||||
BookingItemCreatedEventEntity save(HsBookingItemRealEntity current);
|
||||
|
||||
@Timed("app.booking.items.repo.findByBookingItem")
|
||||
BookingItemCreatedEventEntity findByBookingItem(HsBookingItemRealEntity newBookingItem);
|
||||
}
|
@ -14,9 +14,9 @@ import net.hostsharing.hsadminng.hs.booking.project.HsBookingProject;
|
||||
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
|
||||
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.repr.Stringify;
|
||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.hibernate.annotations.Type;
|
||||
|
||||
import jakarta.persistence.CascadeType;
|
||||
@ -45,7 +45,7 @@ import static java.util.Optional.ofNullable;
|
||||
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.lowerInclusiveFromPostgresDateRange;
|
||||
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
|
||||
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.upperInclusiveFromPostgresDateRange;
|
||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@MappedSuperclass
|
||||
@Getter
|
||||
|
@ -1,32 +1,26 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.item;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingItemsApi;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.validators.BookingItemEntitySaveProcessor;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidatorRegistry;
|
||||
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity;
|
||||
import net.hostsharing.hsadminng.mapper.KeyValueMap;
|
||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
|
||||
|
||||
@RestController
|
||||
@ -36,24 +30,17 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private StrictMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private ApplicationEventPublisher applicationEventPublisher;
|
||||
private Mapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsBookingItemRbacRepository bookingItemRepo;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper jsonMapper;
|
||||
|
||||
@Autowired
|
||||
private EntityManagerWrapper em;
|
||||
@PersistenceContext
|
||||
private EntityManager em;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.bookingItems.api.getListOfBookingItemsByProjectUuid")
|
||||
public ResponseEntity<List<HsBookingItemResource>> getListOfBookingItemsByProjectUuid(
|
||||
public ResponseEntity<List<HsBookingItemResource>> listBookingItemsByProjectUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID projectUuid) {
|
||||
@ -61,14 +48,13 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
||||
|
||||
final var entities = bookingItemRepo.findAllByProjectUuid(projectUuid);
|
||||
|
||||
final var resources = mapper.mapList(entities, HsBookingItemResource.class, RBAC_ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
final var resources = mapper.mapList(entities, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
return ResponseEntity.ok(resources);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.bookingItems.api.postNewBookingItem")
|
||||
public ResponseEntity<HsBookingItemResource> postNewBookingItem(
|
||||
public ResponseEntity<HsBookingItemResource> addBookingItem(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final HsBookingItemInsertResource body) {
|
||||
@ -76,29 +62,21 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
||||
context.define(currentSubject, assumedRoles);
|
||||
|
||||
final var entityToSave = mapper.map(body, HsBookingItemRbacEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
|
||||
final var saveProcessor = new BookingItemEntitySaveProcessor(em, entityToSave);
|
||||
final var mapped = saveProcessor
|
||||
.preprocessEntity()
|
||||
.validateEntity()
|
||||
.prepareForSave()
|
||||
.save()
|
||||
.validateContext()
|
||||
.mapUsing(e -> mapper.map(e, HsBookingItemResource.class, ITEM_TO_RESOURCE_POSTMAPPER))
|
||||
.revampProperties();
|
||||
publishSavedEvent(saveProcessor, body);
|
||||
|
||||
final var saved = HsBookingItemEntityValidatorRegistry.validated(em, bookingItemRepo.save(entityToSave));
|
||||
|
||||
final var uri =
|
||||
MvcUriComponentsBuilder.fromController(getClass())
|
||||
.path("/api/hs/booking/items/{id}")
|
||||
.buildAndExpand(mapped.getUuid())
|
||||
.buildAndExpand(saved.getUuid())
|
||||
.toUri();
|
||||
final var mapped = mapper.map(saved, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
return ResponseEntity.created(uri).body(mapped);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.bookingItems.api.getSingleBookingItemByUuid")
|
||||
public ResponseEntity<HsBookingItemResource> getSingleBookingItemByUuid(
|
||||
public ResponseEntity<HsBookingItemResource> getBookingItemByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID bookingItemUuid) {
|
||||
@ -109,13 +87,12 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
||||
result.ifPresent(entity -> em.detach(entity)); // prevent further LAZY-loading
|
||||
return result
|
||||
.map(bookingItemEntity -> ResponseEntity.ok(
|
||||
mapper.map(bookingItemEntity, HsBookingItemResource.class, RBAC_ENTITY_TO_RESOURCE_POSTMAPPER)))
|
||||
mapper.map(bookingItemEntity, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER)))
|
||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.bookingItems.api.deleteBookingIemByUuid")
|
||||
public ResponseEntity<Void> deleteBookingIemByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
@ -130,7 +107,6 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.bookingItems.api.patchBookingItem")
|
||||
public ResponseEntity<HsBookingItemResource> patchBookingItem(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
@ -144,34 +120,18 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
||||
new HsBookingItemEntityPatcher(current).apply(body);
|
||||
|
||||
final var saved = bookingItemRepo.save(HsBookingItemEntityValidatorRegistry.validated(em, current));
|
||||
final var mapped = mapper.map(saved, HsBookingItemResource.class, RBAC_ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
final var mapped = mapper.map(saved, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
return ResponseEntity.ok(mapped);
|
||||
}
|
||||
|
||||
private void publishSavedEvent(final BookingItemEntitySaveProcessor saveProcessor, final HsBookingItemInsertResource body) {
|
||||
try {
|
||||
final var bookingItemRealEntity = em.getReference(HsBookingItemRealEntity.class, saveProcessor.getEntity().getUuid());
|
||||
applicationEventPublisher.publishEvent(new BookingItemCreatedAppEvent(
|
||||
this, bookingItemRealEntity, jsonMapper.writeValueAsString(body.getHostingAsset())));
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
final BiConsumer<HsBookingItem, HsBookingItemResource> ITEM_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
||||
final BiConsumer<HsBookingItemRbacEntity, HsBookingItemResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
||||
resource.setValidFrom(entity.getValidity().lower());
|
||||
if (entity.getValidity().hasUpperBound()) {
|
||||
resource.setValidTo(entity.getValidity().upper().minusDays(1));
|
||||
}
|
||||
};
|
||||
|
||||
final BiConsumer<HsBookingItemRbacEntity, HsBookingItemResource> RBAC_ENTITY_TO_RESOURCE_POSTMAPPER = ITEM_TO_RESOURCE_POSTMAPPER::accept;
|
||||
|
||||
final BiConsumer<HsBookingItemInsertResource, HsBookingItemRbacEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
|
||||
entity.setProject(em.find(HsBookingProjectRealEntity.class, resource.getProjectUuid()));
|
||||
ofNullable(resource.getParentItemUuid())
|
||||
.map(parentItemUuid -> em.find(HsBookingItemRealEntity.class, parentItemUuid))
|
||||
.ifPresent(entity::setParentItem);
|
||||
entity.setValidity(toPostgresDateRange(LocalDate.now(), resource.getValidTo()));
|
||||
entity.putResources(KeyValueMap.from(resource.getResources()));
|
||||
};
|
||||
|
@ -4,7 +4,7 @@ import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRbacEntity;
|
||||
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProject;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
|
||||
@ -31,7 +31,7 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetc
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_booking", name = "item_rv")
|
||||
@Table(name = "hs_booking_item_rv")
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@Setter
|
||||
@ -49,7 +49,7 @@ public class HsBookingItemRbacEntity extends HsBookingItem {
|
||||
.toRole(GLOBAL, ADMIN).grantPermission(INSERT) // TODO.impl: Why is this necessary to insert test data?
|
||||
.toRole(GLOBAL, ADMIN).grantPermission(DELETE)
|
||||
|
||||
.importEntityAlias("project", HsBookingProjectRbacEntity.class, usingDefaultCase(),
|
||||
.importEntityAlias("project", HsBookingProject.class, usingDefaultCase(),
|
||||
dependsOnColumn("projectUuid"),
|
||||
directlyFetchedByDependsOnColumn(),
|
||||
NULLABLE)
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.item;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
import java.util.List;
|
||||
@ -10,21 +9,15 @@ import java.util.UUID;
|
||||
public interface HsBookingItemRbacRepository extends HsBookingItemRepository<HsBookingItemRbacEntity>,
|
||||
Repository<HsBookingItemRbacEntity, UUID> {
|
||||
|
||||
@Timed("app.bookingItems.repo.findByUuid.rbac")
|
||||
Optional<HsBookingItemRbacEntity> findByUuid(final UUID bookingItemUuid);
|
||||
|
||||
@Timed("app.bookingItems.repo.findByCaption.rbac")
|
||||
List<HsBookingItemRbacEntity> findByCaption(String bookingItemCaption);
|
||||
|
||||
@Timed("app.bookingItems.repo.findAllByProjectUuid.rbac")
|
||||
List<HsBookingItemRbacEntity> findAllByProjectUuid(final UUID projectItemUuid);
|
||||
|
||||
@Timed("app.bookingItems.repo.save.rbac")
|
||||
HsBookingItemRbacEntity save(HsBookingItemRbacEntity current);
|
||||
|
||||
@Timed("app.bookingItems.repo.deleteByUuid.rbac")
|
||||
int deleteByUuid(final UUID uuid);
|
||||
|
||||
@Timed("app.bookingItems.repo.count.rbac")
|
||||
long count();
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import jakarta.persistence.Table;
|
||||
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_booking", name = "item")
|
||||
@Table(name = "hs_booking_item")
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@Setter
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.item;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
import java.util.List;
|
||||
@ -10,21 +9,15 @@ import java.util.UUID;
|
||||
public interface HsBookingItemRealRepository extends HsBookingItemRepository<HsBookingItemRealEntity>,
|
||||
Repository<HsBookingItemRealEntity, UUID> {
|
||||
|
||||
@Timed("app.bookingItems.repo.findByUuid.real")
|
||||
Optional<HsBookingItemRealEntity> findByUuid(final UUID bookingItemUuid);
|
||||
|
||||
@Timed("app.bookingItems.repo.findByCaption.real")
|
||||
List<HsBookingItemRealEntity> findByCaption(String bookingItemCaption);
|
||||
|
||||
@Timed("app.bookingItems.repo.findAllByProjectUuid.real")
|
||||
List<HsBookingItemRealEntity> findAllByProjectUuid(final UUID projectItemUuid);
|
||||
|
||||
@Timed("app.bookingItems.repo.save.real")
|
||||
HsBookingItemRealEntity save(HsBookingItemRealEntity current);
|
||||
|
||||
@Timed("app.bookingItems.repo.deleteByUuid.real")
|
||||
int deleteByUuid(final UUID uuid);
|
||||
|
||||
@Timed("app.bookingItems.repo.count.real")
|
||||
long count();
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.item;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
@ -1,136 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.item.validators;
|
||||
|
||||
import lombok.Getter;
|
||||
import net.hostsharing.hsadminng.errors.MultiValidationException;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem;
|
||||
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
// TODO.refa: introduce common base class with HsHostingAssetEntitySaveProcessor
|
||||
/**
|
||||
* Wraps the steps of the pararation, validation, mapping and revamp around saving of a HsBookingItem into a readable API.
|
||||
*/
|
||||
public class BookingItemEntitySaveProcessor {
|
||||
|
||||
private final HsEntityValidator<HsBookingItem> validator;
|
||||
private String expectedStep = "preprocessEntity";
|
||||
private final EntityManager em;
|
||||
|
||||
@Getter
|
||||
private HsBookingItem entity;
|
||||
|
||||
@Getter
|
||||
private HsBookingItemResource resource;
|
||||
|
||||
public BookingItemEntitySaveProcessor(final EntityManager em, final HsBookingItem entity) {
|
||||
this.em = em;
|
||||
this.entity = entity;
|
||||
this.validator = HsBookingItemEntityValidatorRegistry.forType(entity.getType());
|
||||
}
|
||||
|
||||
/// initial step allowing to set default values before any validations
|
||||
public BookingItemEntitySaveProcessor preprocessEntity() {
|
||||
step("preprocessEntity", "validateEntity");
|
||||
validator.preprocessEntity(entity);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// validates the entity itself including its properties
|
||||
public BookingItemEntitySaveProcessor validateEntity() {
|
||||
step("validateEntity", "prepareForSave");
|
||||
MultiValidationException.throwIfNotEmpty(validator.validateEntity(entity));
|
||||
return this;
|
||||
}
|
||||
|
||||
// TODO.legacy: remove once the migration of legacy data is done
|
||||
/// validates the entity itself including its properties, but ignoring some error messages for import of legacy data
|
||||
public BookingItemEntitySaveProcessor validateEntityIgnoring(final String... ignoreRegExp) {
|
||||
step("validateEntity", "prepareForSave");
|
||||
final var ignoreRegExpPatterns = Arrays.stream(ignoreRegExp).map(Pattern::compile).toList();
|
||||
MultiValidationException.throwIfNotEmpty(
|
||||
validator.validateEntity(entity).stream()
|
||||
.filter(error -> ignoreRegExpPatterns.stream().noneMatch(p -> p.matcher(error).matches() ))
|
||||
.toList()
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// hashing passwords etc.
|
||||
public BookingItemEntitySaveProcessor prepareForSave() {
|
||||
step("prepareForSave", "save");
|
||||
validator.prepareProperties(em, entity);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the entity using the given `saveFunction`.
|
||||
*
|
||||
* <p>`validator.postPersist(em, entity)` is NOT called.
|
||||
* If any postprocessing is necessary, the saveFunction has to implement this.</p>
|
||||
* @param saveFunction
|
||||
* @return this
|
||||
*/
|
||||
public BookingItemEntitySaveProcessor saveUsing(final Function<HsBookingItem, HsBookingItem> saveFunction) {
|
||||
step("save", "validateContext");
|
||||
entity = saveFunction.apply(entity);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the using the `EntityManager`, but does NOT ever merge the entity.
|
||||
*
|
||||
* <p>`validator.postPersist(em, entity)` is called afterwards with the entity guaranteed to be flushed to the database.</p>
|
||||
* @return this
|
||||
*/
|
||||
public BookingItemEntitySaveProcessor save() {
|
||||
return saveUsing(e -> {
|
||||
if (!em.contains(entity)) {
|
||||
em.persist(entity);
|
||||
}
|
||||
em.flush(); // makes RbacEntity available as RealEntity if needed
|
||||
validator.postPersist(em, entity);
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
/// validates the entity within it's parent and child hierarchy (e.g. totals validators and other limits)
|
||||
public BookingItemEntitySaveProcessor validateContext() {
|
||||
step("validateContext", "mapUsing");
|
||||
return HsEntityValidator.doWithEntityManager(em, () -> {
|
||||
MultiValidationException.throwIfNotEmpty(validator.validateContext(entity));
|
||||
return this;
|
||||
});
|
||||
}
|
||||
|
||||
/// maps entity to JSON resource representation
|
||||
public BookingItemEntitySaveProcessor mapUsing(
|
||||
final Function<HsBookingItem, HsBookingItemResource> mapFunction) {
|
||||
step("mapUsing", "revampProperties");
|
||||
resource = mapFunction.apply(entity);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// removes write-only-properties and ads computed-properties
|
||||
@SuppressWarnings("unchecked")
|
||||
public HsBookingItemResource revampProperties() {
|
||||
step("revampProperties", null);
|
||||
final var revampedProps = validator.revampProperties(em, entity, (Map<String, Object>) resource.getResources());
|
||||
resource.setResources(revampedProps);
|
||||
return resource;
|
||||
}
|
||||
|
||||
// Makes sure that the steps are called in the correct order.
|
||||
// Could also be implemented using an interface per method, but that seems exaggerated.
|
||||
private void step(final String current, final String next) {
|
||||
if (!expectedStep.equals(current)) {
|
||||
throw new IllegalStateException("expected " + expectedStep + " but got " + current);
|
||||
}
|
||||
expectedStep = next;
|
||||
}
|
||||
}
|
@ -48,11 +48,10 @@ public class HsBookingItemEntityValidatorRegistry {
|
||||
}
|
||||
|
||||
public static List<String> doValidate(final EntityManager em, final HsBookingItem bookingItem) {
|
||||
final var bookingItemValidator = HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType());
|
||||
return HsEntityValidator.doWithEntityManager(em, () ->
|
||||
HsEntityValidator.sequentiallyValidate(
|
||||
() -> bookingItemValidator.validateEntity(bookingItem),
|
||||
() -> bookingItemValidator.validateContext(bookingItem))
|
||||
() -> HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateEntity(bookingItem),
|
||||
() -> HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateContext(bookingItem))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
package net.hostsharing.hsadminng.hs.booking.item.validators;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem;
|
||||
@ -13,16 +12,13 @@ import static net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns.REGISTRA
|
||||
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
|
||||
|
||||
class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator {
|
||||
public static final String DOMAIN_NAME_PROPERTY_NAME = "domainName";
|
||||
|
||||
public static final String FQDN_REGEX = "^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,12}";
|
||||
public static final String WEBSPACE_NAME_REGEX = "[a-z][a-z0-9]{2}[0-9]{2}";
|
||||
public static final String TARGET_UNIX_USER_NAME_REGEX = "^"+WEBSPACE_NAME_REGEX+"$|^"+WEBSPACE_NAME_REGEX+"-[a-z0-9\\._-]+$";
|
||||
public static final String DOMAIN_NAME_PROPERTY_NAME = "domainName";
|
||||
public static final String VERIFICATION_CODE_PROPERTY_NAME = "verificationCode";
|
||||
|
||||
HsDomainSetupBookingItemValidator() {
|
||||
super(
|
||||
// TODO.spec: feels wrong
|
||||
stringProperty(DOMAIN_NAME_PROPERTY_NAME).writeOnce()
|
||||
.maxLength(253)
|
||||
.matchesRegEx(FQDN_REGEX).describedAs("is not a (non-top-level) fully qualified domain name")
|
||||
@ -49,11 +45,6 @@ class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator {
|
||||
}
|
||||
|
||||
private static String generateVerificationCode(final EntityManager em, final PropertiesProvider propertiesProvider) {
|
||||
final var userDefinedVerificationCode = propertiesProvider.getDirectValue(VERIFICATION_CODE_PROPERTY_NAME, String.class);
|
||||
if (userDefinedVerificationCode != null) {
|
||||
return userDefinedVerificationCode;
|
||||
}
|
||||
|
||||
final var alphaNumeric = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||||
final var secureRandom = new SecureRandom();
|
||||
final var sb = new StringBuilder();
|
||||
|
@ -3,15 +3,31 @@ package net.hostsharing.hsadminng.hs.booking.project;
|
||||
import lombok.*;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.repr.Stringify;
|
||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Column.dependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingCase;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NOT_NULL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.fetchedBySql;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@MappedSuperclass
|
||||
@Getter
|
||||
@ -40,6 +56,8 @@ public abstract class HsBookingProject implements Stringifyable, BaseEntity<HsBo
|
||||
@Column(name = "caption")
|
||||
private String caption;
|
||||
|
||||
// TODO:impl: alert-Contacts
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return stringify.apply(this);
|
||||
@ -50,4 +68,50 @@ public abstract class HsBookingProject implements Stringifyable, BaseEntity<HsBo
|
||||
return ofNullable(debitor).map(HsBookingDebitorEntity::toShortString).orElse("D-???????") +
|
||||
":" + caption;
|
||||
}
|
||||
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("project", HsBookingProject.class)
|
||||
.withIdentityView(SQL.query("""
|
||||
SELECT bookingProject.uuid as uuid, debitorIV.idName || '-' || base.cleanIdentifier(bookingProject.caption) as idName
|
||||
FROM hs_booking_project bookingProject
|
||||
JOIN hs_office_debitor_iv debitorIV ON debitorIV.uuid = bookingProject.debitorUuid
|
||||
"""))
|
||||
.withRestrictedViewOrderBy(SQL.expression("caption"))
|
||||
.withUpdatableColumns("version", "caption")
|
||||
|
||||
.importEntityAlias("debitor", HsOfficeDebitorEntity.class, usingDefaultCase(),
|
||||
dependsOnColumn("debitorUuid"),
|
||||
directlyFetchedByDependsOnColumn(),
|
||||
NOT_NULL)
|
||||
|
||||
.importEntityAlias("debitorRel", HsOfficeRelationRbacEntity.class, usingCase(DEBITOR),
|
||||
dependsOnColumn("debitorUuid"),
|
||||
fetchedBySql("""
|
||||
SELECT ${columns}
|
||||
FROM hs_office_relation debitorRel
|
||||
JOIN hs_office_debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid
|
||||
WHERE debitor.uuid = ${REF}.debitorUuid
|
||||
"""),
|
||||
NOT_NULL)
|
||||
.toRole("debitorRel", ADMIN).grantPermission(INSERT)
|
||||
.toRole(GLOBAL, ADMIN).grantPermission(DELETE)
|
||||
|
||||
.createRole(OWNER, (with) -> {
|
||||
with.incomingSuperRole("debitorRel", AGENT).unassumed();
|
||||
})
|
||||
.createSubRole(ADMIN, (with) -> {
|
||||
with.permission(UPDATE);
|
||||
})
|
||||
.createSubRole(AGENT)
|
||||
.createSubRole(TENANT, (with) -> {
|
||||
with.outgoingSubRole("debitorRel", TENANT);
|
||||
with.permission(SELECT);
|
||||
})
|
||||
|
||||
.limitDiagramTo("project", "debitorRel", "rbac.global");
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
rbac().generateWithBaseFileName("6-hs-booking/620-booking-project/6203-hs-booking-project-rbac");
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,12 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.project;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorRepository;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingProjectsApi;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectPatchResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectResource;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@ -26,7 +25,7 @@ public class HsBookingProjectController implements HsBookingProjectsApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private StandardMapper mapper;
|
||||
private Mapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsBookingProjectRbacRepository bookingProjectRepo;
|
||||
@ -36,8 +35,7 @@ public class HsBookingProjectController implements HsBookingProjectsApi {
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.bookingProjects.api.getListOfBookingProjectsByDebitorUuid")
|
||||
public ResponseEntity<List<HsBookingProjectResource>> getListOfBookingProjectsByDebitorUuid(
|
||||
public ResponseEntity<List<HsBookingProjectResource>> listBookingProjectsByDebitorUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID debitorUuid) {
|
||||
@ -51,8 +49,7 @@ public class HsBookingProjectController implements HsBookingProjectsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.bookingProjects.api.postNewBookingProject")
|
||||
public ResponseEntity<HsBookingProjectResource> postNewBookingProject(
|
||||
public ResponseEntity<HsBookingProjectResource> addBookingProject(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final HsBookingProjectInsertResource body) {
|
||||
@ -74,7 +71,6 @@ public class HsBookingProjectController implements HsBookingProjectsApi {
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.bookingProjects.api.getBookingProjectByUuid")
|
||||
public ResponseEntity<HsBookingProjectResource> getBookingProjectByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
@ -91,7 +87,6 @@ public class HsBookingProjectController implements HsBookingProjectsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.bookingProjects.api.deleteBookingIemByUuid")
|
||||
public ResponseEntity<Void> deleteBookingIemByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
@ -106,7 +101,6 @@ public class HsBookingProjectController implements HsBookingProjectsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.bookingProjects.api.patchBookingProject")
|
||||
public ResponseEntity<HsBookingProjectResource> patchBookingProject(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
|
@ -32,7 +32,7 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.fetchedBySql
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_booking", name = "project_rv")
|
||||
@Table(name = "hs_booking_project_rv")
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@Setter
|
||||
@ -43,8 +43,8 @@ public class HsBookingProjectRbacEntity extends HsBookingProject {
|
||||
return rbacViewFor("project", HsBookingProjectRbacEntity.class)
|
||||
.withIdentityView(SQL.query("""
|
||||
SELECT bookingProject.uuid as uuid, debitorIV.idName || '-' || base.cleanIdentifier(bookingProject.caption) as idName
|
||||
FROM hs_booking.project bookingProject
|
||||
JOIN hs_office.debitor_iv debitorIV ON debitorIV.uuid = bookingProject.debitorUuid
|
||||
FROM hs_booking_project bookingProject
|
||||
JOIN hs_office_debitor_iv debitorIV ON debitorIV.uuid = bookingProject.debitorUuid
|
||||
"""))
|
||||
.withRestrictedViewOrderBy(SQL.expression("caption"))
|
||||
.withUpdatableColumns("version", "caption")
|
||||
@ -58,8 +58,8 @@ public class HsBookingProjectRbacEntity extends HsBookingProject {
|
||||
dependsOnColumn("debitorUuid"),
|
||||
fetchedBySql("""
|
||||
SELECT ${columns}
|
||||
FROM hs_office.relation debitorRel
|
||||
JOIN hs_office.debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid
|
||||
FROM hs_office_relation debitorRel
|
||||
JOIN hs_office_debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid
|
||||
WHERE debitor.uuid = ${REF}.debitorUuid
|
||||
"""),
|
||||
NOT_NULL)
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.project;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
import java.util.List;
|
||||
@ -10,21 +9,14 @@ import java.util.UUID;
|
||||
public interface HsBookingProjectRbacRepository extends HsBookingProjectRepository<HsBookingProjectRbacEntity>,
|
||||
Repository<HsBookingProjectRbacEntity, UUID> {
|
||||
|
||||
@Timed("app.bookingProjects.repo.findByUuid.rbac")
|
||||
Optional<HsBookingProjectRbacEntity> findByUuid(final UUID bookingProjectUuid);
|
||||
|
||||
@Timed("app.bookingProjects.repo.findByCaption.rbac")
|
||||
List<HsBookingProjectRbacEntity> findByCaption(final String projectCaption);
|
||||
|
||||
@Timed("app.bookingProjects.repo.findAllByDebitorUuid.rbac")
|
||||
List<HsBookingProjectRbacEntity> findAllByDebitorUuid(final UUID bookingProjectUuid);
|
||||
|
||||
@Timed("app.bookingProjects.repo.save.rbac")
|
||||
HsBookingProjectRbacEntity save(HsBookingProjectRbacEntity current);
|
||||
|
||||
@Timed("app.bookingProjects.repo.deleteByUuid.rbac")
|
||||
int deleteByUuid(final UUID uuid);
|
||||
|
||||
@Timed("app.bookingProjects.repo.count.rbac")
|
||||
long count();
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import jakarta.persistence.Table;
|
||||
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_booking", name = "project")
|
||||
@Table(name = "hs_booking_project")
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@Setter
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.project;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
import java.util.List;
|
||||
@ -10,21 +9,14 @@ import java.util.UUID;
|
||||
public interface HsBookingProjectRealRepository extends HsBookingProjectRepository<HsBookingProjectRealEntity>,
|
||||
Repository<HsBookingProjectRealEntity, UUID> {
|
||||
|
||||
@Timed("app.bookingProjects.repo.findByUuid.real")
|
||||
Optional<HsBookingProjectRealEntity> findByUuid(final UUID bookingProjectUuid);
|
||||
|
||||
@Timed("app.bookingProjects.repo.findByCaption.real")
|
||||
List<HsBookingProjectRealEntity> findByCaption(final String projectCaption);
|
||||
|
||||
@Timed("app.bookingProjects.repo.findAllByDebitorUuid.real")
|
||||
List<HsBookingProjectRealEntity> findAllByDebitorUuid(final UUID bookingProjectUuid);
|
||||
|
||||
@Timed("app.bookingProjects.repo.save.real")
|
||||
HsBookingProjectRealEntity save(HsBookingProjectRealEntity current);
|
||||
|
||||
@Timed("app.bookingProjects.repo.deleteByUuid.real")
|
||||
int deleteByUuid(final UUID uuid);
|
||||
|
||||
@Timed("app.bookingProjects.repo.count.real")
|
||||
long count();
|
||||
}
|
||||
|
@ -1,28 +1,19 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.project;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface HsBookingProjectRepository<E extends HsBookingProject> {
|
||||
|
||||
@Timed("app.booking.projects.repo.findByUuid")
|
||||
Optional<E> findByUuid(final UUID findByUuid);
|
||||
|
||||
@Timed("app.booking.projects.repo.findByCaption")
|
||||
Optional<E> findByUuid(final UUID bookingProjectUuid);
|
||||
List<E> findByCaption(final String projectCaption);
|
||||
|
||||
@Timed("app.booking.projects.repo.findAllByDebitorUuid")
|
||||
List<E> findAllByDebitorUuid(final UUID bookingProjectUuid);
|
||||
|
||||
@Timed("app.booking.projects.repo.save")
|
||||
E save(E current);
|
||||
|
||||
@Timed("app.booking.projects.repo.deleteByUuid")
|
||||
int deleteByUuid(final UUID uuid);
|
||||
|
||||
@Timed("app.booking.projects.repo.count")
|
||||
long count();
|
||||
}
|
||||
|
@ -14,9 +14,9 @@ import net.hostsharing.hsadminng.hs.booking.project.HsBookingProject;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
|
||||
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.repr.Stringify;
|
||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.hibernate.annotations.Type;
|
||||
|
||||
import jakarta.persistence.CascadeType;
|
||||
@ -42,7 +42,7 @@ import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static java.util.Collections.emptyMap;
|
||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@MappedSuperclass
|
||||
@Getter
|
||||
@ -87,11 +87,12 @@ public abstract class HsHostingAsset implements Stringifyable, BaseEntity<HsHost
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "alarmcontactuuid")
|
||||
private HsOfficeContactRealEntity alarmContact;
|
||||
private HsOfficeContactRealEntity alarmContact; // TODO.impl: rename to alertContact, always/uncatched, 1:n
|
||||
|
||||
@Builder.Default
|
||||
@OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true, fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "parentassetuuid", referencedColumnName = "uuid")
|
||||
private List<HsHostingAssetRealEntity> subHostingAssets;
|
||||
private List<HsHostingAssetRealEntity> subHostingAssets = new ArrayList<>();
|
||||
|
||||
@Column(name = "identifier")
|
||||
private String identifier; // e.g. vm1234, xyz00, example.org, xyz00_abc
|
||||
@ -124,13 +125,6 @@ public abstract class HsHostingAsset implements Stringifyable, BaseEntity<HsHost
|
||||
PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper;}, config).assign(newConfig);
|
||||
}
|
||||
|
||||
public List<HsHostingAssetRealEntity> getSubHostingAssets() {
|
||||
if (subHostingAssets == null) {
|
||||
subHostingAssets = new ArrayList<>();
|
||||
}
|
||||
return subHostingAssets;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PatchableMapWrapper<Object> directProps() {
|
||||
return getConfig();
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealRepository;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntitySaveProcessor;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntityValidatorRegistry;
|
||||
@ -12,7 +11,7 @@ import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAsse
|
||||
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetResource;
|
||||
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource;
|
||||
import net.hostsharing.hsadminng.mapper.KeyValueMap;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@ -36,7 +35,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private StandardMapper mapper;
|
||||
private Mapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsHostingAssetRbacRepository rbacAssetRepo;
|
||||
@ -49,8 +48,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.hosting.assets.api.getListOfHostingAssets")
|
||||
public ResponseEntity<List<HsHostingAssetResource>> getListOfHostingAssets(
|
||||
public ResponseEntity<List<HsHostingAssetResource>> listAssets(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID debitorUuid,
|
||||
@ -67,8 +65,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.hosting.assets.api.postNewHostingAsset")
|
||||
public ResponseEntity<HsHostingAssetResource> postNewHostingAsset(
|
||||
public ResponseEntity<HsHostingAssetResource> addAsset(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final HsHostingAssetInsertResource body) {
|
||||
@ -96,8 +93,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.hosting.assets.api.getSingleHostingAssetByUuid")
|
||||
public ResponseEntity<HsHostingAssetResource> getSingleHostingAssetByUuid(
|
||||
public ResponseEntity<HsHostingAssetResource> getAssetByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID assetUuid) {
|
||||
@ -113,8 +109,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.hosting.assets.api.deleteHostingAssetByUuid")
|
||||
public ResponseEntity<Void> deleteHostingAssetByUuid(
|
||||
public ResponseEntity<Void> deleteAssetUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID assetUuid) {
|
||||
@ -128,8 +123,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.hosting.assets.api.patchHostingAsset")
|
||||
public ResponseEntity<HsHostingAssetResource> patchHostingAsset(
|
||||
public ResponseEntity<HsHostingAssetResource> patchAsset(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID assetUuid,
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntityValidatorRegistry;
|
||||
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetPropsApi;
|
||||
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource;
|
||||
@ -15,8 +14,7 @@ import java.util.Map;
|
||||
public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
|
||||
|
||||
@Override
|
||||
@Timed("app.hosting.assets.api.getListOfHostingAssetTypes")
|
||||
public ResponseEntity<List<String>> getListOfHostingAssetTypes() {
|
||||
public ResponseEntity<List<String>> listAssetTypes() {
|
||||
final var resource = HostingAssetEntityValidatorRegistry.types().stream()
|
||||
.map(Enum::name)
|
||||
.toList();
|
||||
@ -24,8 +22,7 @@ public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Timed("app.hosting.assets.api.getListOfHostingAssetTypeProps")
|
||||
public ResponseEntity<List<Object>> getListOfHostingAssetTypeProps(
|
||||
public ResponseEntity<List<Object>> listAssetTypeProps(
|
||||
final HsHostingAssetTypeResource assetType) {
|
||||
|
||||
final Enum<HsHostingAssetType> type = HsHostingAssetType.of(assetType);
|
||||
|
@ -4,7 +4,7 @@ import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRbacEntity;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRbacEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
@ -33,7 +33,7 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetc
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_hosting", name = "asset_rv")
|
||||
@Table(name = "hs_hosting_asset_rv")
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@Setter
|
||||
@ -47,7 +47,7 @@ public class HsHostingAssetRbacEntity extends HsHostingAsset {
|
||||
.withUpdatableColumns("version", "caption", "config", "assignedToAssetUuid", "alarmContactUuid")
|
||||
.toRole(GLOBAL, ADMIN).grantPermission(INSERT) // TODO.impl: Why is this necessary to insert test data?
|
||||
|
||||
.importEntityAlias("bookingItem", HsBookingItemRbacEntity.class, usingDefaultCase(),
|
||||
.importEntityAlias("bookingItem", HsBookingItem.class, usingDefaultCase(),
|
||||
dependsOnColumn("bookingItemUuid"),
|
||||
directlyFetchedByDependsOnColumn(),
|
||||
NULLABLE)
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
@ -11,10 +10,8 @@ import java.util.UUID;
|
||||
|
||||
public interface HsHostingAssetRbacRepository extends HsHostingAssetRepository<HsHostingAssetRbacEntity>, Repository<HsHostingAssetRbacEntity, UUID> {
|
||||
|
||||
@Timed("app.hostingAsset.repo.findByUuid.rbac")
|
||||
Optional<HsHostingAssetRbacEntity> findByUuid(final UUID serverUuid);
|
||||
|
||||
@Timed("app.hostingAsset.repo.findByIdentifier.rbac")
|
||||
List<HsHostingAssetRbacEntity> findByIdentifier(String assetIdentifier);
|
||||
|
||||
@Query(value = """
|
||||
@ -28,28 +25,23 @@ public interface HsHostingAssetRbacRepository extends HsHostingAssetRepository<H
|
||||
ha.parentassetuuid,
|
||||
ha.type,
|
||||
ha.version
|
||||
from hs_hosting.asset_rv ha
|
||||
left join hs_booking.item bi on bi.uuid = ha.bookingitemuuid
|
||||
left join hs_hosting.asset pha on pha.uuid = ha.parentassetuuid
|
||||
from hs_hosting_asset_rv ha
|
||||
left join hs_booking_item bi on bi.uuid = ha.bookingitemuuid
|
||||
left join hs_hosting_asset pha on pha.uuid = ha.parentassetuuid
|
||||
where (:projectUuid is null or bi.projectuuid=:projectUuid)
|
||||
and (:parentAssetUuid is null or pha.uuid=:parentAssetUuid)
|
||||
and (:type is null or :type=cast(ha.type as text))
|
||||
""", nativeQuery = true)
|
||||
@Timed("app.hostingAsset.repo.findAllByCriteriaImpl.rbac")
|
||||
// The JPQL query did not generate "left join" but just "join".
|
||||
// I also optimized the query by not using the _rv for hs_booking.item and hs_hosting.asset, only for hs_hosting.asset_rv.
|
||||
// I also optimized the query by not using the _rv for hs_booking_item and hs_hosting_asset, only for hs_hosting_asset_rv.
|
||||
List<HsHostingAssetRbacEntity> findAllByCriteriaImpl(UUID projectUuid, UUID parentAssetUuid, String type);
|
||||
|
||||
default List<HsHostingAssetRbacEntity> findAllByCriteria(final UUID projectUuid, final UUID parentAssetUuid, final HsHostingAssetType type) {
|
||||
return findAllByCriteriaImpl(projectUuid, parentAssetUuid, HsHostingAssetType.asString(type));
|
||||
}
|
||||
|
||||
@Timed("app.hostingAsset.repo.save.rbac")
|
||||
HsHostingAssetRbacEntity save(HsHostingAsset current);
|
||||
|
||||
@Timed("app.hostingAsset.repo.deleteByUuid.rbac")
|
||||
int deleteByUuid(final UUID uuid);
|
||||
|
||||
@Timed("app.hostingAsset.repo.count.rbac")
|
||||
long count();
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_hosting", name = "asset")
|
||||
@Table(name = "hs_hosting_asset")
|
||||
@SuperBuilder(builderMethodName = "genericBuilder", toBuilder = true)
|
||||
@Getter
|
||||
@Setter
|
||||
|
@ -1,35 +1,18 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface HsHostingAssetRealRepository extends HsHostingAssetRepository<HsHostingAssetRealEntity>, Repository<HsHostingAssetRealEntity, UUID> {
|
||||
|
||||
@Timed("app.hostingAsset.repo.findByUuid.real")
|
||||
Optional<HsHostingAssetRealEntity> findByUuid(final UUID serverUuid);
|
||||
|
||||
@Timed("app.hostingAsset.repo.findByIdentifier.real")
|
||||
List<HsHostingAssetRealEntity> findByIdentifier(String assetIdentifier);
|
||||
|
||||
default List<HsHostingAssetRealEntity> findByTypeAndIdentifier(@NotNull HsHostingAssetType type, @NotNull String identifier) {
|
||||
return findByTypeAndIdentifierImpl(type.name(), identifier);
|
||||
}
|
||||
|
||||
@Query("""
|
||||
select ha
|
||||
from HsHostingAssetRealEntity ha
|
||||
where cast(ha.type as String) = :type
|
||||
and ha.identifier = :identifier
|
||||
""")
|
||||
@Timed("app.hostingAsset.repo.findByTypeAndIdentifierImpl.real")
|
||||
List<HsHostingAssetRealEntity> findByTypeAndIdentifierImpl(@NotNull String type, @NotNull String identifier);
|
||||
|
||||
@Query(value = """
|
||||
select ha.uuid,
|
||||
ha.alarmcontactuuid,
|
||||
@ -41,28 +24,23 @@ public interface HsHostingAssetRealRepository extends HsHostingAssetRepository<H
|
||||
ha.parentassetuuid,
|
||||
ha.type,
|
||||
ha.version
|
||||
from hs_hosting.asset_rv ha
|
||||
left join hs_booking.item bi on bi.uuid = ha.bookingitemuuid
|
||||
left join hs_hosting.asset pha on pha.uuid = ha.parentassetuuid
|
||||
from hs_hosting_asset_rv ha
|
||||
left join hs_booking_item bi on bi.uuid = ha.bookingitemuuid
|
||||
left join hs_hosting_asset pha on pha.uuid = ha.parentassetuuid
|
||||
where (:projectUuid is null or bi.projectuuid=:projectUuid)
|
||||
and (:parentAssetUuid is null or pha.uuid=:parentAssetUuid)
|
||||
and (:type is null or :type=cast(ha.type as text))
|
||||
""", nativeQuery = true)
|
||||
// The JPQL query did not generate "left join" but just "join".
|
||||
// I also optimized the query by not using the _rv for hs_booking.item and hs_hosting.asset, only for hs_hosting.asset_rv.
|
||||
@Timed("app.hostingAsset.repo.findAllByCriteriaImpl.real")
|
||||
// I also optimized the query by not using the _rv for hs_booking_item and hs_hosting_asset, only for hs_hosting_asset_rv.
|
||||
List<HsHostingAssetRealEntity> findAllByCriteriaImpl(UUID projectUuid, UUID parentAssetUuid, String type);
|
||||
|
||||
default List<HsHostingAssetRealEntity> findAllByCriteria(final UUID projectUuid, final UUID parentAssetUuid, final HsHostingAssetType type) {
|
||||
return findAllByCriteriaImpl(projectUuid, parentAssetUuid, HsHostingAssetType.asString(type));
|
||||
}
|
||||
|
||||
@Timed("app.hostingAsset.repo.save.real")
|
||||
HsHostingAssetRealEntity save(HsHostingAssetRealEntity current);
|
||||
|
||||
@Timed("app.hostingAsset.repo.deleteByUuid.real")
|
||||
int deleteByUuid(final UUID uuid);
|
||||
|
||||
@Timed("app.hostingAsset.repo.count.real")
|
||||
long count();
|
||||
}
|
||||
|
@ -1,32 +1,24 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface HsHostingAssetRepository<E extends HsHostingAsset> {
|
||||
|
||||
@Timed("app.hosting.assets.repo.findByUuid")
|
||||
Optional<E> findByUuid(final UUID serverUuid);
|
||||
|
||||
@Timed("app.hosting.assets.repo.findByIdentifier")
|
||||
List<E> findByIdentifier(String assetIdentifier);
|
||||
|
||||
@Timed("app.hosting.assets.repo.findAllByCriteriaImpl")
|
||||
List<E> findAllByCriteriaImpl(UUID projectUuid, UUID parentAssetUuid, String type);
|
||||
|
||||
default List<E> findAllByCriteria(final UUID projectUuid, final UUID parentAssetUuid, final HsHostingAssetType type) {
|
||||
return findAllByCriteriaImpl(projectUuid, parentAssetUuid, HsHostingAssetType.asString(type));
|
||||
}
|
||||
|
||||
@Timed("app.hosting.assets.repo.save")
|
||||
E save(HsHostingAsset current);
|
||||
|
||||
@Timed("app.hosting.assets.repo.deleteByUuid")
|
||||
int deleteByUuid(final UUID uuid);
|
||||
|
||||
@Timed("app.hosting.assets.repo.count")
|
||||
long count();
|
||||
}
|
||||
|
@ -1,159 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset.factories;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetSubInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetTypeResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
|
||||
import net.hostsharing.hsadminng.lambda.Reducer;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.mapper.ToStringConverter;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
|
||||
import jakarta.validation.ValidationException;
|
||||
import java.net.IDN;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_DNS_SETUP;
|
||||
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_HTTP_SETUP;
|
||||
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_MBOX_SETUP;
|
||||
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SMTP_SETUP;
|
||||
|
||||
public class DomainSetupHostingAssetFactory extends HostingAssetFactory {
|
||||
|
||||
public DomainSetupHostingAssetFactory(
|
||||
final EntityManagerWrapper emw,
|
||||
final HsBookingItemRealEntity newBookingItemRealEntity,
|
||||
final HsHostingAssetAutoInsertResource asset,
|
||||
final StandardMapper standardMapper) {
|
||||
super(emw, newBookingItemRealEntity, asset, standardMapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HsHostingAsset create() {
|
||||
final var domainSetupAsset = createDomainSetupAsset(getDomainName());
|
||||
final var subHostingAssets = domainSetupAsset.getSubHostingAssets();
|
||||
|
||||
// TODO.legacy: as long as we need to be compatible, we always do all technical domain-setups
|
||||
|
||||
final var domainHttpSetupAssetResource = findSubHostingAssetResource(HsHostingAssetTypeResource.DOMAIN_HTTP_SETUP);
|
||||
final var assignedToUnixUserAssetEntity = domainHttpSetupAssetResource
|
||||
.map(HsHostingAssetSubInsertResource::getAssignedToAssetUuid)
|
||||
.map(uuid -> emw.find(HsHostingAssetRealEntity.class, uuid))
|
||||
.orElseThrow(() -> new ValidationException("DOMAIN_HTTP_SETUP subAsset with assignedToAssetUuid required in compatibility mode"));
|
||||
|
||||
subHostingAssets.add(
|
||||
createDomainSubSetupAssetEntity(
|
||||
domainSetupAsset,
|
||||
DOMAIN_HTTP_SETUP,
|
||||
builder -> builder
|
||||
.assignedToAsset(assignedToUnixUserAssetEntity)
|
||||
.identifier(getDomainName() + "|HTTP")
|
||||
.caption("HTTP-Setup für " + IDN.toUnicode(getDomainName())))
|
||||
);
|
||||
|
||||
// Do not add to subHostingAssets in compatibility mode, in this case, DNS setup works via file system.
|
||||
// The entity is created just for validation purposes.
|
||||
createDomainSubSetupAssetEntity(
|
||||
domainSetupAsset,
|
||||
DOMAIN_DNS_SETUP,
|
||||
builder -> builder
|
||||
.assignedToAsset(assignedToUnixUserAssetEntity.getParentAsset())
|
||||
.identifier(getDomainName() + "|DNS")
|
||||
.caption("DNS-Setup für " + IDN.toUnicode(getDomainName())));
|
||||
|
||||
subHostingAssets.add(
|
||||
createDomainSubSetupAssetEntity(
|
||||
domainSetupAsset,
|
||||
DOMAIN_MBOX_SETUP,
|
||||
builder -> builder
|
||||
.assignedToAsset(assignedToUnixUserAssetEntity.getParentAsset())
|
||||
.identifier(getDomainName() + "|MBOX")
|
||||
.caption("MBOX-Setup für " + IDN.toUnicode(getDomainName())))
|
||||
);
|
||||
|
||||
subHostingAssets.add(
|
||||
createDomainSubSetupAssetEntity(
|
||||
domainSetupAsset,
|
||||
DOMAIN_SMTP_SETUP,
|
||||
builder -> builder
|
||||
.assignedToAsset(assignedToUnixUserAssetEntity.getParentAsset())
|
||||
.identifier(getDomainName() + "|SMTP")
|
||||
.caption("SMTP-Setup für " + IDN.toUnicode(getDomainName())))
|
||||
);
|
||||
|
||||
return domainSetupAsset;
|
||||
}
|
||||
|
||||
private HsHostingAssetRealEntity createDomainSetupAsset(final String domainName) {
|
||||
return HsHostingAssetRealEntity.builder()
|
||||
.bookingItem(fromBookingItem)
|
||||
.type(HsHostingAssetType.DOMAIN_SETUP)
|
||||
.identifier(domainName)
|
||||
.caption(asset.getCaption() != null ? asset.getCaption() : domainName)
|
||||
.alarmContact(ref(HsOfficeContactRealEntity.class, asset.getAlarmContactUuid()))
|
||||
// the sub-hosting-assets get added later
|
||||
.build();
|
||||
}
|
||||
|
||||
private HsHostingAssetRealEntity createDomainSubSetupAssetEntity(
|
||||
final HsHostingAssetRealEntity domainSetupAsset,
|
||||
final HsHostingAssetType subAssetType,
|
||||
final Function<HsHostingAssetRealEntity.HsHostingAssetRealEntityBuilder<?, ?>, HsHostingAssetRealEntity.HsHostingAssetRealEntityBuilder<?, ?>> builderTransformer) {
|
||||
final var resourceType = HsHostingAssetTypeResource.valueOf(subAssetType.name());
|
||||
|
||||
final var subAssetResourceOptional = findSubHostingAssetResource(resourceType);
|
||||
|
||||
subAssetResourceOptional.ifPresentOrElse(
|
||||
this::verifyNotOverspecified,
|
||||
() -> { throw new ValidationException("sub-asset of type " + resourceType.name() + " required in legacy mode, but missing"); }
|
||||
);
|
||||
|
||||
return builderTransformer.apply(
|
||||
HsHostingAssetRealEntity.builder()
|
||||
.type(subAssetType)
|
||||
.parentAsset(domainSetupAsset))
|
||||
.build();
|
||||
}
|
||||
|
||||
private Optional<HsHostingAssetSubInsertResource> findSubHostingAssetResource(final HsHostingAssetTypeResource resourceType) {
|
||||
return getSubHostingAssetResources().stream()
|
||||
.filter(ha -> ha.getType() == resourceType)
|
||||
.reduce(Reducer::toSingleElement);
|
||||
}
|
||||
|
||||
// TODO.legacy: while we need to stay compatible, only default values can be used, thus only the type can be specified
|
||||
private void verifyNotOverspecified(final HsHostingAssetSubInsertResource givenSubAssetResource) {
|
||||
final var convert = new ToStringConverter().ignoring("assignedToAssetUuid");
|
||||
final var expectedSubAssetResource = new HsHostingAssetSubInsertResource();
|
||||
expectedSubAssetResource.setType(givenSubAssetResource.getType());
|
||||
if ( !convert.from(givenSubAssetResource).equals(convert.from(expectedSubAssetResource)) ) {
|
||||
throw new ValidationException("sub asset " + givenSubAssetResource.getType() + " is over-specified, in compatibility mode, only default values allowed");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private String getDomainName() {
|
||||
return asset.getIdentifier();
|
||||
}
|
||||
|
||||
private List<HsHostingAssetSubInsertResource> getSubHostingAssetResources() {
|
||||
return asset.getSubHostingAssets();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void persist(final HsHostingAsset newHostingAsset) {
|
||||
super.persist(newHostingAsset);
|
||||
newHostingAsset.getSubHostingAssets().forEach(super::persist);
|
||||
}
|
||||
|
||||
private <T> T ref(final Class<T> entityClass, final UUID uuid) {
|
||||
return uuid != null ? emw.getReference(entityClass, uuid) : null;
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset.factories;
|
||||
|
||||
import jakarta.validation.ValidationException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntitySaveProcessor;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
|
||||
|
||||
@RequiredArgsConstructor
|
||||
abstract class HostingAssetFactory {
|
||||
|
||||
final EntityManagerWrapper emw;
|
||||
final HsBookingItemRealEntity fromBookingItem;
|
||||
final HsHostingAssetAutoInsertResource asset;
|
||||
final StandardMapper standardMapper;
|
||||
|
||||
protected abstract HsHostingAsset create();
|
||||
|
||||
public String createAndPersist() {
|
||||
try {
|
||||
final HsHostingAsset newHostingAsset = create();
|
||||
persist(newHostingAsset);
|
||||
return null;
|
||||
} catch (final ValidationException exc) {
|
||||
return exc.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
protected void persist(final HsHostingAsset newHostingAsset) {
|
||||
new HostingAssetEntitySaveProcessor(emw, newHostingAsset)
|
||||
.preprocessEntity()
|
||||
.validateEntity()
|
||||
.prepareForSave()
|
||||
.save()
|
||||
.validateContext();
|
||||
}
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset.factories;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.validation.ValidationException;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.SneakyThrows;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedAppEvent;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class HsBookingItemCreatedListener implements ApplicationListener<BookingItemCreatedAppEvent> {
|
||||
|
||||
@Autowired
|
||||
private EntityManagerWrapper emw;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper jsonMapper;
|
||||
|
||||
@Autowired
|
||||
private StandardMapper standardMapper;
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void onApplicationEvent(@NotNull BookingItemCreatedAppEvent bookingItemCreatedAppEvent) {
|
||||
if (containsAssetJson(bookingItemCreatedAppEvent)) {
|
||||
createRelatedHostingAsset(bookingItemCreatedAppEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean containsAssetJson(final BookingItemCreatedAppEvent bookingItemCreatedAppEvent) {
|
||||
return bookingItemCreatedAppEvent.getEntity().getAssetJson() != null;
|
||||
}
|
||||
|
||||
private void createRelatedHostingAsset(final BookingItemCreatedAppEvent event) throws JsonProcessingException {
|
||||
final var newBookingItemRealEntity = event.getEntity().getBookingItem();
|
||||
final var asset = jsonMapper.readValue(event.getEntity().getAssetJson(), HsHostingAssetAutoInsertResource.class);
|
||||
final var factory = switch (newBookingItemRealEntity.getType()) {
|
||||
case PRIVATE_CLOUD, CLOUD_SERVER, MANAGED_SERVER ->
|
||||
forNowNoAutomaticHostingAssetCreationPossible(emw, newBookingItemRealEntity, asset, standardMapper);
|
||||
case MANAGED_WEBSPACE -> new ManagedWebspaceHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper);
|
||||
case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper);
|
||||
};
|
||||
if (factory != null) {
|
||||
final var statusMessage = factory.createAndPersist();
|
||||
// TODO.impl: once we implement retry, we need to amend this code (persist/merge/delete)
|
||||
if (statusMessage != null) {
|
||||
event.getEntity().setStatusMessage(statusMessage);
|
||||
emw.persist(event.getEntity());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private HostingAssetFactory forNowNoAutomaticHostingAssetCreationPossible(
|
||||
final EntityManagerWrapper emw,
|
||||
final HsBookingItemRealEntity fromBookingItem,
|
||||
final HsHostingAssetAutoInsertResource asset,
|
||||
final StandardMapper standardMapper
|
||||
) {
|
||||
return new HostingAssetFactory(emw, fromBookingItem, asset, standardMapper) {
|
||||
|
||||
@Override
|
||||
protected HsHostingAsset create() {
|
||||
// TODO.impl: we should validate the asset JSON, but some violations are un-avoidable at that stage
|
||||
throw new ValidationException("waiting for manual setup of hosting asset for booking item of type " + fromBookingItem.getType());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset.factories;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetTypeResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
|
||||
import jakarta.validation.ValidationException;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
|
||||
public class ManagedWebspaceHostingAssetFactory extends HostingAssetFactory {
|
||||
|
||||
public ManagedWebspaceHostingAssetFactory(
|
||||
final EntityManagerWrapper emw,
|
||||
final HsBookingItemRealEntity newBookingItemRealEntity,
|
||||
final HsHostingAssetAutoInsertResource asset,
|
||||
final StandardMapper standardMapper) {
|
||||
super(emw, newBookingItemRealEntity, asset, standardMapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HsHostingAsset create() {
|
||||
if (asset.getType() != HsHostingAssetTypeResource.MANAGED_WEBSPACE) {
|
||||
throw new ValidationException("requires MANAGED_WEBSPACE hosting asset, but got " +
|
||||
Optional.of(asset)
|
||||
.map(HsHostingAssetAutoInsertResource::getType)
|
||||
.map(Enum::name)
|
||||
.orElse(null));
|
||||
}
|
||||
final var managedWebspaceHostingAsset = standardMapper.map(asset, HsHostingAssetRealEntity.class);
|
||||
managedWebspaceHostingAsset.setBookingItem(fromBookingItem);
|
||||
emw.createQuery(
|
||||
"SELECT asset FROM HsHostingAssetRealEntity asset WHERE asset.bookingItem.uuid=:bookingItemUuid",
|
||||
HsHostingAssetRealEntity.class)
|
||||
.setParameter("bookingItemUuid", fromBookingItem.getParentItem().getUuid())
|
||||
.getResultStream().findFirst()
|
||||
.ifPresent(managedWebspaceHostingAsset::setParentAsset);
|
||||
|
||||
return managedWebspaceHostingAsset;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void persist(final HsHostingAsset newManagedWebspaceHostingAsset) {
|
||||
super.persist(newManagedWebspaceHostingAsset);
|
||||
}
|
||||
}
|
@ -42,7 +42,7 @@ public class HostingAssetEntitySaveProcessor {
|
||||
return this;
|
||||
}
|
||||
|
||||
// TODO.legacy: remove once the migration of legacy data is done
|
||||
// TODO.impl: remove once the migration of legacy data is done
|
||||
/// validates the entity itself including its properties, but ignoring some error messages for import of legacy data
|
||||
public HostingAssetEntitySaveProcessor validateEntityIgnoring(final String... ignoreRegExp) {
|
||||
step("validateEntity", "prepareForSave");
|
||||
|
@ -15,7 +15,7 @@ import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanPro
|
||||
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
|
||||
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
|
||||
|
||||
// TODO.legacy: make package private once we've migrated the legacy data
|
||||
// TODO.impl: make package private once we've migrated the legacy data
|
||||
public class HsDomainDnsSetupHostingAssetValidator extends HostingAssetEntityValidator {
|
||||
|
||||
// according to RFC 1035 (section 5) and RFC 1034
|
||||
@ -33,7 +33,7 @@ public class HsDomainDnsSetupHostingAssetValidator extends HostingAssetEntityVal
|
||||
RR_REGEX_NAME + RR_REGEX_IN + RR_REGEX_TTL + RR_RECORD_TYPE + RR_RECORD_DATA + RR_COMMENT;
|
||||
public static final String IDENTIFIER_SUFFIX = "|DNS";
|
||||
|
||||
private static List<String> zoneFileErrors = null; // TODO.legacy: remove once legacy data is migrated
|
||||
private static List<String> zoneFileErrors = null; // TODO.impl: remove once legacy data is migrated
|
||||
|
||||
HsDomainDnsSetupHostingAssetValidator() {
|
||||
super(
|
||||
|
@ -53,7 +53,7 @@ class HsUnixUserHostingAssetValidator extends HostingAssetEntityValidator {
|
||||
}
|
||||
|
||||
private static Integer computeUserId(final EntityManager em, final PropertiesProvider propertiesProvider) {
|
||||
final Object result = em.createNativeQuery("SELECT nextval('hs_hosting.asset_unixuser_system_id_seq')", Integer.class)
|
||||
final Object result = em.createNativeQuery("SELECT nextval('hs_hosting_asset_unixuser_system_id_seq')", Integer.class)
|
||||
.getSingleResult();
|
||||
return (Integer) result;
|
||||
}
|
||||
|
@ -1,11 +1,10 @@
|
||||
package net.hostsharing.hsadminng.hs.office.bankaccount;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeBankAccountsApi;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountResource;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import org.iban4j.BicUtil;
|
||||
import org.iban4j.IbanUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@ -25,15 +24,14 @@ public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private StandardMapper mapper;
|
||||
private Mapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeBankAccountRepository bankAccountRepo;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.bankAccounts.api.patchDebitor")
|
||||
public ResponseEntity<List<HsOfficeBankAccountResource>> getListOfBankAccounts(
|
||||
public ResponseEntity<List<HsOfficeBankAccountResource>> listBankAccounts(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final String holder) {
|
||||
@ -47,8 +45,7 @@ public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.bankAccounts.api.postNewBankAccount")
|
||||
public ResponseEntity<HsOfficeBankAccountResource> postNewBankAccount(
|
||||
public ResponseEntity<HsOfficeBankAccountResource> addBankAccount(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final HsOfficeBankAccountInsertResource body) {
|
||||
@ -74,8 +71,7 @@ public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.bankAccounts.api.getSingleBankAccountByUuid")
|
||||
public ResponseEntity<HsOfficeBankAccountResource> getSingleBankAccountByUuid(
|
||||
public ResponseEntity<HsOfficeBankAccountResource> getBankAccountByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID bankAccountUuid) {
|
||||
@ -91,7 +87,6 @@ public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.bankAccounts.api.deleteBankAccountByUuid")
|
||||
public ResponseEntity<Void> deleteBankAccountByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
|
@ -3,10 +3,10 @@ package net.hostsharing.hsadminng.hs.office.bankaccount;
|
||||
import lombok.*;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.repr.Stringify;
|
||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
@ -16,10 +16,10 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacView.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
|
||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_office", name = "bankaccount_rv")
|
||||
@Table(name = "hs_office_bankaccount_rv")
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.office.bankaccount;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
@ -10,31 +9,23 @@ import java.util.UUID;
|
||||
|
||||
public interface HsOfficeBankAccountRepository extends Repository<HsOfficeBankAccountEntity, UUID> {
|
||||
|
||||
@Timed("app.office.bankAccounts.repo.findByUuid")
|
||||
Optional<HsOfficeBankAccountEntity> findByUuid(UUID id);
|
||||
|
||||
@Query("""
|
||||
SELECT c FROM HsOfficeBankAccountEntity c
|
||||
WHERE lower(c.holder) like lower(concat(:holder, '%'))
|
||||
ORDER BY c.holder
|
||||
""")
|
||||
@Timed("app.office.bankAccounts.repo.findByOptionalHolderLikeImpl")
|
||||
""")
|
||||
List<HsOfficeBankAccountEntity> findByOptionalHolderLikeImpl(String holder);
|
||||
|
||||
default List<HsOfficeBankAccountEntity> findByOptionalHolderLike(String holder) {
|
||||
return findByOptionalHolderLikeImpl(holder == null ? "" : holder);
|
||||
}
|
||||
|
||||
|
||||
@Timed("app.office.bankAccounts.repo.findByIbanOrderByIbanAsc")
|
||||
List<HsOfficeBankAccountEntity> findByIbanOrderByIbanAsc(String iban);
|
||||
|
||||
@Timed("app.office.bankAccounts.repo.save")
|
||||
<S extends HsOfficeBankAccountEntity> S save(S entity);
|
||||
|
||||
@Timed("app.office.bankAccounts.repo.deleteByUuid")
|
||||
int deleteByUuid(final UUID uuid);
|
||||
|
||||
@Timed("app.office.bankAccounts.repo.count")
|
||||
long count();
|
||||
}
|
||||
|
@ -11,9 +11,9 @@ import lombok.experimental.FieldNameConstants;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.repr.Stringify;
|
||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.hibernate.annotations.GenericGenerator;
|
||||
import org.hibernate.annotations.Type;
|
||||
|
||||
@ -27,7 +27,7 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@MappedSuperclass
|
||||
@Getter
|
||||
@ -54,14 +54,8 @@ public class HsOfficeContact implements Stringifyable, BaseEntity<HsOfficeContac
|
||||
@Column(name = "caption")
|
||||
private String caption;
|
||||
|
||||
@Builder.Default
|
||||
@Setter(AccessLevel.NONE)
|
||||
@Type(JsonType.class)
|
||||
@Column(name = "postaladdress")
|
||||
private Map<String, String> postalAddress = new HashMap<>();
|
||||
|
||||
@Transient
|
||||
private PatchableMapWrapper<String> postalAddressWrapper;
|
||||
private String postalAddress; // multiline free-format text
|
||||
|
||||
@Builder.Default
|
||||
@Setter(AccessLevel.NONE)
|
||||
@ -81,17 +75,6 @@ public class HsOfficeContact implements Stringifyable, BaseEntity<HsOfficeContac
|
||||
@Transient
|
||||
private PatchableMapWrapper<String> phoneNumbersWrapper;
|
||||
|
||||
public PatchableMapWrapper<String> getPostalAddress() {
|
||||
return PatchableMapWrapper.of(
|
||||
postalAddressWrapper,
|
||||
(newWrapper) -> {postalAddressWrapper = newWrapper;},
|
||||
postalAddress);
|
||||
}
|
||||
|
||||
public void putPostalAddress(Map<String, String> newPostalAddress) {
|
||||
getPostalAddress().assign(newPostalAddress);
|
||||
}
|
||||
|
||||
public PatchableMapWrapper<String> getEmailAddresses() {
|
||||
return PatchableMapWrapper.of(
|
||||
emailAddressesWrapper,
|
||||
|
@ -1,7 +1,6 @@
|
||||
package net.hostsharing.hsadminng.hs.office.contact;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeContactsApi;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactInsertResource;
|
||||
@ -17,7 +16,6 @@ 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
|
||||
@ -28,25 +26,20 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private StandardMapper mapper;
|
||||
private Mapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeContactRbacRepository contactRepo;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.contacts.api.getListOfContacts")
|
||||
public ResponseEntity<List<HsOfficeContactResource>> getListOfContacts(
|
||||
public ResponseEntity<List<HsOfficeContactResource>> listContacts(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final String caption,
|
||||
final String emailAddress) {
|
||||
final String caption) {
|
||||
context.define(currentSubject, assumedRoles);
|
||||
|
||||
validate("caption, emailAddress").atMaxOne(caption, emailAddress);
|
||||
final var entities = emailAddress != null
|
||||
? contactRepo.findContactByEmailAddress(emailAddress)
|
||||
: contactRepo.findContactByOptionalCaptionLike(caption);
|
||||
final var entities = contactRepo.findContactByOptionalCaptionLike(caption);
|
||||
|
||||
final var resources = mapper.mapList(entities, HsOfficeContactResource.class);
|
||||
return ResponseEntity.ok(resources);
|
||||
@ -54,8 +47,7 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.contacts.api.postNewContact")
|
||||
public ResponseEntity<HsOfficeContactResource> postNewContact(
|
||||
public ResponseEntity<HsOfficeContactResource> addContact(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final HsOfficeContactInsertResource body) {
|
||||
@ -77,8 +69,7 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.contacts.api.getSingleContactByUuid")
|
||||
public ResponseEntity<HsOfficeContactResource> getSingleContactByUuid(
|
||||
public ResponseEntity<HsOfficeContactResource> getContactByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID contactUuid) {
|
||||
@ -94,7 +85,6 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.contacts.api.deleteContactByUuid")
|
||||
public ResponseEntity<Void> deleteContactByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
@ -111,7 +101,6 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.contacts.api.patchContact")
|
||||
public ResponseEntity<HsOfficeContactResource> patchContact(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
|
@ -18,8 +18,7 @@ class HsOfficeContactEntityPatcher implements EntityPatcher<HsOfficeContactPatch
|
||||
@Override
|
||||
public void apply(final HsOfficeContactPatchResource resource) {
|
||||
OptionalFromJson.of(resource.getCaption()).ifPresent(entity::setCaption);
|
||||
Optional.ofNullable(resource.getPostalAddress())
|
||||
.ifPresent(r -> entity.getPostalAddress().patch(KeyValueMap.from(resource.getPostalAddress())));
|
||||
OptionalFromJson.of(resource.getPostalAddress()).ifPresent(entity::setPostalAddress);
|
||||
Optional.ofNullable(resource.getEmailAddresses())
|
||||
.ifPresent(r -> entity.getEmailAddresses().patch(KeyValueMap.from(resource.getEmailAddresses())));
|
||||
Optional.ofNullable(resource.getPhoneNumbers())
|
||||
|
@ -16,7 +16,7 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_office", name = "contact_rv")
|
||||
@Table(name = "hs_office_contact_rv")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.office.contact;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
@ -10,33 +9,18 @@ import java.util.UUID;
|
||||
|
||||
public interface HsOfficeContactRbacRepository extends Repository<HsOfficeContactRbacEntity, UUID> {
|
||||
|
||||
@Timed("app.office.contacts.repo.findByUuid.rbac")
|
||||
Optional<HsOfficeContactRbacEntity> findByUuid(UUID id);
|
||||
|
||||
@Query("""
|
||||
SELECT c FROM HsOfficeContactRbacEntity c
|
||||
WHERE :caption is null
|
||||
OR c.caption like concat(cast(:caption as text), '%')
|
||||
""")
|
||||
@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);
|
||||
|
||||
@Timed("app.office.contacts.repo.deleteByUuid.rbac")
|
||||
int deleteByUuid(final UUID uuid);
|
||||
|
||||
@Timed("app.office.contacts.repo.count.rbac")
|
||||
long count();
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_office", name = "contact")
|
||||
@Table(name = "hs_office_contact")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.office.contact;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
@ -10,23 +9,18 @@ import java.util.UUID;
|
||||
|
||||
public interface HsOfficeContactRealRepository extends Repository<HsOfficeContactRealEntity, UUID> {
|
||||
|
||||
@Timed("app.office.contacts.repo.findByUuid.real")
|
||||
Optional<HsOfficeContactRealEntity> findByUuid(UUID id);
|
||||
|
||||
@Query("""
|
||||
SELECT c FROM HsOfficeContactRealEntity c
|
||||
WHERE :caption is null
|
||||
OR c.caption like concat(cast(:caption as text), '%')
|
||||
""")
|
||||
@Timed("app.office.contacts.repo.findContactByOptionalCaptionLike.real")
|
||||
""")
|
||||
List<HsOfficeContactRealEntity> findContactByOptionalCaptionLike(String caption);
|
||||
|
||||
@Timed("app.office.contacts.repo.save.real")
|
||||
HsOfficeContactRealEntity save(final HsOfficeContactRealEntity entity);
|
||||
|
||||
@Timed("app.office.contacts.repo.deleteByUuid.real")
|
||||
int deleteByUuid(final UUID uuid);
|
||||
|
||||
@Timed("app.office.contacts.repo.count.real")
|
||||
long count();
|
||||
}
|
||||
|
@ -1,16 +1,10 @@
|
||||
package net.hostsharing.hsadminng.hs.office.coopassets;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.errors.MultiValidationException;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopAssetsApi;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource;
|
||||
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipRepository;
|
||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*;
|
||||
import net.hostsharing.hsadminng.errors.MultiValidationException;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.format.annotation.DateTimeFormat.ISO;
|
||||
@ -20,21 +14,13 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
|
||||
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import jakarta.validation.ValidationException;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionType.REVERSAL;
|
||||
import static net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionType.TRANSFER;
|
||||
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.CLEARING;
|
||||
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.DEPOSIT;
|
||||
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.DISBURSAL;
|
||||
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.LOSS;
|
||||
import static net.hostsharing.hsadminng.lambda.WithNonNull.withNonNull;
|
||||
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.*;
|
||||
|
||||
@RestController
|
||||
public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAssetsApi {
|
||||
@ -43,21 +29,14 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private StrictMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private EntityManagerWrapper emw;
|
||||
private Mapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeCoopAssetsTransactionRepository coopAssetsTransactionRepo;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeMembershipRepository membershipRepo;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.coopAssets.api.getListOfCoopAssets")
|
||||
public ResponseEntity<List<HsOfficeCoopAssetsTransactionResource>> getListOfCoopAssets(
|
||||
public ResponseEntity<List<HsOfficeCoopAssetsTransactionResource>> listCoopAssets(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID membershipUuid,
|
||||
@ -70,17 +49,13 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
||||
fromValueDate,
|
||||
toValueDate);
|
||||
|
||||
final var resources = mapper.mapList(
|
||||
entities,
|
||||
HsOfficeCoopAssetsTransactionResource.class,
|
||||
ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
final var resources = mapper.mapList(entities, HsOfficeCoopAssetsTransactionResource.class);
|
||||
return ResponseEntity.ok(resources);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.coopAssets.api.postNewCoopAssetTransaction")
|
||||
public ResponseEntity<HsOfficeCoopAssetsTransactionResource> postNewCoopAssetTransaction(
|
||||
public ResponseEntity<HsOfficeCoopAssetsTransactionResource> addCoopAssetsTransaction(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final HsOfficeCoopAssetsTransactionInsertResource requestBody) {
|
||||
@ -88,10 +63,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
||||
context.define(currentSubject, assumedRoles);
|
||||
validate(requestBody);
|
||||
|
||||
final var entityToSave = mapper.map(
|
||||
requestBody,
|
||||
HsOfficeCoopAssetsTransactionEntity.class,
|
||||
RESOURCE_TO_ENTITY_POSTMAPPER);
|
||||
final var entityToSave = mapper.map(requestBody, HsOfficeCoopAssetsTransactionEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
|
||||
final var saved = coopAssetsTransactionRepo.save(entityToSave);
|
||||
|
||||
final var uri =
|
||||
@ -99,15 +71,15 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
||||
.path("/api/hs/office/coopassetstransactions/{id}")
|
||||
.buildAndExpand(saved.getUuid())
|
||||
.toUri();
|
||||
final var mapped = mapper.map(saved, HsOfficeCoopAssetsTransactionResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
final var mapped = mapper.map(saved, HsOfficeCoopAssetsTransactionResource.class);
|
||||
return ResponseEntity.created(uri).body(mapped);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.coopAssets.api.getSingleCoopAssetTransactionByUuid")
|
||||
public ResponseEntity<HsOfficeCoopAssetsTransactionResource> getSingleCoopAssetTransactionByUuid(
|
||||
final String currentSubject, final String assumedRoles, final UUID assetTransactionUuid) {
|
||||
|
||||
public ResponseEntity<HsOfficeCoopAssetsTransactionResource> getCoopAssetTransactionByUuid(
|
||||
final String currentSubject, final String assumedRoles, final UUID assetTransactionUuid) {
|
||||
|
||||
context.define(currentSubject, assumedRoles);
|
||||
|
||||
@ -115,11 +87,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
||||
if (result.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
final var resource = mapper.map(
|
||||
result.get(),
|
||||
HsOfficeCoopAssetsTransactionResource.class,
|
||||
ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
return ResponseEntity.ok(resource);
|
||||
return ResponseEntity.ok(mapper.map(result.get(), HsOfficeCoopAssetsTransactionResource.class));
|
||||
|
||||
}
|
||||
|
||||
@ -134,7 +102,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
||||
private static void validateDebitTransaction(
|
||||
final HsOfficeCoopAssetsTransactionInsertResource requestBody,
|
||||
final ArrayList<String> violations) {
|
||||
if (List.of(DEPOSIT, HsOfficeCoopAssetsTransactionTypeResource.ADOPTION).contains(requestBody.getTransactionType())
|
||||
if (List.of(DEPOSIT, ADOPTION).contains(requestBody.getTransactionType())
|
||||
&& requestBody.getAssetValue().signum() < 0) {
|
||||
violations.add("for %s, assetValue must be positive but is \"%.2f\"".formatted(
|
||||
requestBody.getTransactionType(), requestBody.getAssetValue()));
|
||||
@ -144,8 +112,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
||||
private static void validateCreditTransaction(
|
||||
final HsOfficeCoopAssetsTransactionInsertResource requestBody,
|
||||
final ArrayList<String> violations) {
|
||||
if (List.of(DISBURSAL, HsOfficeCoopAssetsTransactionTypeResource.TRANSFER, CLEARING, LOSS)
|
||||
.contains(requestBody.getTransactionType())
|
||||
if (List.of(DISBURSAL, TRANSFER, CLEARING, LOSS).contains(requestBody.getTransactionType())
|
||||
&& requestBody.getAssetValue().signum() > 0) {
|
||||
violations.add("for %s, assetValue must be negative but is \"%.2f\"".formatted(
|
||||
requestBody.getTransactionType(), requestBody.getAssetValue()));
|
||||
@ -161,157 +128,10 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
||||
}
|
||||
}
|
||||
|
||||
// TODO.refa: this logic needs to get extracted to a service
|
||||
final BiConsumer<HsOfficeCoopAssetsTransactionEntity, HsOfficeCoopAssetsTransactionResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
||||
resource.setMembershipUuid(entity.getMembership().getUuid());
|
||||
resource.setMembershipMemberNumber(entity.getMembership().getTaggedMemberNumber());
|
||||
|
||||
withNonNull(
|
||||
resource.getReversalAssetTx(), reversalAssetTxResource -> {
|
||||
reversalAssetTxResource.setMembershipUuid(entity.getMembership().getUuid());
|
||||
reversalAssetTxResource.setMembershipMemberNumber(entity.getTaggedMemberNumber());
|
||||
reversalAssetTxResource.setRevertedAssetTxUuid(entity.getUuid());
|
||||
withNonNull(
|
||||
entity.getAdoptionAssetTx(), adoptionAssetTx ->
|
||||
reversalAssetTxResource.setAdoptionAssetTxUuid(adoptionAssetTx.getUuid()));
|
||||
withNonNull(
|
||||
entity.getTransferAssetTx(), transferAssetTxResource ->
|
||||
reversalAssetTxResource.setTransferAssetTxUuid(transferAssetTxResource.getUuid()));
|
||||
});
|
||||
|
||||
withNonNull(
|
||||
resource.getRevertedAssetTx(), revertAssetTxResource -> {
|
||||
revertAssetTxResource.setMembershipUuid(entity.getMembership().getUuid());
|
||||
revertAssetTxResource.setMembershipMemberNumber(entity.getTaggedMemberNumber());
|
||||
revertAssetTxResource.setReversalAssetTxUuid(entity.getUuid());
|
||||
withNonNull(
|
||||
entity.getRevertedAssetTx().getAdoptionAssetTx(), adoptionAssetTx ->
|
||||
revertAssetTxResource.setAdoptionAssetTxUuid(adoptionAssetTx.getUuid()));
|
||||
withNonNull(
|
||||
entity.getRevertedAssetTx().getTransferAssetTx(), transferAssetTxResource ->
|
||||
revertAssetTxResource.setTransferAssetTxUuid(transferAssetTxResource.getUuid()));
|
||||
});
|
||||
|
||||
withNonNull(
|
||||
resource.getAdoptionAssetTx(), adoptionAssetTxResource -> {
|
||||
adoptionAssetTxResource.setMembershipUuid(entity.getAdoptionAssetTx().getMembership().getUuid());
|
||||
adoptionAssetTxResource.setMembershipMemberNumber(entity.getAdoptionAssetTx().getTaggedMemberNumber());
|
||||
adoptionAssetTxResource.setTransferAssetTxUuid(entity.getUuid());
|
||||
withNonNull(
|
||||
entity.getAdoptionAssetTx().getReversalAssetTx(), reversalAssetTx ->
|
||||
adoptionAssetTxResource.setReversalAssetTxUuid(reversalAssetTx.getUuid()));
|
||||
});
|
||||
|
||||
withNonNull(
|
||||
resource.getTransferAssetTx(), transferAssetTxResource -> {
|
||||
resource.getTransferAssetTx().setMembershipUuid(entity.getTransferAssetTx().getMembership().getUuid());
|
||||
resource.getTransferAssetTx()
|
||||
.setMembershipMemberNumber(entity.getTransferAssetTx().getTaggedMemberNumber());
|
||||
resource.getTransferAssetTx().setAdoptionAssetTxUuid(entity.getUuid());
|
||||
withNonNull(
|
||||
entity.getTransferAssetTx().getReversalAssetTx(), reversalAssetTx ->
|
||||
transferAssetTxResource.setReversalAssetTxUuid(reversalAssetTx.getUuid()));
|
||||
});
|
||||
};
|
||||
|
||||
// TODO.refa: this logic needs to get extracted to a service
|
||||
final BiConsumer<HsOfficeCoopAssetsTransactionInsertResource, HsOfficeCoopAssetsTransactionEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
|
||||
|
||||
if (resource.getMembershipUuid() != null) {
|
||||
final HsOfficeMembershipEntity membership = ofNullable(emw.find(
|
||||
HsOfficeMembershipEntity.class,
|
||||
resource.getMembershipUuid()))
|
||||
.orElseThrow(() -> new EntityNotFoundException("membership.uuid %s not found".formatted(
|
||||
resource.getMembershipUuid())));
|
||||
entity.setMembership(membership);
|
||||
}
|
||||
|
||||
if (entity.getTransactionType() == REVERSAL) {
|
||||
if (resource.getRevertedAssetTxUuid() == null) {
|
||||
throw new ValidationException("REVERSAL asset transaction requires revertedAssetTx.uuid");
|
||||
}
|
||||
final var revertedAssetTx = coopAssetsTransactionRepo.findByUuid(resource.getRevertedAssetTxUuid())
|
||||
.orElseThrow(() -> new EntityNotFoundException("revertedAssetTx.uuid %s not found".formatted(
|
||||
resource.getRevertedAssetTxUuid())));
|
||||
revertedAssetTx.setReversalAssetTx(entity);
|
||||
entity.setRevertedAssetTx(revertedAssetTx);
|
||||
if (resource.getAssetValue().negate().compareTo(revertedAssetTx.getAssetValue()) != 0) {
|
||||
throw new ValidationException("given assetValue=" + resource.getAssetValue() +
|
||||
" but must be negative value from reverted asset tx: " + revertedAssetTx.getAssetValue());
|
||||
}
|
||||
|
||||
if (revertedAssetTx.getTransactionType() == TRANSFER) {
|
||||
final var adoptionAssetTx = revertedAssetTx.getAdoptionAssetTx();
|
||||
final var adoptionReversalAssetTx = HsOfficeCoopAssetsTransactionEntity.builder()
|
||||
.transactionType(REVERSAL)
|
||||
.membership(adoptionAssetTx.getMembership())
|
||||
.revertedAssetTx(adoptionAssetTx)
|
||||
.assetValue(adoptionAssetTx.getAssetValue().negate())
|
||||
.comment(resource.getComment())
|
||||
.reference(resource.getReference())
|
||||
.valueDate(resource.getValueDate())
|
||||
.build();
|
||||
adoptionAssetTx.setReversalAssetTx(adoptionReversalAssetTx);
|
||||
adoptionReversalAssetTx.setRevertedAssetTx(adoptionAssetTx);
|
||||
}
|
||||
}
|
||||
|
||||
if (resource.getTransactionType() == HsOfficeCoopAssetsTransactionTypeResource.TRANSFER) {
|
||||
final var adoptingMembership = determineAdoptingMembership(resource);
|
||||
if ( entity.getMembership() == adoptingMembership) {
|
||||
throw new ValidationException("transferring and adopting membership must be different, but both are " +
|
||||
adoptingMembership.getTaggedMemberNumber());
|
||||
}
|
||||
final var adoptingAssetTx = createAdoptingAssetTx(entity, adoptingMembership);
|
||||
entity.setAdoptionAssetTx(adoptingAssetTx);
|
||||
if ( resource.getReverseEntryUuid() != null ) {
|
||||
entity.setAdjustedAssetTx(coopAssetsTransactionRepo.findByUuid(resource.getReverseEntryUuid())
|
||||
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] reverseEntityUuid %s not found".formatted(resource.getReverseEntryUuid()))));
|
||||
}
|
||||
};
|
||||
|
||||
private HsOfficeMembershipEntity determineAdoptingMembership(final HsOfficeCoopAssetsTransactionInsertResource resource) {
|
||||
final var adoptingMembershipUuid = resource.getAdoptingMembershipUuid();
|
||||
final var adoptingMembershipMemberNumber = resource.getAdoptingMembershipMemberNumber();
|
||||
if (adoptingMembershipUuid != null && adoptingMembershipMemberNumber != null) {
|
||||
throw new ValidationException(
|
||||
// @formatter:off
|
||||
resource.getTransactionType() == HsOfficeCoopAssetsTransactionTypeResource.TRANSFER
|
||||
? "either adoptingMembership.uuid or adoptingMembership.memberNumber can be given, not both"
|
||||
: "adoptingMembership.uuid and adoptingMembership.memberNumber must not be given for transactionType="
|
||||
+ resource.getTransactionType());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
if (adoptingMembershipUuid != null) {
|
||||
final var adoptingMembership = membershipRepo.findByUuid(adoptingMembershipUuid);
|
||||
return adoptingMembership.orElseThrow(() ->
|
||||
new ValidationException(
|
||||
"adoptingMembership.uuid='" + adoptingMembershipUuid + "' not found or not accessible"));
|
||||
}
|
||||
|
||||
if (adoptingMembershipMemberNumber != null) {
|
||||
final var adoptingMemberNumber = Integer.valueOf(adoptingMembershipMemberNumber.substring("M-".length()));
|
||||
final var adoptingMembership = membershipRepo.findMembershipByMemberNumber(adoptingMemberNumber);
|
||||
return adoptingMembership.orElseThrow( () ->
|
||||
new ValidationException("adoptingMembership.memberNumber='" + adoptingMembershipMemberNumber
|
||||
+ "' not found or not accessible")
|
||||
);
|
||||
}
|
||||
|
||||
throw new ValidationException(
|
||||
"either adoptingMembership.uuid or adoptingMembership.memberNumber must be given for transactionType="
|
||||
+ HsOfficeCoopAssetsTransactionTypeResource.TRANSFER);
|
||||
}
|
||||
|
||||
private HsOfficeCoopAssetsTransactionEntity createAdoptingAssetTx(
|
||||
final HsOfficeCoopAssetsTransactionEntity transferAssetTxEntity,
|
||||
final HsOfficeMembershipEntity adoptingMembership) {
|
||||
return HsOfficeCoopAssetsTransactionEntity.builder()
|
||||
.membership(adoptingMembership)
|
||||
.transactionType(HsOfficeCoopAssetsTransactionType.ADOPTION)
|
||||
.transferAssetTx(transferAssetTxEntity)
|
||||
.assetValue(transferAssetTxEntity.getAssetValue().negate())
|
||||
.comment(transferAssetTxEntity.getComment())
|
||||
.reference(transferAssetTxEntity.getReference())
|
||||
.valueDate(transferAssetTxEntity.getValueDate())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -8,10 +8,10 @@ import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.repr.Stringify;
|
||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.hibernate.annotations.GenericGenerator;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
@ -31,10 +31,10 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.AGENT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_office", name = "coopassettx_rv")
|
||||
@Table(name = "hs_office_coopassetstransaction_rv")
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@ -50,10 +50,8 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, BaseE
|
||||
.withProp(HsOfficeCoopAssetsTransactionEntity::getAssetValue)
|
||||
.withProp(HsOfficeCoopAssetsTransactionEntity::getReference)
|
||||
.withProp(HsOfficeCoopAssetsTransactionEntity::getComment)
|
||||
.withProp(HsOfficeCoopAssetsTransactionEntity::getRevertedAssetTx)
|
||||
.withProp(HsOfficeCoopAssetsTransactionEntity::getReversalAssetTx)
|
||||
.withProp(HsOfficeCoopAssetsTransactionEntity::getAdoptionAssetTx)
|
||||
.withProp(HsOfficeCoopAssetsTransactionEntity::getTransferAssetTx)
|
||||
.withProp(at -> ofNullable(at.getAdjustedAssetTx()).map(HsOfficeCoopAssetsTransactionEntity::toShortString).orElse(null))
|
||||
.withProp(at -> ofNullable(at.getAdjustmentAssetTx()).map(HsOfficeCoopAssetsTransactionEntity::toShortString).orElse(null))
|
||||
.quotedValues(false);
|
||||
|
||||
@Id
|
||||
@ -79,7 +77,7 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, BaseE
|
||||
* The signed value which directly affects the booking balance.
|
||||
*
|
||||
* <p>This means, that a DEPOSIT is always positive, a DISBURSAL is always negative,
|
||||
* but an REVERSAL can bei either positive or negative.
|
||||
* but an ADJUSTMENT can bei either positive or negative.
|
||||
* See {@link HsOfficeCoopAssetsTransactionType} for</p> more information.
|
||||
*/
|
||||
@Column(name = "assetvalue")
|
||||
@ -97,23 +95,15 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, BaseE
|
||||
@Column(name = "comment")
|
||||
private String comment;
|
||||
|
||||
// Optionally, the UUID of the corresponding transaction for a reversal transaction.
|
||||
/**
|
||||
* Optionally, the UUID of the corresponding transaction for an adjustment transaction.
|
||||
*/
|
||||
@OneToOne
|
||||
@JoinColumn(name = "revertedassettxuuid")
|
||||
private HsOfficeCoopAssetsTransactionEntity revertedAssetTx;
|
||||
@JoinColumn(name = "adjustedassettxuuid")
|
||||
private HsOfficeCoopAssetsTransactionEntity adjustedAssetTx;
|
||||
|
||||
// and the other way around
|
||||
@OneToOne(mappedBy = "revertedAssetTx", cascade = CascadeType.PERSIST)
|
||||
private HsOfficeCoopAssetsTransactionEntity reversalAssetTx;
|
||||
|
||||
// Optionally, the UUID of the corresponding transaction for a transfer transaction.
|
||||
@OneToOne(cascade = CascadeType.PERSIST)
|
||||
@JoinColumn(name = "assetadoptiontxuuid")
|
||||
private HsOfficeCoopAssetsTransactionEntity adoptionAssetTx;
|
||||
|
||||
// and the other way around
|
||||
@OneToOne(mappedBy = "adoptionAssetTx", cascade = CascadeType.PERSIST)
|
||||
private HsOfficeCoopAssetsTransactionEntity transferAssetTx;
|
||||
@OneToOne(mappedBy = "adjustedAssetTx")
|
||||
private HsOfficeCoopAssetsTransactionEntity adjustmentAssetTx;
|
||||
|
||||
@Override
|
||||
public HsOfficeCoopAssetsTransactionEntity load() {
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.office.coopassets;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
@ -11,7 +10,6 @@ import java.util.UUID;
|
||||
|
||||
public interface HsOfficeCoopAssetsTransactionRepository extends Repository<HsOfficeCoopAssetsTransactionEntity, UUID> {
|
||||
|
||||
@Timed("app.office.coopAssets.repo.findByUuid")
|
||||
Optional<HsOfficeCoopAssetsTransactionEntity> findByUuid(UUID id);
|
||||
|
||||
@Query("""
|
||||
@ -20,14 +18,11 @@ public interface HsOfficeCoopAssetsTransactionRepository extends Repository<HsOf
|
||||
AND ( CAST(:fromValueDate AS java.time.LocalDate) IS NULL OR (at.valueDate >= :fromValueDate))
|
||||
AND ( CAST(:toValueDate AS java.time.LocalDate)IS NULL OR (at.valueDate <= :toValueDate))
|
||||
ORDER BY at.membership.memberNumberSuffix, at.valueDate
|
||||
""")
|
||||
@Timed("app.office.coopAssets.repo.findCoopAssetsTransactionByOptionalMembershipUuidAndDateRange")
|
||||
""")
|
||||
List<HsOfficeCoopAssetsTransactionEntity> findCoopAssetsTransactionByOptionalMembershipUuidAndDateRange(
|
||||
UUID membershipUuid, LocalDate fromValueDate, LocalDate toValueDate);
|
||||
|
||||
@Timed("app.office.coopAssets.repo.save")
|
||||
HsOfficeCoopAssetsTransactionEntity save(final HsOfficeCoopAssetsTransactionEntity entity);
|
||||
|
||||
@Timed("app.office.coopAssets.repo.count")
|
||||
long count();
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ public enum HsOfficeCoopAssetsTransactionType {
|
||||
/**
|
||||
* correction of wrong bookings, value can be positive or negative
|
||||
*/
|
||||
REVERSAL,
|
||||
ADJUSTMENT,
|
||||
|
||||
/**
|
||||
* payment received from member after signing shares, value >0
|
||||
|
@ -1,14 +1,12 @@
|
||||
package net.hostsharing.hsadminng.hs.office.coopshares;
|
||||
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopSharesApi;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionResource;
|
||||
import net.hostsharing.hsadminng.errors.MultiValidationException;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.format.annotation.DateTimeFormat.ISO;
|
||||
@ -33,16 +31,14 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private StandardMapper mapper;
|
||||
private Mapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeCoopSharesTransactionRepository coopSharesTransactionRepo;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
|
||||
@Timed("app.office.coopShares.api.getListOfCoopShares")
|
||||
public ResponseEntity<List<HsOfficeCoopSharesTransactionResource>> getListOfCoopShares(
|
||||
public ResponseEntity<List<HsOfficeCoopSharesTransactionResource>> listCoopShares(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID membershipUuid,
|
||||
@ -61,8 +57,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.coopShares.repo.postNewCoopSharesTransaction")
|
||||
public ResponseEntity<HsOfficeCoopSharesTransactionResource> postNewCoopSharesTransaction(
|
||||
public ResponseEntity<HsOfficeCoopSharesTransactionResource> addCoopSharesTransaction(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final HsOfficeCoopSharesTransactionInsertResource requestBody) {
|
||||
@ -85,8 +80,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.coopShares.repo.getSingleCoopShareTransactionByUuid")
|
||||
public ResponseEntity<HsOfficeCoopSharesTransactionResource> getSingleCoopShareTransactionByUuid(
|
||||
public ResponseEntity<HsOfficeCoopSharesTransactionResource> getCoopShareTransactionByUuid(
|
||||
final String currentSubject, final String assumedRoles, final UUID shareTransactionUuid) {
|
||||
|
||||
context.define(currentSubject, assumedRoles);
|
||||
@ -137,9 +131,9 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
|
||||
}
|
||||
|
||||
final BiConsumer<HsOfficeCoopSharesTransactionInsertResource, HsOfficeCoopSharesTransactionEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
|
||||
if ( resource.getRevertedShareTxUuid() != null ) {
|
||||
entity.setRevertedShareTx(coopSharesTransactionRepo.findByUuid(resource.getRevertedShareTxUuid())
|
||||
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] revertedShareTxUuid %s not found".formatted(resource.getRevertedShareTxUuid()))));
|
||||
if ( resource.getAdjustedShareTxUuid() != null ) {
|
||||
entity.setAdjustedShareTx(coopSharesTransactionRepo.findByUuid(resource.getAdjustedShareTxUuid())
|
||||
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] adjustedShareTxUuid %s not found".formatted(resource.getAdjustedShareTxUuid()))));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -8,10 +8,10 @@ import lombok.Setter;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.repr.Stringify;
|
||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
@ -29,10 +29,10 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.AGENT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_office", name = "coopsharetx_rv")
|
||||
@Table(name = "hs_office_coopsharestransaction_rv")
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@ -48,8 +48,8 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, BaseE
|
||||
.withProp(HsOfficeCoopSharesTransactionEntity::getShareCount)
|
||||
.withProp(HsOfficeCoopSharesTransactionEntity::getReference)
|
||||
.withProp(HsOfficeCoopSharesTransactionEntity::getComment)
|
||||
.withProp(at -> ofNullable(at.getRevertedShareTx()).map(HsOfficeCoopSharesTransactionEntity::toShortString).orElse(null))
|
||||
.withProp(at -> ofNullable(at.getReversalShareTx()).map(HsOfficeCoopSharesTransactionEntity::toShortString).orElse(null))
|
||||
.withProp(at -> ofNullable(at.getAdjustedShareTx()).map(HsOfficeCoopSharesTransactionEntity::toShortString).orElse(null))
|
||||
.withProp(at -> ofNullable(at.getAdjustmentShareTx()).map(HsOfficeCoopSharesTransactionEntity::toShortString).orElse(null))
|
||||
.quotedValues(false);
|
||||
|
||||
@Id
|
||||
@ -71,7 +71,7 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, BaseE
|
||||
* The signed value which directly affects the booking balance.
|
||||
*
|
||||
* <p>This means, that a SUBSCRIPTION is always positive, a CANCELLATION is always negative,
|
||||
* but an REVERSAL can bei either positive or negative.
|
||||
* but an ADJUSTMENT can bei either positive or negative.
|
||||
* See {@link HsOfficeCoopSharesTransactionType} for</p> more information.
|
||||
*/
|
||||
@Column(name = "valuedate")
|
||||
@ -93,14 +93,14 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, BaseE
|
||||
private String comment;
|
||||
|
||||
/**
|
||||
* Optionally, the UUID of the corresponding transaction for a REVERSAL transaction.
|
||||
* Optionally, the UUID of the corresponding transaction for an adjustment transaction.
|
||||
*/
|
||||
@OneToOne
|
||||
@JoinColumn(name = "revertedsharetxuuid")
|
||||
private HsOfficeCoopSharesTransactionEntity revertedShareTx;
|
||||
@JoinColumn(name = "adjustedsharetxuuid")
|
||||
private HsOfficeCoopSharesTransactionEntity adjustedShareTx;
|
||||
|
||||
@OneToOne(mappedBy = "revertedShareTx")
|
||||
private HsOfficeCoopSharesTransactionEntity reversalShareTx;
|
||||
@OneToOne(mappedBy = "adjustedShareTx")
|
||||
private HsOfficeCoopSharesTransactionEntity adjustmentShareTx;
|
||||
|
||||
@Override
|
||||
public HsOfficeCoopSharesTransactionEntity load() {
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.office.coopshares;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
@ -11,7 +10,6 @@ import java.util.UUID;
|
||||
|
||||
public interface HsOfficeCoopSharesTransactionRepository extends Repository<HsOfficeCoopSharesTransactionEntity, UUID> {
|
||||
|
||||
@Timed("app.office.coopShares.repo.findByUuid")
|
||||
Optional<HsOfficeCoopSharesTransactionEntity> findByUuid(UUID id);
|
||||
|
||||
@Query("""
|
||||
@ -20,14 +18,11 @@ public interface HsOfficeCoopSharesTransactionRepository extends Repository<HsOf
|
||||
AND ( CAST(:fromValueDate AS java.time.LocalDate) IS NULL OR (st.valueDate >= :fromValueDate))
|
||||
AND ( CAST(:toValueDate AS java.time.LocalDate)IS NULL OR (st.valueDate <= :toValueDate))
|
||||
ORDER BY st.membership.memberNumberSuffix, st.valueDate
|
||||
""")
|
||||
@Timed("app.office.coopShares.repo.findCoopSharesTransactionByOptionalMembershipUuidAndDateRange")
|
||||
""")
|
||||
List<HsOfficeCoopSharesTransactionEntity> findCoopSharesTransactionByOptionalMembershipUuidAndDateRange(
|
||||
UUID membershipUuid, LocalDate fromValueDate, LocalDate toValueDate);
|
||||
|
||||
@Timed("app.office.coopShares.repo.save")
|
||||
HsOfficeCoopSharesTransactionEntity save(final HsOfficeCoopSharesTransactionEntity entity);
|
||||
|
||||
@Timed("app.office.coopShares.repo.count")
|
||||
long count();
|
||||
}
|
||||
|
@ -2,9 +2,9 @@ package net.hostsharing.hsadminng.hs.office.coopshares;
|
||||
|
||||
public enum HsOfficeCoopSharesTransactionType {
|
||||
/**
|
||||
* reversal of wrong bookings, with either positive or negative value identical to reversed transaction
|
||||
* correction of wrong bookings, with either positive or negative value
|
||||
*/
|
||||
REVERSAL,
|
||||
ADJUSTMENT,
|
||||
|
||||
/**
|
||||
* shares signed, e.g. with the declaration of accession, value >0
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.office.debitor;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeDebitorsApi;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorInsertResource;
|
||||
@ -8,8 +7,8 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebito
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorResource;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityExistsValidator;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.hibernate.Hibernate;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@ -23,10 +22,9 @@ import jakarta.persistence.PersistenceContext;
|
||||
import jakarta.validation.ValidationException;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import static net.hostsharing.hsadminng.errors.DisplayAs.DisplayName;
|
||||
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR;
|
||||
import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag;
|
||||
|
||||
@RestController
|
||||
|
||||
@ -36,7 +34,7 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private StandardMapper mapper;
|
||||
private Mapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeDebitorRepository debitorRepo;
|
||||
@ -44,37 +42,29 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
||||
@Autowired
|
||||
private HsOfficeRelationRealRepository relrealRepo;
|
||||
|
||||
@Autowired
|
||||
private EntityExistsValidator entityValidator;
|
||||
|
||||
@PersistenceContext
|
||||
private EntityManager em;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.debitors.api.getListOfDebitors")
|
||||
public ResponseEntity<List<HsOfficeDebitorResource>> getListOfDebitors(
|
||||
public ResponseEntity<List<HsOfficeDebitorResource>> listDebitors(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final String name,
|
||||
final UUID partnerUuid,
|
||||
final String partnerNumber) {
|
||||
final Integer debitorNumber) {
|
||||
context.define(currentSubject, assumedRoles);
|
||||
|
||||
final var entities = partnerNumber != null
|
||||
? debitorRepo.findDebitorsByPartnerNumber(cropTag("P-", partnerNumber))
|
||||
: partnerUuid != null
|
||||
? debitorRepo.findDebitorsByPartnerUuid(partnerUuid)
|
||||
: debitorRepo.findDebitorsByOptionalNameLike(name);
|
||||
final var entities = debitorNumber != null
|
||||
? debitorRepo.findDebitorByDebitorNumber(debitorNumber)
|
||||
: debitorRepo.findDebitorByOptionalNameLike(name);
|
||||
|
||||
final var resources = mapper.mapList(entities, HsOfficeDebitorResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
final var resources = mapper.mapList(entities, HsOfficeDebitorResource.class);
|
||||
return ResponseEntity.ok(resources);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.debitors.api.postNewDebitor")
|
||||
public ResponseEntity<HsOfficeDebitorResource> postNewDebitor(
|
||||
public ResponseEntity<HsOfficeDebitorResource> addDebitor(
|
||||
String currentSubject,
|
||||
String assumedRoles,
|
||||
HsOfficeDebitorInsertResource body) {
|
||||
@ -85,25 +75,25 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
||||
"ERROR: [400] exactly one of debitorRel and debitorRelUuid must be supplied, but found both");
|
||||
Validate.isTrue(body.getDebitorRel() != null || body.getDebitorRelUuid() != null,
|
||||
"ERROR: [400] exactly one of debitorRel and debitorRelUuid must be supplied, but found none");
|
||||
Validate.isTrue(body.getDebitorRel() == null ||
|
||||
body.getDebitorRel().getType() == null || DEBITOR.name().equals(body.getDebitorRel().getType()),
|
||||
"ERROR: [400] debitorRel.type must be '"+DEBITOR.name()+"' or null for default");
|
||||
Validate.isTrue(body.getDebitorRel() == null || body.getDebitorRel().getMark() == null,
|
||||
"ERROR: [400] debitorRel.mark must be null");
|
||||
|
||||
final var entityToSave = mapper.map(body, HsOfficeDebitorEntity.class);
|
||||
if (body.getDebitorRel() != null) {
|
||||
final var debitorRel = mapper.map("debitorRel.", body.getDebitorRel(), HsOfficeRelationRealEntity.class);
|
||||
debitorRel.setType(DEBITOR);
|
||||
entityValidator.validateEntityExists("debitorRel.anchorUuid", debitorRel.getAnchor());
|
||||
entityValidator.validateEntityExists("debitorRel.holderUuid", debitorRel.getHolder());
|
||||
entityValidator.validateEntityExists("debitorRel.contactUuid", debitorRel.getContact());
|
||||
if ( body.getDebitorRel() != null ) {
|
||||
body.getDebitorRel().setType(DEBITOR.name());
|
||||
final var debitorRel = mapper.map(body.getDebitorRel(), HsOfficeRelationRealEntity.class);
|
||||
validateEntityExists("debitorRel.anchorUuid", debitorRel.getAnchor());
|
||||
validateEntityExists("debitorRel.holderUuid", debitorRel.getHolder());
|
||||
validateEntityExists("debitorRel.contactUuid", debitorRel.getContact());
|
||||
entityToSave.setDebitorRel(relrealRepo.save(debitorRel));
|
||||
} else {
|
||||
final var debitorRelOptional = relrealRepo.findByUuid(body.getDebitorRelUuid());
|
||||
debitorRelOptional.ifPresentOrElse(
|
||||
debitorRel -> {entityToSave.setDebitorRel(relrealRepo.save(debitorRel));},
|
||||
() -> {
|
||||
throw new ValidationException(
|
||||
"Unable to find RealRelation by debitorRelUuid: " + body.getDebitorRelUuid());
|
||||
});
|
||||
() -> { throw new ValidationException("Unable to find RealRelation by debitorRelUuid: " + body.getDebitorRelUuid());});
|
||||
}
|
||||
|
||||
final var savedEntity = debitorRepo.save(entityToSave);
|
||||
@ -115,14 +105,13 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
||||
.path("/api/hs/office/debitors/{id}")
|
||||
.buildAndExpand(savedEntity.getUuid())
|
||||
.toUri();
|
||||
final var mapped = mapper.map(savedEntity, HsOfficeDebitorResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
final var mapped = mapper.map(savedEntity, HsOfficeDebitorResource.class);
|
||||
return ResponseEntity.created(uri).body(mapped);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.debitors.api.getSingleDebitorByUuid")
|
||||
public ResponseEntity<HsOfficeDebitorResource> getSingleDebitorByUuid(
|
||||
public ResponseEntity<HsOfficeDebitorResource> getDebitorByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID debitorUuid) {
|
||||
@ -133,29 +122,11 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
||||
if (result.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(mapper.map(result.get(), HsOfficeDebitorResource.class, ENTITY_TO_RESOURCE_POSTMAPPER));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.debitors.api.getSingleDebitorByDebitorNumber")
|
||||
public ResponseEntity<HsOfficeDebitorResource> getSingleDebitorByDebitorNumber(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final Integer debitorNumber) {
|
||||
|
||||
context.define(currentSubject, assumedRoles);
|
||||
|
||||
final var result = debitorRepo.findDebitorByDebitorNumber(debitorNumber);
|
||||
if (result.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(mapper.map(result.get(), HsOfficeDebitorResource.class, ENTITY_TO_RESOURCE_POSTMAPPER));
|
||||
return ResponseEntity.ok(mapper.map(result.get(), HsOfficeDebitorResource.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.debitors.api.deleteDebitorByUuid")
|
||||
public ResponseEntity<Void> deleteDebitorByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
@ -172,7 +143,6 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.debitors.api.patchDebitor")
|
||||
public ResponseEntity<HsOfficeDebitorResource> patchDebitor(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
@ -187,11 +157,18 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
||||
|
||||
final var saved = debitorRepo.save(current);
|
||||
Hibernate.initialize(saved);
|
||||
final var mapped = mapper.map(saved, HsOfficeDebitorResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
final var mapped = mapper.map(saved, HsOfficeDebitorResource.class);
|
||||
return ResponseEntity.ok(mapped);
|
||||
}
|
||||
|
||||
final BiConsumer<HsOfficeDebitorEntity, HsOfficeDebitorResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
||||
resource.setDebitorNumber(entity.getTaggedDebitorNumber());
|
||||
};
|
||||
// TODO.impl: extract this to some generally usable class?
|
||||
private <T extends BaseEntity<T>> T validateEntityExists(final String property, final T entitySkeleton) {
|
||||
final var foundEntity = em.find(entitySkeleton.getClass(), entitySkeleton.getUuid());
|
||||
if ( foundEntity == null) {
|
||||
throw new ValidationException("Unable to find " + DisplayName.of(entitySkeleton) + " by " + property + ": " + entitySkeleton.getUuid());
|
||||
}
|
||||
|
||||
//noinspection unchecked
|
||||
return (T) foundEntity;
|
||||
}
|
||||
}
|
||||
|
@ -11,11 +11,11 @@ import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelation;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.repr.Stringify;
|
||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.hibernate.annotations.GenericGenerator;
|
||||
import org.hibernate.annotations.JoinFormula;
|
||||
import org.hibernate.annotations.NotFound;
|
||||
@ -51,10 +51,10 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.fetchedBySql;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_office", name = "debitor_rv")
|
||||
@Table(name = "hs_office_debitor_rv")
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder(toBuilder = true)
|
||||
@ -87,10 +87,10 @@ public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>,
|
||||
value = """
|
||||
(
|
||||
SELECT DISTINCT partner.uuid
|
||||
FROM hs_office.partner_rv partner
|
||||
JOIN hs_office.relation_rv dRel
|
||||
FROM hs_office_partner_rv partner
|
||||
JOIN hs_office_relation_rv dRel
|
||||
ON dRel.uuid = debitorreluuid AND dRel.type = 'DEBITOR'
|
||||
JOIN hs_office.relation_rv pRel
|
||||
JOIN hs_office_relation_rv pRel
|
||||
ON pRel.uuid = partner.partnerRelUuid AND pRel.type = 'PARTNER'
|
||||
WHERE pRel.holderUuid = dRel.anchorUuid
|
||||
)
|
||||
@ -142,14 +142,19 @@ public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>,
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getTaggedDebitorNumber() {
|
||||
private String getDebitorNumberString() {
|
||||
return ofNullable(partner)
|
||||
.filter(partner -> debitorNumberSuffix != null)
|
||||
.map(HsOfficePartnerEntity::getPartnerNumber)
|
||||
.map(partnerNumber -> DEBITOR_NUMBER_TAG + partnerNumber + debitorNumberSuffix)
|
||||
.map(Object::toString)
|
||||
.map(partnerNumber -> partnerNumber + debitorNumberSuffix)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public Integer getDebitorNumber() {
|
||||
return ofNullable(getDebitorNumberString()).map(Integer::parseInt).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return stringify.apply(this);
|
||||
@ -157,7 +162,7 @@ public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>,
|
||||
|
||||
@Override
|
||||
public String toShortString() {
|
||||
return getTaggedDebitorNumber();
|
||||
return DEBITOR_NUMBER_TAG + getDebitorNumberString();
|
||||
}
|
||||
|
||||
public static RbacView rbac() {
|
||||
@ -165,14 +170,14 @@ public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>,
|
||||
.withIdentityView(SQL.query("""
|
||||
SELECT debitor.uuid AS uuid,
|
||||
'D-' || (SELECT partner.partnerNumber
|
||||
FROM hs_office.partner partner
|
||||
JOIN hs_office.relation partnerRel
|
||||
FROM hs_office_partner partner
|
||||
JOIN hs_office_relation partnerRel
|
||||
ON partnerRel.uuid = partner.partnerRelUUid AND partnerRel.type = 'PARTNER'
|
||||
JOIN hs_office.relation debitorRel
|
||||
JOIN hs_office_relation debitorRel
|
||||
ON debitorRel.anchorUuid = partnerRel.holderUuid AND debitorRel.type = 'DEBITOR'
|
||||
WHERE debitorRel.uuid = debitor.debitorRelUuid)
|
||||
|| debitorNumberSuffix as idName
|
||||
FROM hs_office.debitor AS debitor
|
||||
FROM hs_office_debitor AS debitor
|
||||
"""))
|
||||
.withRestrictedViewOrderBy(SQL.projection("defaultPrefix"))
|
||||
.withUpdatableColumns(
|
||||
@ -204,8 +209,8 @@ public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>,
|
||||
dependsOnColumn("debitorRelUuid"),
|
||||
fetchedBySql("""
|
||||
SELECT ${columns}
|
||||
FROM hs_office.relation AS partnerRel
|
||||
JOIN hs_office.relation AS debitorRel
|
||||
FROM hs_office_relation AS partnerRel
|
||||
JOIN hs_office_relation AS debitorRel
|
||||
ON debitorRel.type = 'DEBITOR' AND debitorRel.anchorUuid = partnerRel.holderUuid
|
||||
WHERE partnerRel.type = 'PARTNER'
|
||||
AND ${REF}.debitorRelUuid = debitorRel.uuid
|
||||
|
@ -1,7 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.office.debitor;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import net.hostsharing.hsadminng.lambda.Reducer;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
@ -11,33 +9,20 @@ import java.util.UUID;
|
||||
|
||||
public interface HsOfficeDebitorRepository extends Repository<HsOfficeDebitorEntity, UUID> {
|
||||
|
||||
@Timed("app.office.debitors.repo.findByUuid")
|
||||
Optional<HsOfficeDebitorEntity> findByUuid(UUID id);
|
||||
|
||||
@Timed("app.office.debitors.repo.findDebitorByPartnerUuid")
|
||||
List<HsOfficeDebitorEntity> findDebitorsByPartnerUuid(UUID partnerUuid);
|
||||
|
||||
@Query("""
|
||||
SELECT debitor FROM HsOfficeDebitorEntity debitor
|
||||
JOIN HsOfficePartnerEntity partner
|
||||
ON partner.partnerRel.holder = debitor.debitorRel.anchor
|
||||
AND partner.partnerRel.type = 'PARTNER' AND debitor.debitorRel.type = 'DEBITOR'
|
||||
WHERE partner.partnerNumber = :partnerNumber
|
||||
AND (:debitorNumberSuffix IS NULL OR debitor.debitorNumberSuffix = :debitorNumberSuffix)
|
||||
""")
|
||||
@Timed("app.office.debitors.repo.findDebitorByPartnerNumberAndDebitorNumberSuffix")
|
||||
List<HsOfficeDebitorEntity> findDebitorByPartnerNumberAndOptionalDebitorNumberSuffix(int partnerNumber, String debitorNumberSuffix);
|
||||
WHERE cast(partner.partnerNumber as integer) = :partnerNumber
|
||||
AND cast(debitor.debitorNumberSuffix as integer) = :debitorNumberSuffix
|
||||
""")
|
||||
List<HsOfficeDebitorEntity> findDebitorByDebitorNumber(int partnerNumber, byte debitorNumberSuffix);
|
||||
|
||||
default Optional<HsOfficeDebitorEntity> findDebitorByDebitorNumber(int debitorNumber) {
|
||||
final var partnerNumber = debitorNumber / 100;
|
||||
final String suffix = String.format("%02d", debitorNumber % 100);
|
||||
final var result = findDebitorByPartnerNumberAndOptionalDebitorNumberSuffix(partnerNumber, suffix);
|
||||
return result.stream().reduce(Reducer::toSingleElement);
|
||||
}
|
||||
|
||||
default List<HsOfficeDebitorEntity> findDebitorsByPartnerNumber(int partnerNumber) {
|
||||
final var result = findDebitorByPartnerNumberAndOptionalDebitorNumberSuffix(partnerNumber, null);
|
||||
return result;
|
||||
default List<HsOfficeDebitorEntity> findDebitorByDebitorNumber(int debitorNumber) {
|
||||
return findDebitorByDebitorNumber( debitorNumber/100, (byte) (debitorNumber%100));
|
||||
}
|
||||
|
||||
@Query("""
|
||||
@ -45,7 +30,7 @@ public interface HsOfficeDebitorRepository extends Repository<HsOfficeDebitorEnt
|
||||
JOIN HsOfficePartnerEntity partner
|
||||
ON partner.partnerRel.holder = debitor.debitorRel.anchor
|
||||
AND partner.partnerRel.type = 'PARTNER' AND debitor.debitorRel.type = 'DEBITOR'
|
||||
JOIN HsOfficePersonRealEntity person
|
||||
JOIN HsOfficePersonEntity person
|
||||
ON person.uuid = partner.partnerRel.holder.uuid
|
||||
OR person.uuid = debitor.debitorRel.holder.uuid
|
||||
JOIN HsOfficeContactRealEntity contact
|
||||
@ -58,15 +43,11 @@ public interface HsOfficeDebitorRepository extends Repository<HsOfficeDebitorEnt
|
||||
OR person.givenName like concat(cast(:name as text), '%')
|
||||
OR contact.caption like concat(cast(:name as text), '%')
|
||||
""")
|
||||
@Timed("app.office.debitors.repo.findDebitorByOptionalNameLike")
|
||||
List<HsOfficeDebitorEntity> findDebitorsByOptionalNameLike(String name);
|
||||
List<HsOfficeDebitorEntity> findDebitorByOptionalNameLike(String name);
|
||||
|
||||
@Timed("app.office.debitors.repo.save")
|
||||
HsOfficeDebitorEntity save(final HsOfficeDebitorEntity entity);
|
||||
|
||||
@Timed("app.office.debitors.repo.count")
|
||||
long count();
|
||||
|
||||
@Timed("app.office.debitors.repo.deleteByUuid")
|
||||
int deleteByUuid(UUID uuid);
|
||||
}
|
||||
|
@ -1,13 +1,11 @@
|
||||
package net.hostsharing.hsadminng.hs.office.membership;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeMembershipsApi;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipPatchResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipResource;
|
||||
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@ -18,50 +16,40 @@ 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.repr.TaggedNumber.cropTag;
|
||||
|
||||
@RestController
|
||||
|
||||
public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
|
||||
|
||||
@Autowired
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private StandardMapper mapper;
|
||||
private Mapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeMembershipRepository membershipRepo;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.membership.api.getListOfMemberships")
|
||||
public ResponseEntity<List<HsOfficeMembershipResource>> getListOfMemberships(
|
||||
public ResponseEntity<List<HsOfficeMembershipResource>> listMemberships(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID partnerUuid,
|
||||
final String partnerNumber) {
|
||||
UUID partnerUuid,
|
||||
Integer memberNumber) {
|
||||
context.define(currentSubject, assumedRoles);
|
||||
|
||||
validate("partnerUuid, partnerNumber").atMaxOne(partnerUuid, partnerNumber);
|
||||
final var entities = ( memberNumber != null)
|
||||
? List.of(membershipRepo.findMembershipByMemberNumber(memberNumber))
|
||||
: membershipRepo.findMembershipsByOptionalPartnerUuid(partnerUuid);
|
||||
|
||||
final var entities = partnerNumber != null
|
||||
? membershipRepo.findMembershipsByPartnerNumber(
|
||||
cropTag(HsOfficePartnerEntity.PARTNER_NUMBER_TAG, partnerNumber))
|
||||
: partnerUuid != null
|
||||
? membershipRepo.findMembershipsByPartnerUuid(partnerUuid)
|
||||
: membershipRepo.findAll();
|
||||
|
||||
final var resources = mapper.mapList(
|
||||
entities, HsOfficeMembershipResource.class,
|
||||
final var resources = mapper.mapList(entities, HsOfficeMembershipResource.class,
|
||||
SEPA_MANDATE_ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
return ResponseEntity.ok(resources);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.membership.api.postNewMembership")
|
||||
public ResponseEntity<HsOfficeMembershipResource> postNewMembership(
|
||||
public ResponseEntity<HsOfficeMembershipResource> addMembership(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final HsOfficeMembershipInsertResource body) {
|
||||
@ -77,16 +65,14 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
|
||||
.path("/api/hs/office/memberships/{id}")
|
||||
.buildAndExpand(saved.getUuid())
|
||||
.toUri();
|
||||
final var mapped = mapper.map(
|
||||
saved, HsOfficeMembershipResource.class,
|
||||
final var mapped = mapper.map(saved, HsOfficeMembershipResource.class,
|
||||
SEPA_MANDATE_ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
return ResponseEntity.created(uri).body(mapped);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.membership.api.getSingleMembershipByUuid")
|
||||
public ResponseEntity<HsOfficeMembershipResource> getSingleMembershipByUuid(
|
||||
public ResponseEntity<HsOfficeMembershipResource> getMembershipByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID membershipUuid) {
|
||||
@ -97,33 +83,12 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
|
||||
if (result.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(mapper.map(
|
||||
result.get(), HsOfficeMembershipResource.class,
|
||||
SEPA_MANDATE_ENTITY_TO_RESOURCE_POSTMAPPER));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.membership.api.getSingleMembershipByMembershipNumber")
|
||||
public ResponseEntity<HsOfficeMembershipResource> getSingleMembershipByMembershipNumber(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final Integer membershipNumber) {
|
||||
|
||||
context.define(currentSubject, assumedRoles);
|
||||
|
||||
final var result = membershipRepo.findMembershipByMemberNumber(membershipNumber);
|
||||
if (result.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(mapper.map(
|
||||
result.get(), HsOfficeMembershipResource.class,
|
||||
return ResponseEntity.ok(mapper.map(result.get(), HsOfficeMembershipResource.class,
|
||||
SEPA_MANDATE_ENTITY_TO_RESOURCE_POSTMAPPER));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.membership.api.deleteMembershipByUuid")
|
||||
public ResponseEntity<Void> deleteMembershipByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
@ -140,7 +105,6 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.membership.api.patchMembership")
|
||||
public ResponseEntity<HsOfficeMembershipResource> patchMembership(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
@ -159,7 +123,7 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
|
||||
}
|
||||
|
||||
final BiConsumer<HsOfficeMembershipEntity, HsOfficeMembershipResource> SEPA_MANDATE_ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
||||
resource.setMemberNumber(entity.getTaggedMemberNumber());
|
||||
// TODO.refa: this should be possible via ModelMapper config
|
||||
resource.setValidFrom(entity.getValidity().lower());
|
||||
if (entity.getValidity().hasUpperBound()) {
|
||||
resource.setValidTo(entity.getValidity().upper().minusDays(1));
|
||||
|
@ -9,12 +9,12 @@ import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.repr.Stringify;
|
||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.hibernate.annotations.Type;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
@ -53,10 +53,10 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.OWNER;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.TENANT;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.fetchedBySql;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_office", name = "membership_rv")
|
||||
@Table(name = "hs_office_membership_rv")
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@ -130,7 +130,6 @@ public class HsOfficeMembershipEntity implements BaseEntity<HsOfficeMembershipEn
|
||||
}
|
||||
return validity;
|
||||
}
|
||||
|
||||
public Integer getMemberNumber() {
|
||||
if (partner == null || partner.getPartnerNumber() == null || memberNumberSuffix == null ) {
|
||||
return null;
|
||||
@ -139,10 +138,6 @@ public class HsOfficeMembershipEntity implements BaseEntity<HsOfficeMembershipEn
|
||||
return getPartner().getPartnerNumber() * 100 + Integer.parseInt(memberNumberSuffix, 10);
|
||||
}
|
||||
|
||||
public String getTaggedMemberNumber() {
|
||||
return MEMBER_NUMBER_TAG + getMemberNumber();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return stringify.apply(this);
|
||||
@ -165,8 +160,8 @@ public class HsOfficeMembershipEntity implements BaseEntity<HsOfficeMembershipEn
|
||||
.withIdentityView(SQL.query("""
|
||||
SELECT m.uuid AS uuid,
|
||||
'M-' || p.partnerNumber || m.memberNumberSuffix as idName
|
||||
FROM hs_office.membership AS m
|
||||
JOIN hs_office.partner AS p ON p.uuid = m.partnerUuid
|
||||
FROM hs_office_membership AS m
|
||||
JOIN hs_office_partner AS p ON p.uuid = m.partnerUuid
|
||||
"""))
|
||||
.withRestrictedViewOrderBy(SQL.projection("validity"))
|
||||
.withUpdatableColumns("validity", "membershipFeeBillable", "status")
|
||||
@ -175,8 +170,8 @@ public class HsOfficeMembershipEntity implements BaseEntity<HsOfficeMembershipEn
|
||||
dependsOnColumn("partnerUuid"),
|
||||
fetchedBySql("""
|
||||
SELECT ${columns}
|
||||
FROM hs_office.partner AS partner
|
||||
JOIN hs_office.relation AS partnerRel ON partnerRel.uuid = partner.partnerRelUuid
|
||||
FROM hs_office_partner AS partner
|
||||
JOIN hs_office_relation AS partnerRel ON partnerRel.uuid = partner.partnerRelUuid
|
||||
WHERE partner.uuid = ${REF}.partnerUuid
|
||||
"""),
|
||||
NOT_NULL)
|
||||
|
@ -2,18 +2,18 @@ package net.hostsharing.hsadminng.hs.office.membership;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipPatchResource;
|
||||
import net.hostsharing.hsadminng.mapper.EntityPatcher;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class HsOfficeMembershipEntityPatcher implements EntityPatcher<HsOfficeMembershipPatchResource> {
|
||||
|
||||
private final StandardMapper mapper;
|
||||
private final Mapper mapper;
|
||||
private final HsOfficeMembershipEntity entity;
|
||||
|
||||
public HsOfficeMembershipEntityPatcher(
|
||||
final StandardMapper mapper,
|
||||
final Mapper mapper,
|
||||
final HsOfficeMembershipEntity entity) {
|
||||
this.mapper = mapper;
|
||||
this.entity = entity;
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.office.membership;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
@ -11,52 +10,34 @@ import java.util.UUID;
|
||||
|
||||
public interface HsOfficeMembershipRepository extends Repository<HsOfficeMembershipEntity, UUID> {
|
||||
|
||||
@Timed("app.office.membership.repo.findByUuid")
|
||||
Optional<HsOfficeMembershipEntity> findByUuid(UUID id);
|
||||
|
||||
@Timed("app.office.membership.repo.save")
|
||||
HsOfficeMembershipEntity save(final HsOfficeMembershipEntity entity);
|
||||
|
||||
@Timed("app.office.membership.repo.findAll")
|
||||
List<HsOfficeMembershipEntity> findAll();
|
||||
|
||||
@Query("""
|
||||
SELECT membership FROM HsOfficeMembershipEntity membership
|
||||
WHERE membership.partner.uuid = :partnerUuid
|
||||
WHERE ( CAST(:partnerUuid as org.hibernate.type.UUIDCharType) IS NULL
|
||||
OR membership.partner.uuid = :partnerUuid )
|
||||
ORDER BY membership.partner.partnerNumber, membership.memberNumberSuffix
|
||||
""")
|
||||
@Timed("app.office.membership.repo.findMembershipsByOptionalPartnerUuid")
|
||||
List<HsOfficeMembershipEntity> findMembershipsByPartnerUuid(UUID partnerUuid);
|
||||
|
||||
@Query("""
|
||||
SELECT membership FROM HsOfficeMembershipEntity membership
|
||||
WHERE membership.partner.partnerNumber = :partnerNumber
|
||||
ORDER BY membership.partner.partnerNumber, membership.memberNumberSuffix
|
||||
""")
|
||||
@Timed("app.office.membership.repo.findMembershipsByPartnerNumber")
|
||||
List<HsOfficeMembershipEntity> findMembershipsByPartnerNumber(Integer partnerNumber);
|
||||
|
||||
""")
|
||||
List<HsOfficeMembershipEntity> findMembershipsByOptionalPartnerUuid(UUID partnerUuid);
|
||||
@Query("""
|
||||
SELECT membership FROM HsOfficeMembershipEntity membership
|
||||
WHERE (:partnerNumber = membership.partner.partnerNumber)
|
||||
AND (membership.memberNumberSuffix = :suffix)
|
||||
ORDER BY membership.memberNumberSuffix
|
||||
""")
|
||||
@Timed("app.office.membership.repo.findMembershipByMemberNumber")
|
||||
Optional<HsOfficeMembershipEntity> findMembershipByPartnerNumberAndSuffix(
|
||||
HsOfficeMembershipEntity findMembershipByPartnerNumberAndSuffix(
|
||||
@NotNull Integer partnerNumber,
|
||||
@NotNull String suffix);
|
||||
|
||||
default Optional<HsOfficeMembershipEntity> findMembershipByMemberNumber(final Integer memberNumber) {
|
||||
default HsOfficeMembershipEntity findMembershipByMemberNumber(Integer memberNumber) {
|
||||
final var partnerNumber = memberNumber / 100;
|
||||
final String suffix = String.format("%02d", memberNumber % 100);
|
||||
final var result = findMembershipByPartnerNumberAndSuffix(partnerNumber, suffix);
|
||||
return result;
|
||||
final var suffix = memberNumber % 100;
|
||||
return findMembershipByPartnerNumberAndSuffix(partnerNumber, String.format("%02d", suffix));
|
||||
}
|
||||
|
||||
@Timed("app.office.membership.repo.count")
|
||||
long count();
|
||||
|
||||
@Timed("app.office.membership.repo.deleteByUuid")
|
||||
int deleteByUuid(UUID uuid);
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
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.HsOfficeContactRealEntity;
|
||||
@ -9,12 +8,12 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartne
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerPatchResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerRelInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@ -28,7 +27,6 @@ import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.EX_PARTNER;
|
||||
import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag;
|
||||
|
||||
@RestController
|
||||
|
||||
@ -38,7 +36,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private StandardMapper mapper;
|
||||
private Mapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficePartnerRepository partnerRepo;
|
||||
@ -51,8 +49,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.partners.api.getListOfPartners")
|
||||
public ResponseEntity<List<HsOfficePartnerResource>> getListOfPartners(
|
||||
public ResponseEntity<List<HsOfficePartnerResource>> listPartners(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final String name) {
|
||||
@ -66,8 +63,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.partners.api.postNewPartner")
|
||||
public ResponseEntity<HsOfficePartnerResource> postNewPartner(
|
||||
public ResponseEntity<HsOfficePartnerResource> addPartner(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final HsOfficePartnerInsertResource body) {
|
||||
@ -89,8 +85,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.partners.api.getSinglePartnerByUuid")
|
||||
public ResponseEntity<HsOfficePartnerResource> getSinglePartnerByUuid(
|
||||
public ResponseEntity<HsOfficePartnerResource> getPartnerByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID partnerUuid) {
|
||||
@ -104,26 +99,8 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
|
||||
return ResponseEntity.ok(mapper.map(result.get(), HsOfficePartnerResource.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.partners.api.getSinglePartnerByPartnerNumber")
|
||||
public ResponseEntity<HsOfficePartnerResource> getSinglePartnerByPartnerNumber(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final Integer partnerNumber) {
|
||||
|
||||
context.define(currentSubject, assumedRoles);
|
||||
|
||||
final var result = partnerRepo.findPartnerByPartnerNumber(partnerNumber);
|
||||
if (result.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(mapper.map(result.get(), HsOfficePartnerResource.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.partners.api.deletePartnerByUuid")
|
||||
public ResponseEntity<Void> deletePartnerByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
@ -144,7 +121,6 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.partners.api.patchPartner")
|
||||
public ResponseEntity<HsOfficePartnerResource> patchPartner(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
@ -167,14 +143,13 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
|
||||
|
||||
private void optionallyCreateExPartnerRelation(final HsOfficePartnerEntity 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 HsOfficePartnerEntity createPartnerEntity(final HsOfficePartnerInsertResource body) {
|
||||
final var entityToSave = new HsOfficePartnerEntity();
|
||||
entityToSave.setPartnerNumber(cropTag(HsOfficePartnerEntity.PARTNER_NUMBER_TAG, body.getPartnerNumber()));
|
||||
entityToSave.setPartnerNumber(body.getPartnerNumber());
|
||||
entityToSave.setPartnerRel(persistPartnerRel(body.getPartnerRel()));
|
||||
entityToSave.setDetails(mapper.map(body.getDetails(), HsOfficePartnerDetailsEntity.class));
|
||||
return entityToSave;
|
||||
@ -183,8 +158,8 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
|
||||
private HsOfficeRelationRealEntity persistPartnerRel(final HsOfficePartnerRelInsertResource resource) {
|
||||
final var entity = new HsOfficeRelationRealEntity();
|
||||
entity.setType(HsOfficeRelationType.PARTNER);
|
||||
entity.setAnchor(ref(HsOfficePersonRealEntity.class, resource.getAnchorUuid()));
|
||||
entity.setHolder(ref(HsOfficePersonRealEntity.class, resource.getHolderUuid()));
|
||||
entity.setAnchor(ref(HsOfficePersonEntity.class, resource.getAnchorUuid()));
|
||||
entity.setHolder(ref(HsOfficePersonEntity.class, resource.getHolderUuid()));
|
||||
entity.setContact(ref(HsOfficeContactRealEntity.class, resource.getContactUuid()));
|
||||
em.persist(entity);
|
||||
return entity;
|
||||
|
@ -2,11 +2,11 @@ package net.hostsharing.hsadminng.hs.office.partner;
|
||||
|
||||
import lombok.*;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.repr.Stringify;
|
||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
@ -17,10 +17,10 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_office", name = "partner_details_rv")
|
||||
@Table(name = "hs_office_partner_details_rv")
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@ -71,9 +71,9 @@ public class HsOfficePartnerDetailsEntity implements BaseEntity<HsOfficePartnerD
|
||||
return rbacViewFor("partnerDetails", HsOfficePartnerDetailsEntity.class)
|
||||
.withIdentityView(SQL.query("""
|
||||
SELECT partnerDetails.uuid as uuid, partner_iv.idName as idName
|
||||
FROM hs_office.partner_details AS partnerDetails
|
||||
JOIN hs_office.partner partner ON partner.detailsUuid = partnerDetails.uuid
|
||||
JOIN hs_office.partner_iv partner_iv ON partner_iv.uuid = partner.uuid
|
||||
FROM hs_office_partner_details AS partnerDetails
|
||||
JOIN hs_office_partner partner ON partner.detailsUuid = partnerDetails.uuid
|
||||
JOIN hs_office_partner_iv partner_iv ON partner_iv.uuid = partner.uuid
|
||||
"""))
|
||||
.withRestrictedViewOrderBy(SQL.expression("uuid"))
|
||||
.withUpdatableColumns(
|
||||
|
@ -7,15 +7,15 @@ import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContact;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePerson;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelation;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.repr.Stringify;
|
||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.hibernate.annotations.NotFound;
|
||||
import org.hibernate.annotations.NotFoundAction;
|
||||
|
||||
@ -33,10 +33,10 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_office", name = "partner_rv")
|
||||
@Table(name = "hs_office_partner_rv")
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@ -51,7 +51,7 @@ public class HsOfficePartnerEntity implements Stringifyable, BaseEntity<HsOffice
|
||||
.withIdProp(HsOfficePartnerEntity::toShortString)
|
||||
.withProp(p -> ofNullable(p.getPartnerRel())
|
||||
.map(HsOfficeRelation::getHolder)
|
||||
.map(HsOfficePerson::toShortString)
|
||||
.map(HsOfficePersonEntity::toShortString)
|
||||
.orElse(null))
|
||||
.withProp(p -> ofNullable(p.getPartnerRel())
|
||||
.map(HsOfficeRelation::getContact)
|
||||
@ -110,6 +110,7 @@ public class HsOfficePartnerEntity implements Stringifyable, BaseEntity<HsOffice
|
||||
usingDefaultCase(),
|
||||
directlyFetchedByDependsOnColumn(),
|
||||
dependsOnColumn("partnerRelUuid"))
|
||||
.toRole("partnerRel", AGENT).grantPermission(INSERT)
|
||||
.createPermission(DELETE).grantedTo("partnerRel", OWNER)
|
||||
.createPermission(UPDATE).grantedTo("partnerRel", ADMIN)
|
||||
.createPermission(SELECT).grantedTo("partnerRel", TENANT)
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.office.partner;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
@ -10,17 +9,15 @@ import java.util.UUID;
|
||||
|
||||
public interface HsOfficePartnerRepository extends Repository<HsOfficePartnerEntity, UUID> {
|
||||
|
||||
@Timed("app.office.partners.repo.findByUuid")
|
||||
Optional<HsOfficePartnerEntity> findByUuid(UUID id);
|
||||
|
||||
@Timed("app.office.partners.repo.findAll")
|
||||
List<HsOfficePartnerEntity> findAll(); // TODO.refa: move to a repo in test sources
|
||||
List<HsOfficePartnerEntity> findAll(); // TODO.impl: move to a repo in test sources
|
||||
|
||||
@Query("""
|
||||
SELECT partner FROM HsOfficePartnerEntity partner
|
||||
JOIN HsOfficeRelationRealEntity rel ON rel.uuid = partner.partnerRel.uuid
|
||||
JOIN HsOfficeContactRealEntity contact ON contact.uuid = rel.contact.uuid
|
||||
JOIN HsOfficePersonRealEntity person ON person.uuid = rel.holder.uuid
|
||||
JOIN HsOfficePersonEntity person ON person.uuid = rel.holder.uuid
|
||||
WHERE :name is null
|
||||
OR partner.details.birthName like concat(cast(:name as text), '%')
|
||||
OR contact.caption like concat(cast(:name as text), '%')
|
||||
@ -28,18 +25,12 @@ public interface HsOfficePartnerRepository extends Repository<HsOfficePartnerEnt
|
||||
OR person.givenName like concat(cast(:name as text), '%')
|
||||
OR person.familyName like concat(cast(:name as text), '%')
|
||||
""")
|
||||
@Timed("app.office.partners.repo.findPartnerByOptionalNameLike")
|
||||
List<HsOfficePartnerEntity> findPartnerByOptionalNameLike(String name);
|
||||
HsOfficePartnerEntity findPartnerByPartnerNumber(Integer partnerNumber);
|
||||
|
||||
@Timed("app.office.partners.repo.findPartnerByPartnerNumber")
|
||||
Optional<HsOfficePartnerEntity> findPartnerByPartnerNumber(Integer partnerNumber);
|
||||
|
||||
@Timed("app.office.partners.repo.save")
|
||||
HsOfficePartnerEntity save(final HsOfficePartnerEntity entity);
|
||||
|
||||
@Timed("app.office.partners.repo.count")
|
||||
long count();
|
||||
|
||||
@Timed("app.office.partners.repo.deleteByUuid")
|
||||
int deleteByUuid(UUID uuid);
|
||||
}
|
||||
|
@ -1,79 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.office.person;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.repr.Stringify;
|
||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.MappedSuperclass;
|
||||
import jakarta.persistence.Version;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
|
||||
@MappedSuperclass
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@AllArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@FieldNameConstants
|
||||
@DisplayAs("Person")
|
||||
public class HsOfficePerson<T extends HsOfficePerson<?> & BaseEntity<?>> implements BaseEntity<T>, Stringifyable {
|
||||
|
||||
private static Stringify<HsOfficePerson> toString = stringify(HsOfficePerson.class, "person")
|
||||
.withProp(Fields.personType, HsOfficePerson::getPersonType)
|
||||
.withProp(Fields.tradeName, HsOfficePerson::getTradeName)
|
||||
.withProp(Fields.salutation, HsOfficePerson::getSalutation)
|
||||
.withProp(Fields.title, HsOfficePerson::getTitle)
|
||||
.withProp(Fields.familyName, HsOfficePerson::getFamilyName)
|
||||
.withProp(Fields.givenName, HsOfficePerson::getGivenName);
|
||||
|
||||
@Id
|
||||
@GeneratedValue
|
||||
private UUID uuid;
|
||||
|
||||
@Version
|
||||
private int version;
|
||||
|
||||
@Column(name = "persontype")
|
||||
private HsOfficePersonType personType;
|
||||
|
||||
@Column(name = "tradename")
|
||||
private String tradeName;
|
||||
|
||||
@Column(name = "salutation")
|
||||
private String salutation;
|
||||
|
||||
@Column(name = "title")
|
||||
private String title;
|
||||
|
||||
@Column(name = "familyname")
|
||||
private String familyName;
|
||||
|
||||
@Column(name = "givenname")
|
||||
private String givenName;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toString.apply(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toShortString() {
|
||||
return personType + " " +
|
||||
(!StringUtils.isEmpty(tradeName) ? tradeName : (familyName + ", " + givenName));
|
||||
}
|
||||
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package net.hostsharing.hsadminng.hs.office.person;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficePersonsApi;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonInsertResource;
|
||||
@ -24,21 +23,20 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private StandardMapper mapper;
|
||||
private Mapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficePersonRbacRepository personRepo;
|
||||
private HsOfficePersonRepository personRepo;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.persons.api.getListOfPersons")
|
||||
public ResponseEntity<List<HsOfficePersonResource>> getListOfPersons(
|
||||
public ResponseEntity<List<HsOfficePersonResource>> listPersons(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final String name) {
|
||||
final String caption) {
|
||||
context.define(currentSubject, assumedRoles);
|
||||
|
||||
final var entities = personRepo.findPersonByOptionalNameLike(name);
|
||||
final var entities = personRepo.findPersonByOptionalNameLike(caption);
|
||||
|
||||
final var resources = mapper.mapList(entities, HsOfficePersonResource.class);
|
||||
return ResponseEntity.ok(resources);
|
||||
@ -46,15 +44,14 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.persons.api.postNewPerson")
|
||||
public ResponseEntity<HsOfficePersonResource> postNewPerson(
|
||||
public ResponseEntity<HsOfficePersonResource> addPerson(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final HsOfficePersonInsertResource body) {
|
||||
|
||||
context.define(currentSubject, assumedRoles);
|
||||
|
||||
final var entityToSave = mapper.map(body, HsOfficePersonRbacEntity.class);
|
||||
final var entityToSave = mapper.map(body, HsOfficePersonEntity.class);
|
||||
|
||||
final var saved = personRepo.save(entityToSave);
|
||||
|
||||
@ -69,8 +66,7 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.office.persons.api.getSinglePersonByUuid")
|
||||
public ResponseEntity<HsOfficePersonResource> getSinglePersonByUuid(
|
||||
public ResponseEntity<HsOfficePersonResource> getPersonByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
final UUID personUuid) {
|
||||
@ -86,7 +82,6 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.persons.api.deletePersonByUuid")
|
||||
public ResponseEntity<Void> deletePersonByUuid(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
@ -103,7 +98,6 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.office.persons.api.patchPerson")
|
||||
public ResponseEntity<HsOfficePersonResource> patchPerson(
|
||||
final String currentSubject,
|
||||
final String assumedRoles,
|
||||
|
@ -0,0 +1,102 @@
|
||||
package net.hostsharing.hsadminng.hs.office.person;
|
||||
|
||||
import lombok.*;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@Table(name = "hs_office_person_rv")
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@FieldNameConstants
|
||||
@DisplayAs("Person")
|
||||
public class HsOfficePersonEntity implements BaseEntity<HsOfficePersonEntity>, Stringifyable {
|
||||
|
||||
private static Stringify<HsOfficePersonEntity> toString = stringify(HsOfficePersonEntity.class, "person")
|
||||
.withProp(Fields.personType, HsOfficePersonEntity::getPersonType)
|
||||
.withProp(Fields.tradeName, HsOfficePersonEntity::getTradeName)
|
||||
.withProp(Fields.salutation, HsOfficePersonEntity::getSalutation)
|
||||
.withProp(Fields.title, HsOfficePersonEntity::getTitle)
|
||||
.withProp(Fields.familyName, HsOfficePersonEntity::getFamilyName)
|
||||
.withProp(Fields.givenName, HsOfficePersonEntity::getGivenName);
|
||||
|
||||
@Id
|
||||
@GeneratedValue
|
||||
private UUID uuid;
|
||||
|
||||
@Version
|
||||
private int version;
|
||||
|
||||
@Column(name = "persontype")
|
||||
private HsOfficePersonType personType;
|
||||
|
||||
@Column(name = "tradename")
|
||||
private String tradeName;
|
||||
|
||||
@Column(name = "salutation")
|
||||
private String salutation;
|
||||
|
||||
@Column(name = "title")
|
||||
private String title;
|
||||
|
||||
@Column(name = "familyname")
|
||||
private String familyName;
|
||||
|
||||
@Column(name = "givenname")
|
||||
private String givenName;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toString.apply(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toShortString() {
|
||||
return personType + " " +
|
||||
(!StringUtils.isEmpty(tradeName) ? tradeName : (familyName + ", " + givenName));
|
||||
}
|
||||
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("person", HsOfficePersonEntity.class)
|
||||
.withIdentityView(SQL.projection("concat(tradeName, familyName, givenName)"))
|
||||
.withUpdatableColumns("personType", "title", "salutation", "tradeName", "givenName", "familyName")
|
||||
.toRole(GLOBAL, GUEST).grantPermission(INSERT)
|
||||
|
||||
.createRole(OWNER, (with) -> {
|
||||
with.permission(DELETE);
|
||||
with.owningUser(CREATOR);
|
||||
with.incomingSuperRole(GLOBAL, ADMIN);
|
||||
})
|
||||
.createSubRole(ADMIN, (with) -> {
|
||||
with.permission(UPDATE);
|
||||
})
|
||||
.createSubRole(REFERRER, (with) -> {
|
||||
with.permission(SELECT);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
rbac().generateWithBaseFileName("5-hs-office/502-person/5023-hs-office-person-rbac");
|
||||
}
|
||||
}
|
@ -9,9 +9,9 @@ import java.util.Optional;
|
||||
|
||||
class HsOfficePersonEntityPatcher implements EntityPatcher<HsOfficePersonPatchResource> {
|
||||
|
||||
private final HsOfficePersonRbacEntity entity;
|
||||
private final HsOfficePersonEntity entity;
|
||||
|
||||
HsOfficePersonEntityPatcher(final HsOfficePersonRbacEntity entity) {
|
||||
HsOfficePersonEntityPatcher(final HsOfficePersonEntity entity) {
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
|
@ -1,52 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.office.person;
|
||||
|
||||
import lombok.*;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_office", name = "person_rv")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@FieldNameConstants
|
||||
@DisplayAs("RbacPerson")
|
||||
public class HsOfficePersonRbacEntity extends HsOfficePerson<HsOfficePersonRbacEntity> {
|
||||
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("person", HsOfficePersonRbacEntity.class)
|
||||
.withIdentityView(SQL.projection("concat(tradeName, familyName, givenName)"))
|
||||
.withUpdatableColumns("personType", "title", "salutation", "tradeName", "givenName", "familyName")
|
||||
.toRole(GLOBAL, GUEST).grantPermission(INSERT)
|
||||
|
||||
.createRole(OWNER, (with) -> {
|
||||
with.permission(DELETE);
|
||||
with.owningUser(CREATOR);
|
||||
with.incomingSuperRole(GLOBAL, ADMIN);
|
||||
})
|
||||
.createSubRole(ADMIN, (with) -> {
|
||||
with.permission(UPDATE);
|
||||
})
|
||||
.createSubRole(REFERRER, (with) -> {
|
||||
with.permission(SELECT);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
rbac().generateWithBaseFileName("5-hs-office/502-person/5023-hs-office-person-rbac");
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.office.person;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface HsOfficePersonRbacRepository extends Repository<HsOfficePersonRbacEntity, UUID> {
|
||||
|
||||
@Timed("app.office.persons.repo.findByUuid.rbac")
|
||||
Optional<HsOfficePersonRbacEntity> findByUuid(UUID personUuid);
|
||||
|
||||
@Query("""
|
||||
SELECT p FROM HsOfficePersonRbacEntity p
|
||||
WHERE :name is null
|
||||
OR p.tradeName like concat(cast(:name as text), '%')
|
||||
OR p.givenName like concat(cast(:name as text), '%')
|
||||
OR p.familyName like concat(cast(:name as text), '%')
|
||||
""")
|
||||
@Timed("app.office.persons.repo.findPersonByOptionalNameLike.rbac")
|
||||
List<HsOfficePersonRbacEntity> findPersonByOptionalNameLike(String name);
|
||||
|
||||
@Timed("app.office.persons.repo.save.rbac")
|
||||
HsOfficePersonRbacEntity save(final HsOfficePersonRbacEntity entity);
|
||||
|
||||
@Timed("app.office.persons.repo.deleteByUuid.rbac")
|
||||
int deleteByUuid(final UUID personUuid);
|
||||
|
||||
@Timed("app.office.persons.repo.count.rbac")
|
||||
long count();
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.office.person;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_office", name = "person")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@FieldNameConstants
|
||||
@DisplayAs("RealPerson")
|
||||
public class HsOfficePersonRealEntity extends HsOfficePerson<HsOfficePersonRealEntity> {
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.office.person;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface HsOfficePersonRealRepository extends Repository<HsOfficePersonRealEntity, UUID> {
|
||||
|
||||
@Timed("app.office.persons.repo.findByUuid.real")
|
||||
Optional<HsOfficePersonRealEntity> findByUuid(UUID personUuid);
|
||||
|
||||
@Query("""
|
||||
SELECT p FROM HsOfficePersonRealEntity p
|
||||
WHERE :name is null
|
||||
OR p.tradeName like concat(cast(:name as text), '%')
|
||||
OR p.givenName like concat(cast(:name as text), '%')
|
||||
OR p.familyName like concat(cast(:name as text), '%')
|
||||
""")
|
||||
@Timed("app.office.persons.repo.findPersonByOptionalNameLike.real")
|
||||
List<HsOfficePersonRealEntity> findPersonByOptionalNameLike(String name);
|
||||
|
||||
@Timed("app.office.persons.repo.save.real")
|
||||
HsOfficePersonRealEntity save(final HsOfficePersonRealEntity entity);
|
||||
|
||||
@Timed("app.office.persons.repo.count.real")
|
||||
long count();
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user